v0.50.1 a637010

session_picker_test.go RawHistoryFolder

1package core
2
3import (
4	"context"
5	"os"
6	"path/filepath"
7	"testing"
8
9	"github.com/bnema/zut/packages/provider"
10)
11
12func TestListSessionPathsContextScopesAndValidatesBuckets(t *testing.T) {
13	root := t.TempDir()
14	cwdA := filepath.Join(t.TempDir(), "workspace-a")
15	cwdB := filepath.Join(t.TempDir(), "workspace-b")
16
17	newSession := func(cwd, text string) *Session {
18		t.Helper()
19		s, err := NewSession(root, cwd, "provider", "model", "test")
20		if err != nil {
21			t.Fatal(err)
22		}
23		if err := s.AppendMessage(provider.Message{Role: provider.RoleUser, Content: []provider.Content{provider.TextBlock{Text: text}}}); err != nil {
24			t.Fatal(err)
25		}
26		if err := s.Close(); err != nil {
27			t.Fatal(err)
28		}
29		return s
30	}
31
32	a := newSession(cwdA, "from a")
33	b := newSession(cwdB, "from b")
34
35	if got := ListSessionPathsContext(context.Background(), root, cwdA, false); len(got) != 1 || got[0] != a.Path {
36		t.Fatalf("current scope = %q, want %q", got, a.Path)
37	}
38	got := ListSessionPathsContext(context.Background(), root, cwdA, true)
39	if len(got) != 2 || !containsPath(got, a.Path) || !containsPath(got, b.Path) {
40		t.Fatalf("all scope = %q, want both valid sessions", got)
41	}
42
43	invalidDir := filepath.Join(root, "sessions", "not-a-cwd-bucket")
44	if err := os.MkdirAll(invalidDir, 0o755); err != nil {
45		t.Fatal(err)
46	}
47	if err := os.WriteFile(filepath.Join(invalidDir, "invalid.jsonl"), []byte("{}\n"), 0o644); err != nil {
48		t.Fatal(err)
49	}
50	wrongBucket := filepath.Join(root, "sessions", "0123456789abcdef")
51	if err := os.MkdirAll(wrongBucket, 0o755); err != nil {
52		t.Fatal(err)
53	}
54	contents, err := os.ReadFile(a.Path)
55	if err != nil {
56		t.Fatal(err)
57	}
58	if err := os.WriteFile(filepath.Join(wrongBucket, "wrong.jsonl"), contents, 0o644); err != nil {
59		t.Fatal(err)
60	}
61	if got := ListSessionPathsContext(context.Background(), root, cwdA, true); len(got) != 2 {
62		t.Fatalf("all scope included invalid session paths: %q", got)
63	}
64}
65
66func TestManagedSessionMetaRejectsWrongBucket(t *testing.T) {
67	root := t.TempDir()
68	cwd := t.TempDir()
69	session, err := NewSession(root, cwd, "provider", "model", "test")
70	if err != nil {
71		t.Fatal(err)
72	}
73	if err := session.AppendMessage(provider.Message{Role: provider.RoleUser, Content: []provider.Content{provider.TextBlock{Text: "keep this session"}}}); err != nil {
74		t.Fatal(err)
75	}
76	if err := session.Close(); err != nil {
77		t.Fatal(err)
78	}
79	if _, err := ManagedSessionMeta(context.Background(), root, session.Path); err != nil {
80		t.Fatalf("managed session metadata: %v", err)
81	}
82	wrongPath := filepath.Join(root, "sessions", "0123456789abcdef", "moved.jsonl")
83	if err := os.MkdirAll(filepath.Dir(wrongPath), 0o755); err != nil {
84		t.Fatal(err)
85	}
86	contents, err := os.ReadFile(session.Path)
87	if err != nil {
88		t.Fatal(err)
89	}
90	if err := os.WriteFile(wrongPath, contents, 0o644); err != nil {
91		t.Fatal(err)
92	}
93	if _, err := ManagedSessionMeta(context.Background(), root, wrongPath); err == nil {
94		t.Fatal("wrong-bucket session was accepted")
95	}
96}
97
98func TestReadSessionSearchSegmentsIncludesOnlyUserAssistantText(t *testing.T) {
99	root := t.TempDir()
100	cwd := t.TempDir()
101	s, err := NewSession(root, cwd, "provider", "model", "test")
102	if err != nil {
103		t.Fatal(err)
104	}
105	for _, msg := range []provider.Message{
106		{Role: provider.RoleUser, Content: []provider.Content{provider.TextBlock{Text: "find this user sentence"}}},
107		{Role: provider.RoleAssistant, Content: []provider.Content{provider.TextBlock{Text: "find this assistant sentence"}, provider.ToolCallBlock{ID: "call", Name: "secret-tool"}}},
108		{Role: provider.RoleTool, Content: []provider.Content{provider.ToolResultBlock{CallID: "call", Content: []provider.Content{provider.TextBlock{Text: "never expose tool result"}}}}},
109		{Role: provider.RoleDeveloper, Content: []provider.Content{provider.TextBlock{Text: "never expose developer text"}}},
110	} {
111		if err := s.AppendMessage(msg); err != nil {
112			t.Fatal(err)
113		}
114	}
115	if err := s.Close(); err != nil {
116		t.Fatal(err)
117	}
118
119	segments, err := ReadSessionSearchSegments(context.Background(), s.Path)
120	if err != nil {
121		t.Fatal(err)
122	}
123	if len(segments) != 2 {
124		t.Fatalf("segments = %#v, want exactly user and assistant text", segments)
125	}
126	if segments[0].Text != "find this user sentence" || segments[1].Text != "find this assistant sentence" {
127		t.Fatalf("segments = %#v", segments)
128	}
129}
130
131func containsPath(paths []string, want string) bool {
132	for _, path := range paths {
133		if path == want {
134			return true
135		}
136	}
137	return false
138}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close