返回 DeepSeek-Reasonix
historical_catalog_refresh_test.go
根目录 / desktop / historical_catalog_refresh_test.go
1 package main
2
3 import (
4 "context"
5 "errors"
6 "fmt"
7 "os"
8 "path/filepath"
9 "sync/atomic"
10 "testing"
11 "time"
12
13 "reasonix/internal/config"
14 "reasonix/internal/historywork"
15 )
16
17 func TestHistoricalCatalogPublishesBoundedDiscoveryBeforeCompletion(t *testing.T) {
18 isolateDesktopUserDirs(t)
19 app := newHistoricalLifecycleApp(t)
20 root := config.SessionStoreDir()
21 for i := range historywork.BatchEntries + 1 {
22 path := filepath.Join(root, fmt.Sprintf("progress-%04d", i))
23 if err := os.MkdirAll(path, 0700); err != nil {
24 t.Fatal(err)
25 }
26 // An identifiable but damaged source is checked in bounded batches;
27 // its recovery evidence survives cancellation without becoming a row.
28 if err := os.WriteFile(filepath.Join(path, "manifest.json"), []byte("{unread"), 0600); err != nil {
29 t.Fatal(err)
30 }
31 }
32 ctx, cancel := context.WithCancel(t.Context())
33 defer cancel()
34 notifications := 0
35 firstCount := 0
36 app.projectTreeChangedHook = func() {
37 notifications++
38 app.historicalImports.mu.Lock()
39 count := len(app.historicalImports.catalog)
40 app.historicalImports.mu.Unlock()
41 firstCount = count
42 if count == 0 || count > historywork.BatchEntries {
43 t.Errorf("first publication count=%d", count)
44 }
45 cancel()
46 }
47 _, err := app.discoverHistoricalSessions(ctx, false)
48 if !errors.Is(err, context.Canceled) || notifications != 1 {
49 t.Fatalf("canceled partial discovery: notifications=%d err=%v", notifications, err)
50 }
51 app.historicalImports.mu.Lock()
52 defer app.historicalImports.mu.Unlock()
53 if len(app.historicalImports.catalog) != firstCount {
54 t.Fatal("cancellation removed previously published entries")
55 }
56 for _, entry := range app.historicalImports.catalog {
57 if entry.node.Health != "unavailable" {
58 t.Fatal("damaged metadata was advertised as a usable session")
59 }
60 }
61 }
62
63 func refreshHistoricalCatalogForTest(app *App) {
64 c := &app.historicalImports
65 c.mu.Lock()
66 c.catalogEnabled = true
67 c.catalogAt = time.Time{}
68 c.mu.Unlock()
69 app.requestHistoricalCatalog()
70 c.workers.Wait()
71 }
72
73 func TestHistoricalCatalogRefreshDoesNotInvalidateUnchangedHistory(t *testing.T) {
74 isolateDesktopUserDirs(t)
75 for _, id := range []string{"unchanged-history", "second-history", "third-history"} {
76 coldV4MigrationFixture(t, config.SessionStoreDir(), id)
77 }
78 app := newHistoricalLifecycleApp(t)
79 if _, err := app.ListHistoricalSessions(); err != nil {
80 t.Fatal(err)
81 }
82 var notifications, scans atomic.Int32
83 app.projectTreeChangedHook = func() { notifications.Add(1) }
84 app.projectTreeCatalogRefreshHook = func() { scans.Add(1) }
85 for range 3 {
86 refreshHistoricalCatalogForTest(app)
87 }
88 if notifications.Load() != 0 || scans.Load() != 0 {
89 t.Fatalf("unchanged discovery invalidated history: notifications=%d broad scans=%d", notifications.Load(), scans.Load())
90 }
91 }
92
93 func TestHistoricalCatalogRefreshPublishesChangesWithoutRescanningLegacyCatalog(t *testing.T) {
94 isolateDesktopUserDirs(t)
95 app := newHistoricalLifecycleApp(t)
96 if _, err := app.ListHistoricalSessions(); err != nil {
97 t.Fatal(err)
98 }
99 var notifications, scans atomic.Int32
100 app.projectTreeChangedHook = func() { notifications.Add(1) }
101 app.projectTreeCatalogRefreshHook = func() { scans.Add(1) }
102 root := config.SessionStoreDir()
103 coldV4MigrationFixture(t, root, "new-history")
104 refreshHistoricalCatalogForTest(app)
105 if notifications.Load() != 1 || scans.Load() != 0 {
106 t.Fatalf("discovery must publish only its changed view: notifications=%d broad scans=%d", notifications.Load(), scans.Load())
107 }
108 if err := os.Rename(filepath.Join(root, "new-history"), filepath.Join(t.TempDir(), "removed-history")); err != nil {
109 t.Fatal(err)
110 }
111 refreshHistoricalCatalogForTest(app)
112 if notifications.Load() != 2 || scans.Load() != 0 {
113 t.Fatalf("source removal did not publish exactly once: notifications=%d broad scans=%d", notifications.Load(), scans.Load())
114 }
115 }
116
117 func TestHistoricalCatalogFailedRefreshDoesNotImmediatelyRetry(t *testing.T) {
118 isolateDesktopUserDirs(t)
119 app := newHistoricalLifecycleApp(t)
120 path := app.workspaceRegistry().Path()
121 if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
122 t.Fatal(err)
123 }
124 if err := os.WriteFile(path, []byte(`{"version":`), 0o600); err != nil {
125 t.Fatal(err)
126 }
127 var notifications atomic.Int32
128 app.projectTreeChangedHook = func() { notifications.Add(1) }
129 refreshHistoricalCatalogForTest(app)
130 c := &app.historicalImports
131 c.mu.Lock()
132 at := c.catalogAt
133 c.mu.Unlock()
134 if at.IsZero() || notifications.Load() != 0 {
135 t.Fatalf("failed discovery must settle without a refresh loop: attempt=%v notifications=%d", at, notifications.Load())
136 }
137 app.requestHistoricalCatalog()
138 c.workers.Wait()
139 c.mu.Lock()
140 retried := c.catalogAt != at
141 c.mu.Unlock()
142 if retried {
143 t.Fatal("failed discovery retried immediately")
144 }
145 if err := os.Remove(path); err != nil {
146 t.Fatal(err)
147 }
148 coldV4MigrationFixture(t, config.SessionStoreDir(), "recovered-history")
149 refreshHistoricalCatalogForTest(app)
150 if notifications.Load() != 1 || len(app.GetHistoricalImportStatus().Items) != 1 {
151 t.Fatal("settling a failed refresh prevented later discovery")
152 }
153 }
154
155 func TestHistoricalCatalogStartupOwnsDiscoveryAdmission(t *testing.T) {
156 isolateDesktopUserDirs(t)
157 app := newHistoricalLifecycleApp(t)
158 c := &app.historicalImports
159 c.discoveryMu.Lock()
160 app.startDesktopSessionMigration(t.Context())
161 c.mu.Lock()
162 pending := c.discoveryPending
163 c.mu.Unlock()
164 // Release the blocked worker before any assertion can run cleanup.
165 c.discoveryMu.Unlock()
166 c.workers.Wait()
167 if !pending {
168 t.Fatal("startup discovery admitted a second sidebar refresh")
169 }
170 c.mu.Lock()
171 defer c.mu.Unlock()
172 if c.discoveryPending {
173 t.Fatal("startup discovery never released refresh admission")
174 }
175 }
176
177 func TestHistoricalCatalogRefreshPublishesExternalPresentationChanges(t *testing.T) {
178 isolateDesktopUserDirs(t)
179 coldV4MigrationFixture(t, config.SessionStoreDir(), "renamed-history")
180 app := newHistoricalLifecycleApp(t)
181 id := historicalLifecycleID(t, app, "renamed-history")
182 var notifications, scans atomic.Int32
183 app.projectTreeChangedHook = func() { notifications.Add(1) }
184 app.projectTreeCatalogRefreshHook = func() { scans.Add(1) }
185 other := newHistoricalLifecycleApp(t)
186 if err := other.saveHistoricalSourcePresentation(id, func(p *historicalSourcePresentation) { p.Title = "Changed externally" }); err != nil {
187 t.Fatal(err)
188 }
189 refreshHistoricalCatalogForTest(app)
190 refreshHistoricalCatalogForTest(app)
191 if notifications.Load() != 1 || scans.Load() != 0 {
192 t.Fatalf("external presentation must publish once without index work: notifications=%d scans=%d", notifications.Load(), scans.Load())
193 }
194 if status := app.GetHistoricalImportStatus(); len(status.Items) != 1 || status.Items[0].Title != "Changed externally" {
195 t.Fatalf("external title not reflected: %+v", status)
196 }
197 }
198
199 func TestHistoricalCatalogRefreshPublishesExternalImport(t *testing.T) {
200 isolateDesktopUserDirs(t)
201 coldV4MigrationFixture(t, config.SessionStoreDir(), "externally-imported")
202 app := newHistoricalLifecycleApp(t)
203 id := historicalLifecycleID(t, app, "externally-imported")
204 var notifications, scans atomic.Int32
205 app.projectTreeChangedHook = func() { notifications.Add(1) }
206 app.projectTreeCatalogRefreshHook = func() { scans.Add(1) }
207 other := newHistoricalLifecycleApp(t)
208 if _, err := other.StartHistoricalImport([]string{id}); err != nil {
209 t.Fatal(err)
210 }
211 status := awaitHistoricalBatch(t, other)
212 if len(status.Items) != 1 || status.Items[0].Status != "imported" {
213 t.Fatalf("external import failed: %+v", status)
214 }
215 refreshHistoricalCatalogForTest(app)
216 refreshHistoricalCatalogForTest(app)
217 if notifications.Load() != 1 || scans.Load() != 0 {
218 t.Fatalf("external import must publish once without index work: notifications=%d scans=%d", notifications.Load(), scans.Load())
219 }
220 status = app.GetHistoricalImportStatus()
221 if len(status.Items) != 1 || status.Items[0].Status != "imported" {
222 t.Fatalf("external import not reflected: %+v", status)
223 }
224 }
225
225 lines GO