返回 DeepSeek-Reasonix
mutation_order_test.go
根目录 / internal / sessioncatalog / mutation_order_test.go
1 package sessioncatalog
2
3 import (
4 "context"
5 "os"
6 "path/filepath"
7 "sync"
8 "testing"
9
10 "reasonix/internal/agent"
11 )
12
13 func TestNewerRemovalGenerationSurvivesOlderRecreation(t *testing.T) {
14 ctx := context.Background()
15 catalog, err := Open(ctx, Options{InMemory: true, DisableRepair: true})
16 if err != nil {
17 t.Fatal(err)
18 }
19 t.Cleanup(func() { _ = catalog.Close(context.Background()) })
20 record := SessionRecord{
21 Path: "/sessions/race.jsonl", Directory: "/sessions", Scope: "global", TopicID: "topic",
22 Preview: "initial", TurnsState: TurnsValid, Health: HealthOK,
23 }
24 if err := catalog.UpsertSession(ctx, record); err != nil {
25 t.Fatal(err)
26 }
27 pathKey := catalog.pathKey(record.Path)
28 oldRemoval := catalog.mutationSeq.Add(1)
29 catalog.removedPaths.Store(pathKey, oldRemoval)
30 record.Preview = "stale recreation"
31 record.enqueueSequence = catalog.mutationSeq.Add(1)
32
33 loaded := make(chan struct{})
34 resume := make(chan struct{})
35 var once sync.Once
36 catalog.testPathMutationLoadedHook = func(key string) {
37 if key == pathKey {
38 once.Do(func() {
39 close(loaded)
40 <-resume
41 })
42 }
43 }
44 done := make(chan error, 1)
45 go func() { done <- catalog.upsertSessions(ctx, []SessionRecord{record}, nil, "test") }()
46 <-loaded
47 newRemoval := catalog.mutationSeq.Add(1)
48 catalog.removedPaths.Store(pathKey, newRemoval)
49 close(resume)
50 if err := <-done; err != nil {
51 t.Fatal(err)
52 }
53 catalog.testPathMutationLoadedHook = nil
54
55 removedAt, ok := catalog.removedPaths.Load(pathKey)
56 if !ok || removedAt != newRemoval {
57 t.Fatalf("removal generation = %v, %v, want %d", removedAt, ok, newRemoval)
58 }
59 var preview string
60 if err := catalog.db.QueryRowContext(ctx, `SELECT preview FROM catalog_sessions WHERE path_key=?`, pathKey).Scan(&preview); err != nil {
61 t.Fatal(err)
62 }
63 if preview != "initial" {
64 t.Fatalf("older recreation committed preview %q", preview)
65 }
66 }
67
68 func TestStaleExactIndexAndReconcileKeepRemovalTombstone(t *testing.T) {
69 ctx := context.Background()
70 dir := t.TempDir()
71 path := filepath.Join(dir, "session.jsonl")
72 if err := os.WriteFile(path, []byte("{}\n"), 0o600); err != nil {
73 t.Fatal(err)
74 }
75 if err := agent.SaveBranchMeta(path, agent.BranchMeta{
76 Scope: "global", TopicID: "topic", Preview: "initial",
77 SchemaVersion: agent.BranchMetaCountsVersion, Turns: 1,
78 }); err != nil {
79 t.Fatal(err)
80 }
81 catalog, err := Open(ctx, Options{Path: filepath.Join(t.TempDir(), "catalog.sqlite"), DisableRepair: true})
82 if err != nil {
83 t.Fatal(err)
84 }
85 t.Cleanup(func() { _ = catalog.Close(context.Background()) })
86 target := DirectoryTarget{Path: dir, Scope: "global"}
87 if err := catalog.ReconcileDirectory(ctx, target); err != nil {
88 t.Fatal(err)
89 }
90 if err := agent.UpdateBranchMeta(path, false, func(meta *agent.BranchMeta) error {
91 meta.Preview = "stale projection should never commit"
92 return nil
93 }); err != nil {
94 t.Fatal(err)
95 }
96 staleExactSequence := catalog.mutationSeq.Add(1)
97 staleScanSequence := catalog.mutationSeq.Add(1)
98 pathKey := catalog.pathKey(path)
99 removalSequence := catalog.mutationSeq.Add(1)
100 catalog.removedPaths.Store(pathKey, removalSequence)
101
102 if err := catalog.indexSessionPath(ctx, target, path, staleExactSequence); err != nil {
103 t.Fatal(err)
104 }
105 if err := catalog.reconcileDirectory(ctx, target, staleScanSequence); err != nil {
106 t.Fatal(err)
107 }
108 if removedAt, ok := catalog.removedPaths.Load(pathKey); !ok || removedAt != removalSequence {
109 t.Fatalf("stale writers cleared removal generation: %v, %v", removedAt, ok)
110 }
111 if _, ok, err := catalog.GetSession(ctx, path); err != nil || ok {
112 t.Fatalf("tombstoned session became visible: ok=%v err=%v", ok, err)
113 }
114
115 if err := agent.UpdateBranchMeta(path, false, func(meta *agent.BranchMeta) error {
116 meta.Preview = "authoritative recreation"
117 meta.Turns = 2
118 return nil
119 }); err != nil {
120 t.Fatal(err)
121 }
122 if err := catalog.indexSessionPath(ctx, target, path, catalog.mutationSeq.Add(1)); err != nil {
123 t.Fatal(err)
124 }
125 if _, ok := catalog.removedPaths.Load(pathKey); ok {
126 t.Fatal("newer authoritative recreation did not clear the older tombstone")
127 }
128 got, ok, err := catalog.GetSession(ctx, path)
129 if err != nil || !ok || got.Preview != "authoritative recreation" || got.Turns != 2 {
130 t.Fatalf("recreated session = %#v, %v, %v", got, ok, err)
131 }
132 }
133
134 func TestExactIndexQueueRetainsNewestGeneration(t *testing.T) {
135 catalog := &Catalog{
136 pathCh: make(chan sessionPathRequest, 1),
137 directoryLocks: map[string]*sync.Mutex{},
138 stop: make(chan struct{}),
139 }
140 path := filepath.Join(string(filepath.Separator), "sessions", "session.jsonl")
141 first := DirectoryTarget{Path: filepath.Dir(path), Scope: "global"}
142 second := DirectoryTarget{Path: filepath.Dir(path), Scope: "project", WorkspaceRoot: "/workspace"}
143 if !catalog.RequestIndexSession(first, path) || !catalog.RequestIndexSession(second, path) {
144 t.Fatal("coalesced exact-index request was rejected")
145 }
146 queued, ok := catalog.pathQueued.Load(queuePathKey(path))
147 if !ok {
148 t.Fatal("latest exact-index request was not retained")
149 }
150 request := queued.(sessionPathRequest)
151 if request.target.Scope != "project" || request.target.WorkspaceRoot != "/workspace" || request.sequence != 2 {
152 t.Fatalf("coalesced request = %#v, want newest generation", request)
153 }
154 if len(catalog.pathCh) != 1 {
155 t.Fatalf("coalesced queue signals = %d, want one", len(catalog.pathCh))
156 }
157 }
158
158 lines GO