1package core
2
3import (
4 "os"
5 "path/filepath"
6 "runtime"
7 "strings"
8 "testing"
9 "time"
10
11 "golang.org/x/sys/unix"
12)
13
14func writePruneTestSession(t *testing.T, path, cwd string) {
15 t.Helper()
16 if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
17 t.Fatal(err)
18 }
19 line := `{"type":"meta","meta":{"id":"test-id","cwd":` + strconvQuote(cwd) + `,"model":"m","provider":"p"}}` + "\n"
20 if err := os.WriteFile(path, []byte(line), 0o600); err != nil {
21 t.Fatal(err)
22 }
23}
24
25func markHiddenFromSessions(t *testing.T, path string) {
26 t.Helper()
27 raw, err := os.ReadFile(path)
28 if err != nil {
29 t.Fatal(err)
30 }
31 // The flag lives inside the meta object, not on the row.
32 line := strings.TrimSuffix(strings.TrimSuffix(string(raw), "\n"), "}")
33 line = strings.TrimSuffix(line, "}") + `,"hide_from_sessions":true}}` + "\n"
34 if err := os.WriteFile(path, []byte(line), 0o600); err != nil {
35 t.Fatal(err)
36 }
37}
38
39func makeFifo(path string) error {
40 return unix.Mkfifo(path, 0o600)
41}
42
43func strconvQuote(s string) string {
44 quoted := strings.ReplaceAll(s, `\`, `\\`)
45 quoted = strings.ReplaceAll(quoted, `"`, `\"`)
46 return `"` + quoted + `"`
47}
48
49func TestScanStoredSessionGroupsAbsentRoot(t *testing.T) {
50 groups, issues := ScanStoredSessionGroups(filepath.Join(t.TempDir(), "no-such-root"))
51 if len(groups) != 0 || len(issues) != 0 {
52 t.Fatalf("groups=%v issues=%v, want empty", groups, issues)
53 }
54}
55
56func TestScanStoredSessionGroupsGroupsByCWD(t *testing.T) {
57 root := t.TempDir()
58 writePruneTestSession(t, filepath.Join(root, "one.jsonl"), "/work/a")
59 writePruneTestSession(t, filepath.Join(root, "agents", "named", "two.jsonl"), "/work/a")
60 writePruneTestSession(t, filepath.Join(root, "three.jsonl"), "/work/b")
61
62 groups, issues := ScanStoredSessionGroups(root)
63 if len(issues) != 0 {
64 t.Fatalf("issues=%v, want none", issues)
65 }
66 if len(groups) != 2 {
67 t.Fatalf("groups=%v, want 2", groups)
68 }
69 if groups[0].CWD != "/work/a" || len(groups[0].Paths) != 2 {
70 t.Fatalf("first group=%+v, want /work/a with 2 paths", groups[0])
71 }
72 if groups[1].CWD != "/work/b" || len(groups[1].Paths) != 1 {
73 t.Fatalf("second group=%+v, want /work/b with 1 path", groups[1])
74 }
75 if groups[0].SizeBytes <= 0 {
76 t.Fatalf("group size=%d, want positive", groups[0].SizeBytes)
77 }
78}
79
80func TestScanStoredSessionGroupsPreservesProblemEntries(t *testing.T) {
81 root := t.TempDir()
82 writePruneTestSession(t, filepath.Join(root, "ok.jsonl"), "/work/a")
83 writePruneTestSession(t, filepath.Join(root, "bad.jsonl"), "")
84 if err := os.WriteFile(filepath.Join(root, "broken.jsonl"), []byte("{not json\n"), 0o600); err != nil {
85 t.Fatal(err)
86 }
87 link := filepath.Join(root, "link.jsonl")
88 if err := os.Symlink(filepath.Join(root, "ok.jsonl"), link); err != nil {
89 t.Skipf("symlinks unsupported: %v", err)
90 }
91
92 groups, issues := ScanStoredSessionGroups(root)
93 if len(groups) != 1 || len(groups[0].Paths) != 1 {
94 t.Fatalf("groups=%v, want only the valid session", groups)
95 }
96 if len(issues) != 3 {
97 t.Fatalf("issues=%v, want empty-cwd, malformed, and symlink", issues)
98 }
99 for i := 1; i < len(issues); i++ {
100 if issues[i-1].Path > issues[i].Path {
101 t.Fatalf("issues unsorted: %v", issues)
102 }
103 }
104}
105
106func TestScanStoredSessionGroupsSkipsNonRegularEntries(t *testing.T) {
107 if runtime.GOOS == "windows" {
108 t.Skip("fifo tests need unix privileges")
109 }
110 root := t.TempDir()
111 writePruneTestSession(t, filepath.Join(root, "ok.jsonl"), "/work/a")
112 fifo := filepath.Join(root, "pipe.jsonl")
113 if err := makeFifo(fifo); err != nil {
114 t.Skipf("fifos unsupported: %v", err)
115 }
116
117 done := make(chan struct{})
118 go func() {
119 defer close(done)
120 groups, issues := ScanStoredSessionGroups(root)
121 if len(groups) != 1 || len(groups[0].Paths) != 1 {
122 t.Errorf("groups=%v, want only the valid session", groups)
123 return
124 }
125 found := false
126 for _, issue := range issues {
127 if issue.Path == fifo {
128 found = true
129 }
130 }
131 if !found {
132 t.Errorf("issues=%v, want the fifo reported", issues)
133 }
134 }()
135 select {
136 case <-done:
137 case <-time.After(10 * time.Second):
138 t.Fatal("scanner blocked on a fifo entry")
139 }
140}
141
142func TestScanStoredSessionGroupsSkipsHiddenBranches(t *testing.T) {
143 root := t.TempDir()
144 writePruneTestSession(t, filepath.Join(root, "visible.jsonl"), "/work/a")
145 hidden := filepath.Join(root, "branch.jsonl")
146 writePruneTestSession(t, hidden, "/work/a")
147 markHiddenFromSessions(t, hidden)
148
149 groups, _ := ScanStoredSessionGroups(root)
150 if len(groups) != 1 || len(groups[0].Paths) != 1 {
151 t.Fatalf("groups=%v, want only the visible session", groups)
152 }
153}
154
155func TestScanStoredSessionGroupsReportsSymlinkedRoot(t *testing.T) {
156 if runtime.GOOS == "windows" {
157 t.Skip("symlink tests need privileges on windows")
158 }
159 base := t.TempDir()
160 real := filepath.Join(base, "real")
161 if err := os.MkdirAll(real, 0o755); err != nil {
162 t.Fatal(err)
163 }
164 writePruneTestSession(t, filepath.Join(real, "one.jsonl"), "/work/a")
165 link := filepath.Join(base, "link")
166 if err := os.Symlink(real, link); err != nil {
167 t.Skipf("symlinks unsupported: %v", err)
168 }
169
170 groups, issues := ScanStoredSessionGroups(link)
171 if len(groups) != 0 {
172 t.Fatalf("groups=%v, want none for a symlinked root", groups)
173 }
174 if len(issues) != 1 {
175 t.Fatalf("issues=%v, want the symlinked root reported", issues)
176 }
177}