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