返回 DeepSeek-Reasonix
lineage_test.go
根目录 / internal / sessioncatalog / lineage_test.go
1 package sessioncatalog
2
3 import (
4 "context"
5 "fmt"
6 "os"
7 "path/filepath"
8 "strings"
9 "testing"
10
11 "reasonix/internal/agent"
12 "reasonix/internal/provider"
13 )
14
15 func saveLineageSession(t *testing.T, path string, messages ...string) {
16 t.Helper()
17 s := agent.NewSession("sys")
18 for i, message := range messages {
19 role := provider.RoleUser
20 if i%2 == 1 {
21 role = provider.RoleAssistant
22 }
23 s.Add(provider.Message{Role: role, Content: message})
24 }
25 if err := s.Save(path); err != nil {
26 t.Fatal(err)
27 }
28 }
29
30 func TestClassifyRecoveryLineageUsesParentDirectory(t *testing.T) {
31 dir := t.TempDir()
32 root := filepath.Join(dir, "root.jsonl")
33 saveLineageSession(t, root, "q", "a", "continued", "done")
34 branchSession := agent.NewSession("sys")
35 branchSession.Add(provider.Message{Role: provider.RoleUser, Content: "q"})
36 branchSession.Add(provider.Message{Role: provider.RoleAssistant, Content: "a"})
37 info, err := branchSession.SaveConflictRecoveryBranch(agent.RecoveryBranchOptions{OriginalPath: root})
38 if err != nil {
39 t.Fatal(err)
40 }
41 covered := classifyRecoveryLineage(SessionRecord{
42 Path: info.Path, Recovered: true, ParentID: agent.BranchID(root),
43 })
44 if covered.RecoveryRole != RecoveryRoleCoveredCopy || !covered.RecoveryCopy {
45 t.Fatalf("covered = %+v", covered)
46 }
47 normal := classifyRecoveryLineage(SessionRecord{Path: root})
48 if normal.RecoveryRole != RecoveryRoleNormal {
49 t.Fatalf("normal role = %q", normal.RecoveryRole)
50 }
51 }
52
53 func TestPromoteCanonicalLeavesRequiresContentCoverage(t *testing.T) {
54 dir := t.TempDir()
55 root := filepath.Join(dir, "root.jsonl")
56 leaf := filepath.Join(dir, "leaf.jsonl")
57 saveLineageSession(t, root, "q", "a")
58 saveLineageSession(t, leaf, "q", "a", "next", "answer")
59 recs := []SessionRecord{
60 {Path: root, RecoveryRole: RecoveryRoleNormal, Turns: 1, TurnsState: TurnsValid},
61 {Path: leaf, Recovered: true, ParentID: "root", RecoveryGroupID: "root", RecoveryRole: RecoveryRoleDiverged, Turns: 2, TurnsState: TurnsValid},
62 }
63 out := promoteCanonicalLeaves(recs)
64 if !out[1].RecoveryCanonical || out[1].RecoveryRole != RecoveryRoleAdopted {
65 t.Fatalf("unique covering leaf = %+v", out[1])
66 }
67
68 peer := filepath.Join(dir, "peer.jsonl")
69 saveLineageSession(t, peer, "q", "a", "other", "branch")
70 recs = append(recs, SessionRecord{
71 Path: peer, Recovered: true, ParentID: "root", RecoveryGroupID: "root", RecoveryRole: RecoveryRoleDiverged, Turns: 2, TurnsState: TurnsValid,
72 })
73 out = promoteCanonicalLeaves(recs)
74 canonical := 0
75 for _, record := range out[1:] {
76 if record.RecoveryRole != RecoveryRoleDiverged {
77 t.Fatalf("ambiguous equal-length leaves must keep diverged role: %+v", out)
78 }
79 if record.RecoveryCanonical {
80 canonical++
81 }
82 }
83 // Ordinary list still needs exactly one stable representative even when
84 // content truly forks; History remains the place to open the other leaf.
85 if canonical != 1 {
86 t.Fatalf("ambiguous group canonical count = %d, want 1 stable representative: %+v", canonical, out)
87 }
88 }
89
90 func TestPromoteCanonicalLeavesIgnoresUnknownTurnMetadataAndCoversAncestors(t *testing.T) {
91 dir := t.TempDir()
92 root := filepath.Join(dir, "root.jsonl")
93 ancestor := filepath.Join(dir, "ancestor.jsonl")
94 leaf := filepath.Join(dir, "leaf.jsonl")
95 saveLineageSession(t, root, "q", "a")
96 saveLineageSession(t, ancestor, "q", "a", "next", "one")
97 saveLineageSession(t, leaf, "q", "a", "next", "one", "again", "two")
98 recs := []SessionRecord{
99 {Path: root, RecoveryRole: RecoveryRoleNormal, TurnsState: TurnsUnknown},
100 {Path: ancestor, Recovered: true, ParentID: "root", RecoveryGroupID: "root", RecoveryRole: RecoveryRoleDiverged, TurnsState: TurnsUnknown},
101 {Path: leaf, Recovered: true, ParentID: "ancestor", RecoveryGroupID: "root", RecoveryRole: RecoveryRoleDiverged, TurnsState: TurnsUnknown},
102 }
103 out := promoteCanonicalLeaves(recs)
104 if !out[2].RecoveryCanonical || out[2].RecoveryRole != RecoveryRoleAdopted {
105 t.Fatalf("leaf = %+v, want adopted despite unknown turns", out[2])
106 }
107 if !out[1].RecoveryCopy || out[1].RecoveryRole != RecoveryRoleCoveredCopy {
108 t.Fatalf("ancestor = %+v, want covered copy", out[1])
109 }
110 }
111
112 func TestCanonicalSessionPathForTopic(t *testing.T) {
113 sessions := []SessionRecord{
114 {Path: "/s/old.jsonl", RecoveryRole: RecoveryRoleNormal},
115 {Path: "/s/leaf.jsonl", RecoveryRole: RecoveryRoleAdopted, RecoveryCanonical: true},
116 }
117 if got := CanonicalSessionPathForTopic(sessions, "/s/old.jsonl"); got != "/s/leaf.jsonl" {
118 t.Fatalf("retarget = %q", got)
119 }
120 if got := CanonicalSessionPathForTopic(sessions, "/s/leaf.jsonl"); got != "" {
121 t.Fatalf("already canonical should not retarget: %q", got)
122 }
123 }
124
125 func TestOrdinaryContinuePathFollowsParentOnly(t *testing.T) {
126 parent := "/s/old.jsonl"
127 leaf := "/s/leaf.jsonl"
128 fork := "/s/fork.jsonl"
129 sessions := []SessionRecord{
130 {Path: parent, RecoveryRole: RecoveryRoleNormal},
131 {Path: leaf, Recovered: true, RecoveryRole: RecoveryRoleAdopted, RecoveryCanonical: true},
132 {Path: fork, Recovered: true, RecoveryRole: RecoveryRoleDiverged},
133 }
134 if got := OrdinaryContinuePath(sessions, parent); got != leaf {
135 t.Fatalf("parent continue = %q, want leaf", got)
136 }
137 if got := OrdinaryContinuePath(sessions, leaf); got != "" {
138 t.Fatalf("leaf continue = %q, want keep", got)
139 }
140 if got := OrdinaryContinuePath(sessions, fork); got != "" {
141 t.Fatalf("fork continue = %q, want keep inspection path", got)
142 }
143 if got := OrdinaryContinuePath(sessions, "/s/stale-parent.jsonl"); got != leaf {
144 t.Fatalf("stale parent continue = %q, want leaf", got)
145 }
146 }
147
148 func TestPromoteCanonicalLeavesHonorsPreferredOriginalMember(t *testing.T) {
149 root := "/s/root.jsonl"
150 branch := "/s/branch.jsonl"
151 records := []SessionRecord{
152 {Path: root, RecoveryRole: RecoveryRoleNormal, RecoveryPreferred: true, TurnsState: TurnsValid},
153 {Path: branch, Recovered: true, ParentID: "root", RecoveryGroupID: "root", RecoveryRole: RecoveryRoleDiverged, TurnsState: TurnsValid},
154 }
155 got := promoteCanonicalLeaves(records)
156 if got[0].RecoveryRole != RecoveryRolePreferred || !got[0].RecoveryCanonical {
157 t.Fatalf("preferred original = %+v, want preferred canonical", got[0])
158 }
159 if got[1].RecoveryCanonical {
160 t.Fatalf("recovery leaf stayed canonical after choosing original: %+v", got[1])
161 }
162 }
163
164 func TestOrdinaryContinuePathFollowsUniqueLinearCompactedLeaf(t *testing.T) {
165 root := filepath.Join("/s", "root.jsonl")
166 parentID := "root"
167 sessions := []SessionRecord{{
168 Path: root, RecoveryRole: RecoveryRoleNormal,
169 Turns: 9, TurnsState: TurnsValid, LastActivityAt: 1,
170 }}
171 for i := 1; i <= 130; i++ {
172 id := fmt.Sprintf("recovery-%03d", i)
173 sessions = append(sessions, SessionRecord{
174 Path: filepath.Join("/s", id+".jsonl"), Recovered: true,
175 ParentID: parentID, RecoveryGroupID: "root", RecoveryRole: RecoveryRoleDiverged,
176 Turns: 9 + (i*63)/130, TurnsState: TurnsValid, LastActivityAt: int64(i + 1),
177 })
178 parentID = id
179 }
180 leaf := sessions[len(sessions)-1].Path
181 if got := OrdinaryContinuePath(sessions, root); got != leaf {
182 t.Fatalf("parent continue = %q, want unique 72-turn leaf %q", got, leaf)
183 }
184 if got := topicRepresentativePath(sessions); got != leaf {
185 t.Fatalf("representative = %q, want unique 72-turn leaf %q", got, leaf)
186 }
187 if got := OrdinaryContinuePath(sessions, leaf); got != "" {
188 t.Fatalf("leaf continue = %q, want keep current leaf", got)
189 }
190 }
191
192 func TestOrdinaryContinuePathRejectsForkedOrRegressingLineage(t *testing.T) {
193 root := SessionRecord{Path: "/s/root.jsonl", RecoveryRole: RecoveryRoleNormal, Turns: 9, TurnsState: TurnsValid, LastActivityAt: 1}
194 linear := SessionRecord{
195 Path: "/s/linear.jsonl", Recovered: true, ParentID: "root", RecoveryGroupID: "root",
196 RecoveryRole: RecoveryRoleDiverged, Turns: 10, TurnsState: TurnsValid, LastActivityAt: 2,
197 }
198 fork := SessionRecord{
199 Path: "/s/fork.jsonl", Recovered: true, ParentID: "root", RecoveryGroupID: "root",
200 RecoveryRole: RecoveryRoleDiverged, Turns: 11, TurnsState: TurnsValid, LastActivityAt: 3,
201 }
202 if got := OrdinaryContinuePath([]SessionRecord{root, linear, fork}, root.Path); got != "" {
203 t.Fatalf("forked continue = %q, want unresolved", got)
204 }
205 regressed := linear
206 regressed.Turns = 8
207 if got := OrdinaryContinuePath([]SessionRecord{root, regressed}, root.Path); got != "" {
208 t.Fatalf("regressing continue = %q, want unresolved", got)
209 }
210 }
211
212 func TestExplicitPreferredRecoveryWinsWithoutMakingPeersCovered(t *testing.T) {
213 dir := t.TempDir()
214 root := filepath.Join(dir, "root.jsonl")
215 left := filepath.Join(dir, "left.jsonl")
216 right := filepath.Join(dir, "right.jsonl")
217 saveLineageSession(t, root, "q", "a")
218 saveLineageSession(t, left, "q", "a", "left", "answer")
219 saveLineageSession(t, right, "q", "a", "right", "answer")
220 records := []SessionRecord{
221 {Path: root, RecoveryRole: RecoveryRoleNormal},
222 {Path: left, Recovered: true, ParentID: "root", RecoveryGroupID: "root", RecoveryRole: RecoveryRoleDiverged, RecoveryPreferred: true},
223 {Path: right, Recovered: true, ParentID: "root", RecoveryGroupID: "root", RecoveryRole: RecoveryRoleDiverged},
224 }
225 out := promoteCanonicalLeaves(records)
226 if !out[1].RecoveryCanonical || out[1].RecoveryRole != RecoveryRolePreferred {
227 t.Fatalf("preferred = %+v", out[1])
228 }
229 if out[2].RecoveryCopy || out[2].RecoveryRole != RecoveryRoleDiverged {
230 t.Fatalf("diverged peer must retain unique content: %+v", out[2])
231 }
232 if got := CanonicalSessionPathForTopic(out, root); got != left {
233 t.Fatalf("canonical path = %q, want %q", got, left)
234 }
235 }
236
237 func TestReconcilePersistsContentProvenCanonicalLeaf(t *testing.T) {
238 ctx := context.Background()
239 dir := t.TempDir()
240 root := filepath.Join(dir, "root.jsonl")
241 saveLineageSession(t, root, "q", "a")
242 continued := agent.NewSession("sys")
243 continued.Add(provider.Message{Role: provider.RoleUser, Content: "q"})
244 continued.Add(provider.Message{Role: provider.RoleAssistant, Content: "a"})
245 continued.Add(provider.Message{Role: provider.RoleUser, Content: "next"})
246 continued.Add(provider.Message{Role: provider.RoleAssistant, Content: "answer"})
247 info, err := continued.SaveConflictRecoveryBranch(agent.RecoveryBranchOptions{OriginalPath: root})
248 if err != nil {
249 t.Fatal(err)
250 }
251 catalog, err := Open(ctx, Options{InMemory: true, DisableRepair: true})
252 if err != nil {
253 t.Fatal(err)
254 }
255 defer catalog.Close(ctx)
256 if err := catalog.ReconcileDirectory(ctx, DirectoryTarget{Path: dir, Scope: "global"}); err != nil {
257 t.Fatal(err)
258 }
259 record, ok, err := catalog.GetSession(ctx, info.Path)
260 if err != nil || !ok {
261 t.Fatalf("GetSession ok=%v err=%v", ok, err)
262 }
263 if record.RecoveryRole != RecoveryRoleAdopted || !record.RecoveryCanonical {
264 t.Fatalf("reconciled recovery = %+v, want adopted canonical", record)
265 }
266 }
267
268 func TestReconcileOpensUniqueLinearCompactedLeafWithoutAuthorizingCleanup(t *testing.T) {
269 ctx := context.Background()
270 dir := t.TempDir()
271 root := filepath.Join(dir, "root.jsonl")
272 leaf := filepath.Join(dir, "leaf.jsonl")
273 rootMessages := make([]string, 0, 18)
274 for i := range 9 {
275 rootMessages = append(rootMessages, fmt.Sprintf("root question %d", i), fmt.Sprintf("root answer %d", i))
276 }
277 leafMessages := make([]string, 0, 144)
278 for i := range 72 {
279 leafMessages = append(leafMessages, fmt.Sprintf("compacted question %d", i), fmt.Sprintf("compacted answer %d", i))
280 }
281 saveLineageSession(t, root, rootMessages...)
282 saveLineageSession(t, leaf, leafMessages...)
283 for path, meta := range map[string]agent.BranchMeta{
284 root: {ID: "root", Scope: "global", TopicID: "conversation", TopicTitle: "Compacted"},
285 leaf: {ID: "leaf", Scope: "global", TopicID: "conversation", TopicTitle: "Compacted", Recovered: true, ParentID: "root", RecoveryDepth: 1},
286 } {
287 if err := agent.SaveBranchMetaPreserveUpdated(path, meta); err != nil {
288 t.Fatal(err)
289 }
290 }
291 catalog, err := Open(ctx, Options{InMemory: true, DisableRepair: true})
292 if err != nil {
293 t.Fatal(err)
294 }
295 defer catalog.Close(ctx)
296 if err := catalog.ReconcileDirectory(ctx, DirectoryTarget{Path: dir, Scope: "global"}); err != nil {
297 t.Fatal(err)
298 }
299 topic, ok, err := catalog.GetTopic(ctx, TopicKey{Scope: "global", TopicID: "conversation"})
300 if err != nil || !ok {
301 t.Fatalf("GetTopic ok=%v err=%v", ok, err)
302 }
303 if topic.RepresentativePath != leaf {
304 t.Fatalf("representative = %q, want compacted leaf %q", topic.RepresentativePath, leaf)
305 }
306 if topic.RecoveryCleanupEligibleCount != 0 {
307 t.Fatalf("cleanup eligible = %d, want 0 without content coverage", topic.RecoveryCleanupEligibleCount)
308 }
309 if got := CanonicalSessionPathForTopic(topic.Sessions, root); got != "" {
310 t.Fatalf("cleanup canonical = %q, want unresolved", got)
311 }
312 if got := OrdinaryContinuePath(topic.Sessions, root); got != leaf {
313 t.Fatalf("ordinary continue = %q, want compacted leaf %q", got, leaf)
314 }
315 }
316
317 func TestReconcileReanchorsCrossTopicRecoveryIntoOneLogicalTopic(t *testing.T) {
318 ctx := context.Background()
319 dir := t.TempDir()
320 root := filepath.Join(dir, "root.jsonl")
321 copyPath := filepath.Join(dir, "copy.jsonl")
322 leaf := filepath.Join(dir, "leaf.jsonl")
323 saveLineageSession(t, root, "q", "a")
324 saveLineageSession(t, copyPath, "q", "a")
325 saveLineageSession(t, leaf, "q", "a", "next", "done")
326 for path, meta := range map[string]agent.BranchMeta{
327 root: {ID: "root", Scope: "global", TopicID: "conversation", TopicTitle: "Upgraded"},
328 copyPath: {ID: "copy", Scope: "global", TopicID: "legacy-copy-topic", Recovered: true, ParentID: "root", RecoveryDepth: 1},
329 leaf: {ID: "leaf", Scope: "global", TopicID: "legacy-leaf-topic", Recovered: true, ParentID: "copy", RecoveryDepth: 2},
330 } {
331 if err := agent.SaveBranchMetaPreserveUpdated(path, meta); err != nil {
332 t.Fatal(err)
333 }
334 }
335 catalog, err := Open(ctx, Options{InMemory: true, DisableRepair: true})
336 if err != nil {
337 t.Fatal(err)
338 }
339 defer catalog.Close(ctx)
340 if err := catalog.ReconcileDirectory(ctx, DirectoryTarget{Path: dir, Scope: "global"}); err != nil {
341 t.Fatal(err)
342 }
343 page, err := catalog.ListTopics(ctx, TopicPageRequest{Scope: "global", Limit: 50})
344 if err != nil {
345 t.Fatal(err)
346 }
347 if len(page.Items) != 1 || page.Items[0].TopicID != "conversation" {
348 t.Fatalf("ListTopics = %+v, want one logical topic conversation", page.Items)
349 }
350 leafRec, ok, err := catalog.GetSession(ctx, leaf)
351 if err != nil || !ok {
352 t.Fatalf("GetSession leaf ok=%v err=%v", ok, err)
353 }
354 if leafRec.TopicID != "conversation" || leafRec.LogicalTopicID != "conversation" {
355 t.Fatalf("leaf projection = %+v, want re-anchored to conversation", leafRec)
356 }
357 if leafRec.OrdinaryVisible {
358 t.Fatalf("leaf must not be ordinary-visible while root exists: %+v", leafRec)
359 }
360 rootRec, ok, err := catalog.GetSession(ctx, root)
361 if err != nil || !ok || !rootRec.OrdinaryVisible {
362 t.Fatalf("root ordinary visibility = %+v ok=%v err=%v", rootRec, ok, err)
363 }
364 }
365
366 func TestRecoveryFilenameParentID(t *testing.T) {
367 parent, ok := agent.RecoveryFilenameParentID("/s/chat-recovery-0123456789abcdef.jsonl")
368 if !ok || parent != "chat" {
369 t.Fatalf("parent = %q ok=%v", parent, ok)
370 }
371 if !agent.LooksLikeRecoveryFilename("/s/chat-recovery-0123456789abcdef.jsonl") {
372 t.Fatal("expected recovery filename")
373 }
374 if agent.LooksLikeRecoveryFilename("/s/chat.jsonl") {
375 t.Fatal("normal session must not look like recovery")
376 }
377 }
378
379 func TestUpgradeMatrixV4RebuildKeepsSingleLogicalRowAndAuthority(t *testing.T) {
380 // Simulates 1.24.2-style multi-topic recovery storm → new v6 projection →
381 // discard cache → reindex. Ordinary list stays one row; JSONL/meta bytes
382 // are never rewritten (the 1.23.0→new-version reinstall path).
383 ctx := context.Background()
384 dir := t.TempDir()
385 cacheDir := t.TempDir()
386 root := filepath.Join(dir, "root.jsonl")
387 copyPath := filepath.Join(dir, "copy.jsonl")
388 leaf := filepath.Join(dir, "leaf.jsonl")
389 saveLineageSession(t, root, "q", "a")
390 saveLineageSession(t, copyPath, "q", "a")
391 saveLineageSession(t, leaf, "q", "a", "next", "done")
392 for path, meta := range map[string]agent.BranchMeta{
393 root: {ID: "root", Scope: "global", TopicID: "conversation", TopicTitle: "Upgraded"},
394 copyPath: {ID: "copy", Scope: "global", TopicID: "legacy-copy-topic", Recovered: true, ParentID: "root", RecoveryDepth: 1},
395 leaf: {ID: "leaf", Scope: "global", TopicID: "legacy-leaf-topic", Recovered: true, ParentID: "copy", RecoveryDepth: 2},
396 } {
397 if err := agent.SaveBranchMetaPreserveUpdated(path, meta); err != nil {
398 t.Fatal(err)
399 }
400 }
401 hash := func(path string) string {
402 t.Helper()
403 data, err := os.ReadFile(path)
404 if err != nil {
405 t.Fatal(err)
406 }
407 return string(data)
408 }
409 before := map[string]string{}
410 for _, path := range []string{root, copyPath, leaf, agent.BranchMetaPath(root), agent.BranchMetaPath(copyPath), agent.BranchMetaPath(leaf)} {
411 before[path] = hash(path)
412 }
413
414 openAndList := func(label string) (topicID, rep string) {
415 t.Helper()
416 catalog, err := Open(ctx, Options{Path: filepath.Join(cacheDir, label+".sqlite"), DisableRepair: true})
417 if err != nil {
418 t.Fatal(err)
419 }
420 defer catalog.Close(ctx)
421 if err := catalog.ReconcileDirectory(ctx, DirectoryTarget{Path: dir, Scope: "global"}); err != nil {
422 t.Fatal(err)
423 }
424 page, err := catalog.ListTopics(ctx, TopicPageRequest{Scope: "global", Limit: 50})
425 if err != nil {
426 t.Fatal(err)
427 }
428 if len(page.Items) != 1 {
429 t.Fatalf("%s ListTopics = %+v, want one logical row", label, page.Items)
430 }
431 return page.Items[0].TopicID, page.Items[0].RepresentativePath
432 }
433
434 topic1, rep1 := openAndList("pass1")
435 topic2, rep2 := openAndList("pass2")
436 if topic1 != "conversation" || topic2 != "conversation" {
437 t.Fatalf("logical topics = %q/%q, want conversation both rebuilds", topic1, topic2)
438 }
439 if rep1 == "" || rep1 != rep2 {
440 t.Fatalf("representative unstable across rebuilds: %q vs %q", rep1, rep2)
441 }
442 for path, want := range before {
443 if got := hash(path); got != want {
444 t.Fatalf("authority file mutated during catalog rebuild: %s", path)
445 }
446 }
447 // v7 isolates the persistent v11 repair scheduler from older writers.
448 if !strings.HasSuffix(filepath.ToSlash(DefaultPath()), "session-catalog/v9.sqlite") && DefaultPath() != "" {
449 t.Fatalf("DefaultPath = %q, want v9.sqlite", DefaultPath())
450 }
451 }
452
453 func TestReconcileFoldsFilenameRecoveryIntoRootTopic(t *testing.T) {
454 ctx := context.Background()
455 dir := t.TempDir()
456 root := filepath.Join(dir, "normal.jsonl")
457 recovery := filepath.Join(dir, "normal-recovery-0123456789abcdef.jsonl")
458 saveLineageSession(t, root, "normal imported prompt")
459 saveLineageSession(t, recovery, "legacy recovery prompt")
460 if err := agent.SaveBranchMetaPreserveUpdated(root, agent.BranchMeta{
461 ID: "normal", Scope: "global", TopicID: "legacy_normal", TopicTitle: "normal",
462 }); err != nil {
463 t.Fatal(err)
464 }
465 if err := agent.SaveBranchMetaPreserveUpdated(recovery, agent.BranchMeta{
466 ID: "normal-recovery-0123456789abcdef", Scope: "global",
467 TopicID: "legacy_recovery", TopicTitle: "recovery",
468 }); err != nil {
469 t.Fatal(err)
470 }
471 catalog, err := Open(ctx, Options{InMemory: true, DisableRepair: true})
472 if err != nil {
473 t.Fatal(err)
474 }
475 defer catalog.Close(ctx)
476 if err := catalog.ReconcileDirectory(ctx, DirectoryTarget{Path: dir, Scope: "global"}); err != nil {
477 t.Fatal(err)
478 }
479 page, err := catalog.ListTopics(ctx, TopicPageRequest{Scope: "global", Limit: 50})
480 if err != nil {
481 t.Fatal(err)
482 }
483 if len(page.Items) != 1 || page.Items[0].TopicID != "legacy_normal" {
484 t.Fatalf("ListTopics = %+v, want one folded legacy_normal topic", page.Items)
485 }
486 rec, ok, err := catalog.GetSession(ctx, recovery)
487 if err != nil || !ok {
488 t.Fatalf("GetSession recovery ok=%v err=%v", ok, err)
489 }
490 if !rec.Recovered || rec.TopicID != "legacy_normal" {
491 t.Fatalf("recovery projection = %+v, want re-anchored recovered row", rec)
492 }
493 }
494
494 lines GO