返回 DeepSeek-Reasonix
historical_import_regression_test.go
根目录 / desktop / historical_import_regression_test.go
1 package main
2
3 import (
4 "encoding/json"
5 "os"
6 "path/filepath"
7 "sync"
8 "testing"
9
10 "reasonix/internal/config"
11 "reasonix/internal/identitylock"
12 "reasonix/internal/session"
13 )
14
15 func TestHistoricalRegressionContentReadySurvivesSourceMove(t *testing.T) {
16 isolateDesktopUserDirs(t)
17 root := config.SessionStoreDir()
18 const id = "review-content-ready"
19 old := coldV4MigrationFixture(t, root, id)
20 app := newHistoricalLifecycleApp(t)
21 sourceID := historicalLifecycleID(t, app, id)
22 workspace, err := app.ensureDesktopWorkspace(t.Context(), "global", "")
23 if err != nil {
24 t.Fatal(err)
25 }
26 path := filepath.Join(root, id)
27 fingerprint, err := desktopSourceFingerprint(path)
28 if err != nil {
29 t.Fatal(err)
30 }
31 opID, err := app.prepareDesktopImport(t.Context(), desktopMigrationSource{scope: "global"}, path, fingerprint, id, workspace)
32 if err != nil {
33 t.Fatal(err)
34 }
35 bundle := filepath.Join(t.TempDir(), "bundle")
36 if err := old.Export(t.Context(), session.SessionRef{HostID: "migration-source", SessionID: id}, bundle); err != nil {
37 t.Fatal(err)
38 }
39 if _, err := app.desktopSessionService("").ImportWithHeader(t.Context(), bundle, session.CreateOptions{SessionID: id, CWD: globalWorkspaceRoot(), Origin: session.SessionOriginCanonicalImport}); err != nil {
40 t.Fatal(err)
41 }
42 if err := app.commitDesktopImport(t.Context(), desktopMigrationSource{scope: "global", operationID: opID, deferArchive: true}, path, "canonical", fingerprint, id, workspace); err != nil {
43 t.Fatal(err)
44 }
45 if err := os.Rename(path, filepath.Join(t.TempDir(), "moved-source")); err != nil {
46 t.Fatal(err)
47 }
48 app.stopHistoricalImports()
49 app.closeSessionServices()
50 app = newHistoricalLifecycleApp(t)
51 release, err := identitylock.Acquire(t.Context(), filepath.Join(root, "."+id+".ownership.lock"))
52 if err != nil {
53 t.Fatal(err)
54 }
55 defer release()
56 if _, err := app.ImportHistoricalSession(sourceID); err != nil {
57 t.Fatalf("valid content_ready target cannot recover after source moved: %v", err)
58 }
59 }
60
61 func TestHistoricalRegressionColdV4VisibleInNormalLists(t *testing.T) {
62 isolateDesktopUserDirs(t)
63 coldV4MigrationFixture(t, config.SessionStoreDir(), "review-visible")
64 app := newHistoricalLifecycleApp(t)
65 installSessionCatalogForTest(t, app, config.SessionDir(), "global", "")
66 management, err := app.ListHistoricalSessions()
67 if err != nil || len(management.Items) != 1 {
68 t.Fatalf("fixture missing from historical management: %+v %v", management, err)
69 }
70 page, err := app.ListProjectTopics(ProjectTopicPageRequest{Scope: "global", Limit: 50})
71 if err != nil {
72 t.Fatal(err)
73 }
74 if len(page.Items) != 1 {
75 t.Errorf("cold v4 source absent from sidebar: %+v", page.Items)
76 }
77 rows := app.listSessionsFromDir(config.SessionDir(), "")
78 if len(rows) != 1 {
79 t.Errorf("cold v4 source absent from history: %+v", rows)
80 }
81 if len(page.Items) != 1 {
82 return
83 }
84 selector := SessionSelector{Source: page.Items[0].Source}
85 if _, err := app.RenameSessionTarget(selector, "Renamed historical source"); err != nil {
86 t.Fatal(err)
87 }
88 if err := app.SetSessionPinned(selector, true); err != nil {
89 t.Fatal(err)
90 }
91 shells := app.mergeCanonicalWorkspaceShells([]ProjectNode{{Kind: "global_folder", Key: "global_folder"}})
92 if len(shells) != 1 || len(shells[0].Children) != 1 || !shells[0].Children[0].Pinned {
93 t.Fatalf("source-only workspace lost its pinned shell: %+v", shells)
94 }
95 filtered, err := app.ListProjectTopics(ProjectTopicPageRequest{Scope: "global", Query: "Renamed historical", Limit: 1})
96 if err != nil || len(filtered.Items) != 1 || !filtered.Items[0].Pinned {
97 t.Fatalf("display overrides missing from search: %+v %v", filtered, err)
98 }
99 prepared, err := app.PrepareSession(selector)
100 if err != nil {
101 t.Fatal(err)
102 }
103 app.historicalImports.mu.Lock()
104 call := app.historicalImports.operations[prepared.OperationID]
105 app.historicalImports.mu.Unlock()
106 result, err := waitHistoricalImport(call)
107 if err != nil {
108 t.Fatal(err)
109 }
110 page, err = app.ListProjectTopics(ProjectTopicPageRequest{Scope: "global", Limit: 50})
111 if err != nil || len(page.Items) != 1 || page.Items[0].Session == nil || !page.Items[0].Pinned {
112 t.Fatalf("adoption duplicated source or lost pin: %+v %v", page, err)
113 }
114 if err := app.ArchiveCanonicalSession(result.Session); err != nil {
115 t.Fatal(err)
116 }
117 page, err = app.ListProjectTopics(ProjectTopicPageRequest{Scope: "global", Limit: 50})
118 if err != nil || len(page.Items) != 0 {
119 t.Fatalf("retained source revived after archive: %+v %v", page, err)
120 }
121 }
122
123 func TestHistoricalRegressionPinHistoricalSourceImmediatelyVisible(t *testing.T) {
124 isolateDesktopUserDirs(t)
125 path, _, _ := migrationSingleDAGFixture(t)
126 app := newHistoricalLifecycleApp(t)
127 installSessionCatalogForTest(t, app, filepath.Dir(path), "global", "")
128 req := ProjectTopicPageRequest{Scope: "global", Limit: 50}
129 page, err := app.ListProjectTopics(req)
130 if err != nil || len(page.Items) != 1 || page.Items[0].Source == nil {
131 t.Fatalf("fixture: %+v %v", page, err)
132 }
133 if err := app.SetSessionPinned(SessionSelector{Source: page.Items[0].Source}, true); err != nil {
134 t.Fatal(err)
135 }
136 page, err = app.ListProjectTopics(req)
137 if err != nil {
138 t.Fatal(err)
139 }
140 if len(page.Items) != 1 || !page.Items[0].Pinned {
141 t.Fatalf("successful pin not projected before preparation: %+v", page.Items)
142 }
143 }
144
145 func TestHistoricalRegressionStartupPublishesScopedCatalogWithoutImport(t *testing.T) {
146 isolateDesktopUserDirs(t)
147 workspace := t.TempDir()
148 if err := saveProjectsFile(desktopProjectFile{Projects: []desktopProject{{Root: workspace}}}); err != nil {
149 t.Fatal(err)
150 }
151 coldV4MigrationFixture(t, config.SessionStoreDir(), "global-source")
152 coldV4MigrationFixture(t, config.ProjectSessionStoreDir(workspace), "project-source")
153 app := newHistoricalLifecycleApp(t)
154 release, err := identitylock.Acquire(t.Context(), filepath.Join(config.SessionStoreDir(), ".global-source.ownership.lock"))
155 if err != nil {
156 t.Fatal(err)
157 }
158 defer release()
159 runHistoryDiscoveryStartup(t, app)
160 app.ctx = t.Context() // The startup helper cancels its watchdog context on return.
161 // No management RPC or explicit preparation has been called.
162 for _, scope := range []string{"global", "project"} {
163 root := ""
164 if scope == "project" {
165 root = workspace
166 }
167 page, err := app.ListProjectTopics(ProjectTopicPageRequest{Scope: scope, WorkspaceRoot: root, Limit: 50})
168 if err != nil || len(page.Items) != 1 || !page.Items[0].Historical || page.Items[0].Label != scope+"-source" {
169 t.Fatalf("startup catalog for %s: %+v %v", scope, page, err)
170 }
171 }
172 if rows := app.listSessionsFromDir(t.TempDir(), ""); len(rows) != 0 {
173 t.Fatalf("unknown directory leaked global sources: %+v", rows)
174 }
175 state, err := app.workspaceRegistry().Load(t.Context())
176 if err != nil || len(state.SourceMappings) != 0 || len(state.PendingOperations) != 0 {
177 t.Fatalf("discovery started a conversion: %+v %v", state, err)
178 }
179 }
180
181 func TestHistoricalRegressionShutdownPreservesPendingBatch(t *testing.T) {
182 isolateDesktopUserDirs(t)
183 coldV4MigrationFixture(t, config.SessionStoreDir(), "review-first")
184 coldV4MigrationFixture(t, config.SessionStoreDir(), "review-second")
185 app := newHistoricalLifecycleApp(t)
186 first := historicalLifecycleID(t, app, "review-first")
187 second := historicalLifecycleID(t, app, "review-second")
188 entered, proceed := make(chan struct{}), make(chan struct{})
189 var once sync.Once
190 release := func() { once.Do(func() { close(proceed) }) }
191 t.Cleanup(release)
192 app.desktopSessions.beforeMigrationRegistryCommit = func() error { close(entered); <-proceed; return nil }
193 if _, err := app.StartHistoricalImport([]string{first, second}); err != nil {
194 t.Fatal(err)
195 }
196 <-entered
197 stopped := make(chan struct{})
198 go func() { app.stopHistoricalImports(); close(stopped) }()
199 <-app.historicalImports.ctx.Done()
200 release()
201 <-stopped
202 awaitHistoricalBatch(t, app)
203 data, err := os.ReadFile(historicalImportQueuePath())
204 if err != nil {
205 t.Fatal(err)
206 }
207 var saved historicalImportQueueSidecar
208 if err := json.Unmarshal(data, &saved); err != nil {
209 t.Fatal(err)
210 }
211 if len(saved.Queue) != 1 || saved.Queue[0] != second {
212 t.Fatalf("shutdown lost pending second selection: current=%q queue=%v", saved.Current, saved.Queue)
213 }
214 app.closeSessionServices()
215 restarted := newHistoricalLifecycleApp(t)
216 status, err := restarted.ListHistoricalSessions()
217 if err != nil || !status.Paused || status.Running || status.Remaining != 2 {
218 t.Fatalf("restart did not retain paused batch: %+v %v", status, err)
219 }
220 if _, err := restarted.ControlHistoricalImport("resume"); err != nil {
221 t.Fatal(err)
222 }
223 status = awaitHistoricalBatch(t, restarted)
224 if status.Completed != 2 || status.Remaining != 0 {
225 t.Fatalf("manual continuation failed: %+v", status)
226 }
227 }
228
229 func TestHistoricalRegressionShutdownDrainsStartupDiscovery(t *testing.T) {
230 isolateDesktopUserDirs(t)
231 app := newHistoricalLifecycleApp(t)
232 c := &app.historicalImports
233 c.discoveryMu.Lock()
234 app.startDesktopSessionMigration(t.Context())
235 stopped := make(chan struct{})
236 go func() { app.stopHistoricalImports(); close(stopped) }()
237 <-c.ctx.Done()
238 select {
239 case <-stopped:
240 c.discoveryMu.Unlock()
241 t.Fatal("shutdown did not drain startup discovery")
242 default:
243 }
244 c.discoveryMu.Unlock()
245 <-stopped
246 select {
247 case <-app.desktopMigrationDone:
248 default:
249 t.Fatal("startup recovery outlived the coordinator")
250 }
251 }
252
253 func TestHistoricalRegressionTwoInstancesPreservePresentationWrites(t *testing.T) {
254 isolateDesktopUserDirs(t)
255 a, b := newHistoricalLifecycleApp(t), newHistoricalLifecycleApp(t)
256 if _, err := a.ListHistoricalSessions(); err != nil {
257 t.Fatal(err)
258 }
259 if _, err := b.ListHistoricalSessions(); err != nil {
260 t.Fatal(err)
261 }
262 if err := a.saveHistoricalSourcePresentation("source-a", func(p *historicalSourcePresentation) { p.Title = "title-a" }); err != nil {
263 t.Fatal(err)
264 }
265 if err := b.saveHistoricalSourcePresentation("source-b", func(p *historicalSourcePresentation) { p.Title = "title-b" }); err != nil {
266 t.Fatal(err)
267 }
268 data, err := os.ReadFile(historicalImportQueuePath())
269 if err != nil {
270 t.Fatal(err)
271 }
272 var saved historicalImportQueueSidecar
273 if err := json.Unmarshal(data, &saved); err != nil {
274 t.Fatal(err)
275 }
276 if saved.Presentations["source-a"].Title != "title-a" {
277 t.Fatalf("second Desktop erased first title: %+v", saved.Presentations)
278 }
279 }
280
281 func TestHistoricalRegressionUpdateCheckRejectsActiveWriter(t *testing.T) {
282 isolateDesktopUserDirs(t)
283 const id = "review-writer"
284 old := coldV4MigrationFixture(t, config.SessionStoreDir(), id)
285 app := newHistoricalLifecycleApp(t)
286 sourceID := historicalLifecycleID(t, app, id)
287 if _, err := app.ImportHistoricalSession(sourceID); err != nil {
288 t.Fatal(err)
289 }
290 binding, err := old.Open(t.Context(), session.SessionRef{HostID: "migration-source", SessionID: id})
291 if err != nil {
292 t.Fatal(err)
293 }
294 defer binding.Release(t.Context())
295 defer old.Close(t.Context(), binding.Runtime().Ref())
296 result := app.checkHistoricalSourceUpdate(t.Context(), sourceID, historicalSource{path: filepath.Join(config.SessionStoreDir(), id), format: "canonical", scope: "global"})
297 if result.Status != "blocked" {
298 t.Fatalf("source checked while writer owns runtime: %+v", result)
299 }
300 }
301
301 lines GO