返回 DeepSeek-Reasonix
project_root_catalog_test.go
根目录 / desktop / project_root_catalog_test.go
1 package main
2
3 import (
4 "context"
5 "os"
6 "path/filepath"
7 "sync"
8 "testing"
9 "time"
10
11 "reasonix/internal/sessioncatalog"
12 )
13
14 func TestSessionCatalogTargetsIncludeRestoredProjectTab(t *testing.T) {
15 isolateDesktopUserDirs(t)
16 app := NewApp()
17 root := t.TempDir()
18 sessionDir := desktopSessionDir(root)
19 app.tabs["restored"] = &WorkspaceTab{
20 ID: "restored", Scope: "project", WorkspaceRoot: root,
21 SessionPath: filepath.Join(sessionDir, "restored.jsonl"),
22 }
23 for _, target := range app.sessionCatalogTargets() {
24 if target.Path == sessionDir && target.Scope == "project" && target.WorkspaceRoot == root {
25 return
26 }
27 }
28 t.Fatalf("session catalog targets = %#v, want restored project directory %q", app.sessionCatalogTargets(), sessionDir)
29 }
30
31 func waitForCatalogSessionPath(t *testing.T, app *App, workspaceRoot, sessionDir, sessionPath string) sessioncatalog.SessionRecord {
32 t.Helper()
33 deadline := time.Now().Add(15 * time.Second)
34 var last []sessioncatalog.SessionRecord
35 for time.Now().Before(deadline) {
36 catalog := app.sessionCatalog.Load()
37 if catalog == nil {
38 t.Fatal("session catalog is not installed")
39 }
40 page, err := catalog.ListSessions(context.Background(), sessioncatalog.SessionPageRequest{
41 Scope: "project", WorkspaceRoot: workspaceRoot, Directory: sessionDir, Limit: 20,
42 })
43 if err != nil {
44 t.Fatalf("list project sessions: %v", err)
45 }
46 last = page.Items
47 for _, item := range page.Items {
48 if item.Path == sessionPath {
49 return item
50 }
51 }
52 time.Sleep(10 * time.Millisecond)
53 }
54 t.Fatalf("project session %q did not become visible: %#v", sessionPath, last)
55 return sessioncatalog.SessionRecord{}
56 }
57
58 func TestRegisterProjectRootReconcilesNewProjectSessionDirectory(t *testing.T) {
59 isolateDesktopUserDirs(t)
60 app := NewApp()
61 globalDir := desktopSessionDir(globalWorkspaceRoot())
62 if err := os.MkdirAll(globalDir, 0o755); err != nil {
63 t.Fatalf("mkdir global session dir: %v", err)
64 }
65 installSessionCatalogForTest(t, app, globalDir, "global", "")
66
67 root := t.TempDir()
68 sessionDir := desktopSessionDir(root)
69 if err := os.MkdirAll(sessionDir, 0o755); err != nil {
70 t.Fatalf("mkdir project session dir: %v", err)
71 }
72 sessionPath := writeTopicSession(t, sessionDir, "existing.jsonl", "topic-existing", "Existing topic", root)
73
74 app.registerProjectRoot(root)
75
76 session := waitForCatalogSessionPath(t, app, root, sessionDir, sessionPath)
77 if session.TopicTitle != "Existing topic" {
78 t.Fatalf("session topic title = %q, want Existing topic", session.TopicTitle)
79 }
80 }
81
82 func TestRegisterProjectRootDoesNotRescanExistingProjectOnActivation(t *testing.T) {
83 isolateDesktopUserDirs(t)
84 app := NewApp()
85 globalDir := desktopSessionDir(globalWorkspaceRoot())
86 if err := os.MkdirAll(globalDir, 0o755); err != nil {
87 t.Fatal(err)
88 }
89 installSessionCatalogForTest(t, app, globalDir, "global", "")
90 root := t.TempDir()
91 if err := os.MkdirAll(desktopSessionDir(root), 0o755); err != nil {
92 t.Fatal(err)
93 }
94
95 started := make(chan struct{}, 1)
96 release := make(chan struct{})
97 done := make(chan struct{}, 1)
98 app.catalogReconcileHook = func(sessioncatalog.DirectoryTarget) {
99 started <- struct{}{}
100 <-release
101 }
102 app.catalogReconcileDoneHook = func(sessioncatalog.DirectoryTarget) { done <- struct{}{} }
103 app.registerProjectRoot(root)
104 <-started
105 for range 25 {
106 app.registerProjectRoot(root)
107 }
108 close(release)
109 <-done
110 select {
111 case <-started:
112 t.Fatal("activating an already registered project scheduled another full reconcile")
113 default:
114 }
115 }
116
117 func TestExplicitReconcileCoalescesConcurrentRequests(t *testing.T) {
118 isolateDesktopUserDirs(t)
119 app := NewApp()
120 dir := t.TempDir()
121 installSessionCatalogForTest(t, app, dir, "global", "")
122
123 started := make(chan struct{}, 2)
124 releaseFirst := make(chan struct{})
125 done := make(chan struct{}, 1)
126 var calls int
127 var callsMu sync.Mutex
128 app.catalogReconcileHook = func(sessioncatalog.DirectoryTarget) {
129 callsMu.Lock()
130 calls++
131 call := calls
132 callsMu.Unlock()
133 started <- struct{}{}
134 if call == 1 {
135 <-releaseFirst
136 }
137 }
138 app.catalogReconcileDoneHook = func(sessioncatalog.DirectoryTarget) { done <- struct{}{} }
139 app.requestSessionCatalogReconcile(dir)
140 <-started
141 for range 25 {
142 app.requestSessionCatalogReconcile(dir)
143 }
144 close(releaseFirst)
145 <-started
146 <-done
147 callsMu.Lock()
148 defer callsMu.Unlock()
149 if calls != 2 {
150 t.Fatalf("reconcile passes = %d, want one active pass plus one coalesced dirty pass", calls)
151 }
152 }
153
154 func TestStopSessionCatalogWaitsForExplicitReconcile(t *testing.T) {
155 isolateDesktopUserDirs(t)
156 app := NewApp()
157 dir := t.TempDir()
158 installSessionCatalogForTest(t, app, dir, "global", "")
159
160 started := make(chan struct{})
161 release := make(chan struct{})
162 app.catalogReconcileHook = func(sessioncatalog.DirectoryTarget) {
163 close(started)
164 <-release
165 }
166 app.requestSessionCatalogReconcile(dir)
167 <-started
168 stopped := make(chan bool, 1)
169 go func() {
170 stopped <- app.stopSessionCatalog(time.Second)
171 }()
172 select {
173 case <-stopped:
174 t.Fatal("catalog stopped before its explicit reconcile released")
175 case <-time.After(25 * time.Millisecond):
176 }
177 close(release)
178 select {
179 case clean := <-stopped:
180 if !clean {
181 t.Fatal("catalog stop reported an incomplete drain after reconcile released")
182 }
183 case <-time.After(time.Second):
184 t.Fatal("catalog stop did not drain its explicit reconcile")
185 }
186 }
187
187 lines GO