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}