返回 DeepSeek-Reasonix
export_snapshot_test.go
根目录 / internal / session / export_snapshot_test.go
1 package session
2
3 import (
4 "context"
5 "encoding/json"
6 "errors"
7 "fmt"
8 "io"
9 "path/filepath"
10 "reasonix/internal/provider"
11 "sync/atomic"
12 "testing"
13 "time"
14 )
15
16 func TestFlushThroughDoesNotWaitForLaterWrites(t *testing.T) {
17 first, second := make(chan struct{}), make(chan struct{})
18 releaseFirst, releaseSecond := make(chan struct{}), make(chan struct{})
19 var writes atomic.Int32
20 s, err := OpenWithOptions(filepath.Join(t.TempDir(), "session"), "s", OpenOptions{Write: func(ctx context.Context, w io.Writer, data []byte) error {
21 switch writes.Add(1) {
22 case 1:
23 close(first)
24 <-releaseFirst
25 case 2:
26 close(second)
27 <-releaseSecond
28 }
29 return writeAllContext(ctx, w, data)
30 }})
31 if err != nil {
32 t.Fatal(err)
33 }
34 defer s.Close(context.Background())
35 defer close(releaseSecond)
36 if _, err = s.Append(t.Context(), Batch{OperationID: "one", Events: []Event{{Kind: "turn/start"}}}); err != nil {
37 t.Fatal(err)
38 }
39 done := make(chan error, 1)
40 go func() { _, err := s.FlushThrough(t.Context(), 1); done <- err }()
41 <-first
42 if _, err = s.Append(t.Context(), Batch{OperationID: "two", Events: []Event{{Kind: "turn/end", Payload: []byte(`{"status":"completed"}`)}}}); err != nil {
43 t.Fatal(err)
44 }
45 close(releaseFirst)
46 <-second
47 select {
48 case err := <-done:
49 if err != nil {
50 t.Fatal(err)
51 }
52 case <-time.After(3 * time.Second):
53 t.Fatal("snapshot waited for a later commit")
54 }
55 }
56
57 func TestExportSnapshotIncludesAllPagesAndExcludesLaterAppends(t *testing.T) {
58 service, err := NewService("local", NewFilesystemPersistence(filepath.Join(t.TempDir(), "sessions")))
59 if err != nil {
60 t.Fatal(err)
61 }
62 defer service.CloseAll(context.Background())
63 runtime, err := service.Create(t.Context(), CreateOptions{SessionID: "export"})
64 if err != nil {
65 t.Fatal(err)
66 }
67 ids := make([]string, 137)
68 for i := range ids {
69 ids[i] = fmt.Sprintf("m%d", i)
70 }
71 appendWindowMessages(t, runtime, ids...)
72 snapshot, err := service.Query().CaptureExportSnapshot(t.Context(), runtime.Ref())
73 if err != nil {
74 t.Fatal(err)
75 }
76 appendWindowMessages(t, runtime, "later")
77 var got []string
78 err = service.Query().VisitExportMessages(t.Context(), snapshot, func(message PersistentMessage) error { got = append(got, message.MessageID); return nil })
79 if err != nil {
80 t.Fatal(err)
81 }
82 if !idsEqual(ids, got) {
83 t.Fatalf("export got %d records, want %d", len(got), len(ids))
84 }
85 }
86
87 func TestToolObservationResolvesOutsideResidentWindow(t *testing.T) {
88 service, err := NewService("local", NewFilesystemPersistence(filepath.Join(t.TempDir(), "sessions")))
89 if err != nil {
90 t.Fatal(err)
91 }
92 defer service.CloseAll(context.Background())
93 runtime, err := service.Create(t.Context(), CreateOptions{SessionID: "tool-pages"})
94 if err != nil {
95 t.Fatal(err)
96 }
97 appendMessage := func(m provider.Message) {
98 t.Helper()
99 payload, _ := json.Marshal(map[string]any{"message": m})
100 if _, err := runtime.Session().Append(t.Context(), Batch{OperationID: m.ID, Events: []Event{{Kind: "message/complete", Payload: payload}}}); err != nil {
101 t.Fatal(err)
102 }
103 }
104 appendMessage(provider.Message{ID: "call", Role: provider.RoleAssistant, ToolCalls: []provider.ToolCall{{ID: "tool-id", Name: "bash", Arguments: "{}"}}})
105 ids := make([]string, 110)
106 for i := range ids {
107 ids[i] = fmt.Sprintf("gap-%d", i)
108 }
109 appendWindowMessages(t, runtime, ids...)
110 appendMessage(provider.Message{ID: "result", Role: provider.RoleTool, ToolCallID: "tool-id", Content: "OK", ToolRunState: provider.ToolRunCompleted})
111 if _, err = runtime.Session().Flush(t.Context()); err != nil {
112 t.Fatal(err)
113 }
114 // Prepare deterministically: this test exercises cross-page evidence, not the
115 // background locator scheduler or its UI polling deadline.
116 if _, _, err := service.Query().prepareHistoryIndex(t.Context(), runtime.Ref()); err != nil {
117 t.Fatal(err)
118 }
119 page := windowReady(t, service.Query(), runtime.Ref(), HistoryWindowRequest{Anchor: "message", MessageID: "call", Direction: "newer", Limit: 1})
120 observed := page.Messages[0].ToolObservations["tool-id"]
121 if observed.State != "completed" || observed.MessageID != "result" || observed.ContentRef == nil {
122 t.Fatalf("missing out-of-window evidence: %+v", observed)
123 }
124 }
125
126 func TestToolObservationUsesStartedEvidenceBeforeResult(t *testing.T) {
127 service, err := NewService("local", NewFilesystemPersistence(filepath.Join(t.TempDir(), "sessions")))
128 if err != nil {
129 t.Fatal(err)
130 }
131 defer service.CloseAll(context.Background())
132 runtime, err := service.Create(t.Context(), CreateOptions{SessionID: "running-tool"})
133 if err != nil {
134 t.Fatal(err)
135 }
136 data, _ := json.Marshal(map[string]any{"message": provider.Message{ID: "call", Role: provider.RoleAssistant, ToolCalls: []provider.ToolCall{{ID: "tool", Name: "bash", Arguments: "{}"}}}})
137 if _, err = runtime.Session().AppendBatch(t.Context(), "call", []Event{{Kind: "message/complete", Payload: data}, {Kind: "tool/call", Payload: json.RawMessage(`{"id":"tool","name":"bash"}`)}, {Kind: "tool/start", Payload: json.RawMessage(`{"id":"tool","name":"bash"}`)}}); err != nil {
138 t.Fatal(err)
139 }
140 snapshot, err := service.Query().CaptureExportSnapshot(t.Context(), runtime.Ref())
141 if err != nil {
142 t.Fatal(err)
143 }
144 err = service.Query().VisitExportMessages(t.Context(), snapshot, func(message PersistentMessage) error {
145 if observed := message.ToolObservations["tool"]; observed.State != "running" || observed.MessageID != "" {
146 t.Fatalf("lost running evidence: %+v", observed)
147 }
148 return nil
149 })
150 if err != nil {
151 t.Fatal(err)
152 }
153 }
154
155 func TestFlushThroughCancellationDoesNotCancelSharedWrite(t *testing.T) {
156 entered, release := make(chan struct{}), make(chan struct{})
157 s, err := OpenWithOptions(filepath.Join(t.TempDir(), "session"), "s", OpenOptions{Write: func(ctx context.Context, w io.Writer, data []byte) error {
158 close(entered)
159 <-release
160 return writeAllContext(ctx, w, data)
161 }})
162 if err != nil {
163 t.Fatal(err)
164 }
165 defer s.Close(context.Background())
166 if _, err = s.Append(t.Context(), Batch{OperationID: "one", Events: []Event{{Kind: "diagnostic", Payload: json.RawMessage(`{}`)}}}); err != nil {
167 t.Fatal(err)
168 }
169 ctx, cancel := context.WithCancel(t.Context())
170 done := make(chan error, 1)
171 go func() { _, err := s.FlushThrough(ctx, 1); done <- err }()
172 <-entered
173 cancel()
174 err = <-done
175 close(release)
176 if !errors.Is(err, context.Canceled) {
177 t.Fatalf("cancel returned %v", err)
178 }
179 receipt, err := s.FlushThrough(t.Context(), 1)
180 if err != nil || receipt.DurableSequence < 1 {
181 t.Fatalf("cancel affected shared writer: %+v %v", receipt, err)
182 }
183 }
184
185 func TestExportSnapshotUsesVersionedVisibleHistory(t *testing.T) {
186 service, err := NewService("local", NewFilesystemPersistence(filepath.Join(t.TempDir(), "sessions")))
187 if err != nil {
188 t.Fatal(err)
189 }
190 defer service.CloseAll(context.Background())
191 runtime, err := service.Create(t.Context(), CreateOptions{SessionID: "versions"})
192 if err != nil {
193 t.Fatal(err)
194 }
195 appendWindowMessages(t, runtime, "first", "removed")
196 payload, _ := json.Marshal(map[string]any{"message": provider.Message{ID: "first", Role: provider.RoleUser, Content: "UPDATED", Origin: provider.MessageOrigin("user")}})
197 if _, err = runtime.Session().AppendBatch(t.Context(), "update", []Event{{Kind: "message/upsert", Payload: payload}}); err != nil {
198 t.Fatal(err)
199 }
200 if _, err = runtime.Session().AppendBatch(t.Context(), "remove", []Event{{Kind: "message/retract", Payload: json.RawMessage(`{"messageIds":["removed"]}`)}}); err != nil {
201 t.Fatal(err)
202 }
203 snapshot, err := service.Query().CaptureExportSnapshot(t.Context(), runtime.Ref())
204 if err != nil {
205 t.Fatal(err)
206 }
207 count := 0
208 if err = service.Query().VisitExportMessages(t.Context(), snapshot, func(message PersistentMessage) error {
209 count++
210 var m provider.Message
211 if err := json.Unmarshal(message.Inline, &m); err != nil {
212 return err
213 }
214 if m.ID != "first" || m.Content != "UPDATED" {
215 t.Fatalf("export resurrected old content: %+v", m)
216 }
217 return nil
218 }); err != nil {
219 t.Fatal(err)
220 }
221 if count != 1 {
222 t.Fatalf("visible records=%d", count)
223 }
224 }
225
226 func TestExportSnapshotSurvivesLaterHistoryReplacement(t *testing.T) {
227 service, err := NewService("local", NewFilesystemPersistence(filepath.Join(t.TempDir(), "sessions")))
228 if err != nil {
229 t.Fatal(err)
230 }
231 defer service.CloseAll(context.Background())
232 runtime, err := service.Create(t.Context(), CreateOptions{SessionID: "replace-after-snapshot"})
233 if err != nil {
234 t.Fatal(err)
235 }
236 ids := []string{"original-user", "original-answer"}
237 appendWindowMessages(t, runtime, ids...)
238 snapshot, err := service.Query().CaptureExportSnapshot(t.Context(), runtime.Ref())
239 if err != nil {
240 t.Fatal(err)
241 }
242 if current := service.Query().storageGeneration(runtime.Ref().SessionID); snapshot.StorageGeneration != current {
243 t.Fatalf("snapshot generation = %q, want physical generation %q", snapshot.StorageGeneration, current)
244 }
245 replacement := []provider.Message{{ID: "replacement", Role: provider.RoleUser, Content: "new visible history", Origin: provider.MessageOrigin("user")}}
246 payload, _ := json.Marshal(map[string]any{"messages": replacement})
247 if _, err = runtime.Session().Append(t.Context(), Batch{OperationID: "replace-history", Events: []Event{{Kind: "history/replace", Payload: payload}}}); err != nil {
248 t.Fatal(err)
249 }
250 if _, err = runtime.Session().Flush(t.Context()); err != nil {
251 t.Fatal(err)
252 }
253 var got []string
254 if err = service.Query().VisitExportMessages(t.Context(), snapshot, func(message PersistentMessage) error {
255 got = append(got, message.MessageID)
256 return nil
257 }); err != nil {
258 t.Fatal(err)
259 }
260 if !idsEqual(ids, got) {
261 t.Fatalf("fixed snapshot after replacement = %v, want %v", got, ids)
262 }
263 }
264
265 func TestStreamExportCommitsStopsAtCapturedBoundary(t *testing.T) {
266 service, err := NewService("local", NewFilesystemPersistence(filepath.Join(t.TempDir(), "sessions")))
267 if err != nil {
268 t.Fatal(err)
269 }
270 defer service.CloseAll(context.Background())
271 runtime, err := service.Create(t.Context(), CreateOptions{SessionID: "diagnostic-prefix"})
272 if err != nil {
273 t.Fatal(err)
274 }
275 appendWindowMessages(t, runtime, "first")
276 snapshot, err := service.Query().CaptureExportSnapshot(t.Context(), runtime.Ref())
277 if err != nil {
278 t.Fatal(err)
279 }
280 appendWindowMessages(t, runtime, "later")
281 if _, err = runtime.Session().Flush(t.Context()); err != nil {
282 t.Fatal(err)
283 }
284 var last uint64
285 err = service.Query().StreamExportCommits(t.Context(), snapshot, func(commit Commit) error {
286 last = commit.LastSequence()
287 if last > snapshot.SnapshotSequence {
288 t.Fatal("export read beyond fixed boundary")
289 }
290 return nil
291 })
292 if err != nil {
293 t.Fatal(err)
294 }
295 if last != snapshot.SnapshotSequence {
296 t.Fatalf("got watermark %d, want %d", last, snapshot.SnapshotSequence)
297 }
298 }
299
299 lines GO