v0.3.47 56b82c0

sessionscmd_test.go RawHistoryFolder

1package agent
2
3import (
4	"bytes"
5	"io/fs"
6	"os"
7	"path/filepath"
8	"reflect"
9	"strings"
10	"testing"
11
12	"github.com/patriceckhart/zot/packages/core"
13	"github.com/patriceckhart/zot/packages/provider"
14)
15
16func TestParseSessionSelection(t *testing.T) {
17	tests := []struct {
18		input   string
19		count   int
20		want    []int
21		wantErr bool
22	}{
23		{input: "", count: 3},
24		{input: "none", count: 3},
25		{input: "all", count: 3, want: []int{0, 1, 2}},
26		{input: "1,3-4,3", count: 4, want: []int{0, 2, 3}},
27		{input: "0", count: 3, wantErr: true},
28		{input: "4", count: 3, wantErr: true},
29		{input: "3-2", count: 3, wantErr: true},
30		{input: "1,,2", count: 3, wantErr: true},
31	}
32	for _, tt := range tests {
33		t.Run(tt.input, func(t *testing.T) {
34			got, err := parseSessionSelection(tt.input, tt.count)
35			if (err != nil) != tt.wantErr {
36				t.Fatalf("error = %v, wantErr %v", err, tt.wantErr)
37			}
38			if !reflect.DeepEqual(got, tt.want) {
39				t.Fatalf("selection = %v, want %v", got, tt.want)
40			}
41		})
42	}
43}
44
45func TestSessionsPruneDryRunPreservesSessions(t *testing.T) {
46	home := t.TempDir()
47	t.Setenv("ZOT_HOME", home)
48	missing := filepath.Join(home, "missing-project")
49	existing := t.TempDir()
50	missingPath := createPruneTestSession(t, home, missing)
51	existingPath := createPruneTestSession(t, home, existing)
52
53	var out, errOut bytes.Buffer
54	if err := runSessionsPrune(sessionsPruneOptions{dryRun: true}, strings.NewReader(""), &out, &errOut, os.Stat); err != nil {
55		t.Fatal(err)
56	}
57	if !strings.Contains(out.String(), missing) || strings.Contains(out.String(), existing) {
58		t.Fatalf("output = %q, want only missing cwd", out.String())
59	}
60	if !strings.Contains(out.String(), "dry run: 1 session in 1 directory would be deleted") {
61		t.Fatalf("output = %q, want dry-run summary", out.String())
62	}
63	for _, path := range []string{missingPath, existingPath} {
64		if _, err := os.Stat(path); err != nil {
65			t.Fatalf("dry run removed %s: %v", path, err)
66		}
67	}
68}
69
70func TestSessionsPruneInteractiveDeletesSelectedGroup(t *testing.T) {
71	home := t.TempDir()
72	t.Setenv("ZOT_HOME", home)
73	firstCWD := filepath.Join(home, "gone-a")
74	secondCWD := filepath.Join(home, "gone-b")
75	first := createPruneTestSession(t, home, firstCWD)
76	second := createPruneTestSession(t, home, secondCWD)
77
78	var out, errOut bytes.Buffer
79	if err := runSessionsPrune(sessionsPruneOptions{}, strings.NewReader("2\ny\n"), &out, &errOut, os.Stat); err != nil {
80		t.Fatal(err)
81	}
82	if _, err := os.Stat(first); err != nil {
83		t.Fatalf("unselected session was removed: %v", err)
84	}
85	if _, err := os.Stat(second); !os.IsNotExist(err) {
86		t.Fatalf("selected session still exists or stat failed unexpectedly: %v", err)
87	}
88	if !strings.Contains(out.String(), "deleted 1 session from 1 directory") {
89		t.Fatalf("output = %q", out.String())
90	}
91}
92
93func TestSessionsPrunePreservesGroupWhenDirectoryCheckIsInconclusive(t *testing.T) {
94	home := t.TempDir()
95	t.Setenv("ZOT_HOME", home)
96	cwd := filepath.Join(home, "unavailable-mount")
97	path := createPruneTestSession(t, home, cwd)
98	stat := func(string) (fs.FileInfo, error) { return nil, fs.ErrPermission }
99
100	var out, errOut bytes.Buffer
101	if err := runSessionsPrune(sessionsPruneOptions{all: true, yes: true}, strings.NewReader(""), &out, &errOut, stat); err != nil {
102		t.Fatal(err)
103	}
104	if _, err := os.Stat(path); err != nil {
105		t.Fatalf("session was removed after inconclusive stat: %v", err)
106	}
107	if !strings.Contains(errOut.String(), "permission denied") {
108		t.Fatalf("stderr = %q, want stat warning", errOut.String())
109	}
110	if !strings.Contains(out.String(), "no stale session directories found") {
111		t.Fatalf("output = %q", out.String())
112	}
113}
114
115func TestSessionsPruneRechecksDirectoryBeforeDeleting(t *testing.T) {
116	home := t.TempDir()
117	t.Setenv("ZOT_HOME", home)
118	cwd := filepath.Join(home, "temporarily-missing")
119	path := createPruneTestSession(t, home, cwd)
120	calls := 0
121	stat := func(path string) (fs.FileInfo, error) {
122		calls++
123		if calls == 1 {
124			return nil, fs.ErrNotExist
125		}
126		return nil, nil
127	}
128
129	var out, errOut bytes.Buffer
130	err := runSessionsPrune(sessionsPruneOptions{all: true, yes: true}, strings.NewReader(""), &out, &errOut, stat)
131	if err != nil {
132		t.Fatal(err)
133	}
134	if _, err := os.Stat(path); err != nil {
135		t.Fatalf("session was removed after cwd reappeared: %v", err)
136	}
137	if !strings.Contains(errOut.String(), "directory now exists") {
138		t.Fatalf("stderr = %q, want recheck warning", errOut.String())
139	}
140}
141
142func TestParseSessionsPruneOptionsRequiresAllForYes(t *testing.T) {
143	if _, err := parseSessionsPruneOptions([]string{"--yes"}); err == nil {
144		t.Fatal("--yes without --all was accepted")
145	}
146	opts, err := parseSessionsPruneOptions([]string{"--all", "--yes"})
147	if err != nil {
148		t.Fatal(err)
149	}
150	if !opts.all || !opts.yes {
151		t.Fatalf("options = %#v", opts)
152	}
153}
154
155func createPruneTestSession(t *testing.T, root, cwd string) string {
156	t.Helper()
157	session, err := core.NewSession(root, cwd, "test", "test-model", "test-version")
158	if err != nil {
159		t.Fatal(err)
160	}
161	if err := session.AppendMessage(provider.Message{
162		Role:    provider.RoleUser,
163		Content: []provider.Content{provider.TextBlock{Text: "hello"}},
164	}); err != nil {
165		t.Fatal(err)
166	}
167	if err := session.Close(); err != nil {
168		t.Fatal(err)
169	}
170	return session.Path
171}

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