返回 DeepSeek-Reasonix
save_writer_tail_test.go
根目录 / internal / agent / save_writer_tail_test.go
1 package agent
2
3 import (
4 "errors"
5 "os"
6 "path/filepath"
7 "testing"
8 "time"
9
10 "reasonix/internal/provider"
11 "reasonix/internal/store"
12 )
13
14 func bindSessionWriter(t *testing.T, s *Session, path string) *SessionWriter {
15 t.Helper()
16 w, err := AcquireSessionWriter(path)
17 if err != nil {
18 t.Fatalf("AcquireSessionWriter: %v", err)
19 }
20 t.Cleanup(w.Release)
21 if err := w.Bind(s, NextSessionWriteGeneration()); err != nil {
22 t.Fatalf("Bind: %v", err)
23 }
24 return w
25 }
26
27 func snapshotDigest(t *testing.T, s *Session) [32]byte {
28 t.Helper()
29 digest, err := digestSessionMessages(s.Snapshot())
30 if err != nil {
31 t.Fatalf("digestSessionMessages: %v", err)
32 }
33 return digest
34 }
35
36 func TestWriterTailDecisionNoOpAfterSave(t *testing.T) {
37 path := schemaOneSessionPath(t, "session.jsonl")
38 s := NewSession("sys")
39 s.Add(provider.Message{Role: provider.RoleUser, Content: "hello"})
40 bindSessionWriter(t, s, path)
41 if err := s.SaveSnapshot(path); err != nil {
42 t.Fatalf("SaveSnapshot: %v", err)
43 }
44
45 decision, ok := s.writerTailDecision(path, s.Snapshot(), snapshotDigest(t, s), false)
46 if !ok {
47 t.Fatal("writer-tail CAS disarmed after own save")
48 }
49 if !decision.upToDate {
50 t.Fatalf("decision = %+v, want upToDate", decision)
51 }
52 }
53
54 func TestWriterTailDecisionAppendAfterSave(t *testing.T) {
55 path := schemaOneSessionPath(t, "session.jsonl")
56 s := NewSession("sys")
57 s.Add(provider.Message{Role: provider.RoleUser, Content: "hello"})
58 bindSessionWriter(t, s, path)
59 if err := s.SaveSnapshot(path); err != nil {
60 t.Fatalf("SaveSnapshot: %v", err)
61 }
62 before := len(s.Snapshot())
63 s.Add(provider.Message{Role: provider.RoleAssistant, Content: "hi"})
64
65 decision, ok := s.writerTailDecision(path, s.Snapshot(), snapshotDigest(t, s), false)
66 if !ok {
67 t.Fatal("writer-tail CAS disarmed for append")
68 }
69 if !decision.appendOnly || decision.appendFrom != before {
70 t.Fatalf("decision = %+v, want appendOnly from %d", decision, before)
71 }
72 }
73
74 func TestWriterTailRetryAfterPreWALReservation(t *testing.T) {
75 path := schemaOneSessionPath(t, "session.jsonl")
76 s := NewSession("sys")
77 s.Add(provider.Message{Role: provider.RoleUser, Content: "first"})
78 bindSessionWriter(t, s, path)
79 if err := s.SaveSnapshot(path); err != nil {
80 t.Fatalf("initial SaveSnapshot: %v", err)
81 }
82
83 s.Add(provider.Message{Role: provider.RoleAssistant, Content: "second"})
84 _, restore := crashAt(t, "wal-append", store.SessionEventLog(path))
85 crash := saveCrashing(func() { _ = s.SaveSnapshot(path) })
86 restore()
87 if crash == nil {
88 t.Fatal("save must crash after reserving the revision and before WAL append")
89 }
90
91 if err := s.SaveSnapshot(path); err != nil {
92 t.Fatalf("same-writer retry after reserved revision: %v", err)
93 }
94 loaded, err := LoadSession(path)
95 if err != nil {
96 t.Fatalf("LoadSession after retry: %v", err)
97 }
98 if got := len(loaded.Snapshot()); got != 3 {
99 t.Fatalf("messages after retry = %d, want 3", got)
100 }
101 }
102
103 func TestWriterTailDecisionDisarmedWithoutWriter(t *testing.T) {
104 path := filepath.Join(t.TempDir(), "session.jsonl")
105 s := NewSession("sys")
106 s.Add(provider.Message{Role: provider.RoleUser, Content: "hello"})
107 if err := s.SaveSnapshot(path); err != nil {
108 t.Fatalf("SaveSnapshot: %v", err)
109 }
110 if _, ok := s.writerTailDecision(path, s.Snapshot(), snapshotDigest(t, s), false); ok {
111 t.Fatal("unbound session must not use writer-tail CAS")
112 }
113 }
114
115 func TestWriterTailDecisionDisarmedWhenUnpaired(t *testing.T) {
116 path := filepath.Join(t.TempDir(), "session.jsonl")
117 s := NewSession("sys")
118 s.Add(provider.Message{Role: provider.RoleUser, Content: "hello"})
119 bindSessionWriter(t, s, path)
120 if err := s.SaveSnapshot(path); err != nil {
121 t.Fatalf("SaveSnapshot: %v", err)
122 }
123 st := s.persistState(path)
124 s.markPersisted(path, st.digest, st.version, st.revision, 0)
125
126 if _, ok := s.writerTailDecision(path, s.Snapshot(), snapshotDigest(t, s), false); ok {
127 t.Fatal("unpaired recovery baseline must not use writer-tail CAS")
128 }
129 }
130
131 func TestWriterTailDecisionDisarmedWhenLogGrows(t *testing.T) {
132 path := filepath.Join(t.TempDir(), "session.jsonl")
133 s := NewSession("sys")
134 s.Add(provider.Message{Role: provider.RoleUser, Content: "hello"})
135 bindSessionWriter(t, s, path)
136 if err := s.SaveSnapshot(path); err != nil {
137 t.Fatalf("SaveSnapshot: %v", err)
138 }
139
140 foreign := NewSession("sys")
141 foreign.Add(provider.Message{Role: provider.RoleUser, Content: "hello"})
142 foreign.Add(provider.Message{Role: provider.RoleAssistant, Content: "other"})
143 msgs := foreign.Snapshot()
144 digest, err := digestSessionMessages(msgs)
145 if err != nil {
146 t.Fatalf("digest: %v", err)
147 }
148 revision, _, err := sessionContentRevision(path)
149 if err != nil {
150 t.Fatalf("sessionContentRevision: %v", err)
151 }
152 if err := appendSessionReplaceEvent(path, msgs, digest, revision, "snapshot"); err != nil {
153 t.Fatalf("append foreign event: %v", err)
154 }
155
156 if _, ok := s.writerTailDecision(path, s.Snapshot(), snapshotDigest(t, s), false); ok {
157 t.Fatal("external event-log growth must disarm writer-tail CAS")
158 }
159 }
160
161 func TestWriterTailBindAdoptsLoadBaseline(t *testing.T) {
162 path := schemaOneSessionPath(t, "session.jsonl")
163 s := NewSession("sys")
164 s.Add(provider.Message{Role: provider.RoleUser, Content: "hello"})
165 if err := s.SaveSnapshot(path); err != nil {
166 t.Fatalf("SaveSnapshot: %v", err)
167 }
168 loaded, err := LoadSession(path)
169 if err != nil {
170 t.Fatalf("LoadSession: %v", err)
171 }
172 bindSessionWriter(t, loaded, path)
173
174 decision, ok := loaded.writerTailDecision(path, loaded.Snapshot(), snapshotDigest(t, loaded), false)
175 if !ok || !decision.upToDate {
176 t.Fatalf("load+bind writer-tail = ok=%v decision=%+v, want upToDate", ok, decision)
177 }
178 }
179
180 func TestWriterTailClassifyDoesNotRereadTranscriptBody(t *testing.T) {
181 path := schemaOneSessionPath(t, "session.jsonl")
182 s := NewSession("sys")
183 s.Add(provider.Message{Role: provider.RoleUser, Content: "hello"})
184 bindSessionWriter(t, s, path)
185 if err := s.SaveSnapshot(path); err != nil {
186 t.Fatalf("SaveSnapshot: %v", err)
187 }
188
189 logPath := store.SessionEventLog(path)
190 body, err := os.ReadFile(logPath)
191 if err != nil {
192 t.Fatalf("read event log: %v", err)
193 }
194 if err := os.WriteFile(logPath, make([]byte, len(body)), 0o644); err != nil {
195 t.Fatalf("overwrite event log: %v", err)
196 }
197 if err := os.Remove(path); err != nil {
198 t.Fatalf("remove jsonl: %v", err)
199 }
200
201 digest := snapshotDigest(t, s)
202 decision, err := s.classifySnapshotWrite(path, s.Snapshot(), digest, 0, false)
203 if err != nil {
204 t.Fatalf("classifySnapshotWrite: %v", err)
205 }
206 if !decision.upToDate {
207 t.Fatalf("decision = %+v, want upToDate without rereading wiped bodies", decision)
208 }
209 }
210
211 func TestWriterBoundSaveHonorsCompatibilityFileLock(t *testing.T) {
212 path := filepath.Join(t.TempDir(), "session.jsonl")
213 s := NewSession("sys")
214 s.Add(provider.Message{Role: provider.RoleUser, Content: "hello"})
215 bindSessionWriter(t, s, path)
216 held, err := lockSessionFile(path)
217 if err != nil {
218 t.Fatalf("lockSessionFile: %v", err)
219 }
220 defer held()
221 prevWait, prevPoll := sessionFileLockWait, sessionFileLockPollInterval
222 sessionFileLockWait = 40 * time.Millisecond
223 sessionFileLockPollInterval = 5 * time.Millisecond
224 t.Cleanup(func() {
225 sessionFileLockWait = prevWait
226 sessionFileLockPollInterval = prevPoll
227 })
228 if err := s.SaveSnapshot(path); !errors.Is(err, ErrSessionFileLockHeld) {
229 t.Fatalf("writer-bound save error = %v, want ErrSessionFileLockHeld", err)
230 }
231 }
232
232 lines GO