返回 DeepSeek-Reasonix
review_regression_test.go
根目录 / internal / session / review_regression_test.go
1 package session
2
3 import (
4 "bytes"
5 "context"
6 "encoding/json"
7 "errors"
8 "io"
9 "os"
10 "path/filepath"
11 "runtime"
12 "testing"
13 "time"
14
15 "reasonix/internal/filelock"
16 "reasonix/internal/provider"
17 )
18
19 // owner grants host authority for an exact instance. Tests use it wherever a
20 // host would retire a runtime it published itself.
21 func owner(t *testing.T, service *Service, runtime *Runtime) *RuntimeOwner {
22 t.Helper()
23 grant, err := service.Owner(runtime)
24 if err != nil {
25 t.Fatalf("owner grant: %v", err)
26 }
27 return grant
28 }
29
30 func reviewRuntime(t *testing.T) (*Service, *Runtime) {
31 t.Helper()
32 service, err := NewService("local", NewFilesystemPersistence(filepath.Join(t.TempDir(), "sessions-v4")))
33 if err != nil {
34 t.Fatal(err)
35 }
36 t.Cleanup(func() { _ = service.CloseAll(context.Background()) })
37 runtime, err := service.Create(t.Context(), CreateOptions{SessionID: "review"})
38 if err != nil {
39 t.Fatal(err)
40 }
41 t.Cleanup(func() { _ = runtime.close(context.Background()) })
42 return service, runtime
43 }
44
45 func TestStateSnapshotOmitsHistory(t *testing.T) {
46 _, runtime := reviewRuntime(t)
47 payload, err := json.Marshal(map[string]any{"message": provider.Message{ID: "visible", Role: provider.RoleUser, Content: "hello"}})
48 if err != nil {
49 t.Fatal(err)
50 }
51 if _, err := runtime.Session().AppendBatch(t.Context(), "message", []Event{{Kind: "message/complete", Payload: payload}}); err != nil {
52 t.Fatal(err)
53 }
54 state := runtime.StateSnapshot().Session
55 if state.EventSequence != 1 || len(state.Projection.Messages) != 0 || len(state.Projection.ModelMessages) != 0 {
56 t.Fatalf("activity snapshot includes history or loses its sequence: %+v", state)
57 }
58 if len(runtime.Snapshot().Session.Projection.Messages) != 1 {
59 t.Fatal("state snapshot changed the stored history")
60 }
61 }
62
63 func TestSessionIdentityRejectsPathsWithoutCreatingFiles(t *testing.T) {
64 persistence := NewFilesystemPersistence(t.TempDir())
65 for _, id := range []string{"..", "../escape", "a/b", `a\b`, "/absolute", ".hidden", "CON", "com1.log", "bad:name", ".", ""} {
66 if _, err := persistence.Open(id, ReadWrite); err == nil {
67 t.Fatalf("accepted path as identity: %q", id)
68 }
69 }
70 entries, err := os.ReadDir(persistence.Root)
71 if err != nil || len(entries) != 0 {
72 t.Fatalf("invalid opens changed the root: %v, %v", entries, err)
73 }
74 }
75
76 func TestSessionIdentityRejectsSymlinkOutsideRoot(t *testing.T) {
77 base := t.TempDir()
78 outside := filepath.Join(base, "outside")
79 store, err := CreateStore(outside, "escape")
80 if err != nil {
81 t.Fatal(err)
82 }
83 if err := store.Close(t.Context()); err != nil {
84 t.Fatal(err)
85 }
86 root := filepath.Join(base, "sessions-v4")
87 if err := os.MkdirAll(root, 0o700); err != nil {
88 t.Fatal(err)
89 }
90 if err := os.Symlink(outside, filepath.Join(root, "escape")); err != nil {
91 t.Skipf("symlink unavailable: %v", err)
92 }
93 persistence := NewFilesystemPersistence(root)
94 if _, err := persistence.Stat(t.Context(), "escape"); err == nil {
95 t.Fatal("metadata lookup followed a session symlink outside the store root")
96 }
97 if _, err := persistence.Open("escape", ReadOnly); err == nil {
98 t.Fatal("read-only open followed a session symlink outside the store root")
99 }
100 if err := persistence.Delete(t.Context(), "escape"); err == nil {
101 t.Fatal("delete followed a session symlink outside the store root")
102 }
103 }
104
105 func TestDirectoryOwnershipExcludesWriterDuringRename(t *testing.T) {
106 _, runtime := reviewRuntime(t)
107 store := runtime.Session().Handle().(*Store)
108 if err := runtime.close(t.Context()); err != nil {
109 t.Fatal(err)
110 }
111 release, err := filelock.Acquire(t.Context(), directoryOwnershipPath(store.dir))
112 if err != nil {
113 t.Fatal(err)
114 }
115 defer release()
116 if reopened, err := Open(store.dir, store.SessionID()); !errors.Is(err, ErrWriterOwned) {
117 if reopened != nil {
118 _ = reopened.Close(t.Context())
119 }
120 t.Fatalf("writer entered the directory transition: %v", err)
121 }
122 // The ownership file stays outside the moved tree, including on Windows.
123 if err := os.Rename(store.dir, store.dir+"-removed"); err != nil {
124 t.Fatalf("directory ownership prevents rename: %v", err)
125 }
126 }
127
128 func TestCancelReceiptDoesNotWaitForSessionProjection(t *testing.T) {
129 service, runtime := reviewRuntime(t)
130 ctx, _ := bindTestExecution(t, runtime, "model")
131 store := runtime.Session().Handle().(*Store)
132 store.mu.Lock()
133 defer store.mu.Unlock()
134 done := make(chan error, 1)
135 go func() { _, err := service.CancelSession(runtime.Ref()); done <- err }()
136 select {
137 case err := <-done:
138 if err != nil || !errors.Is(ctx.Err(), context.Canceled) {
139 t.Fatalf("cancel = %v, context = %v", err, ctx.Err())
140 }
141 case <-time.After(5 * time.Second):
142 t.Fatal("cancel receipt waits for the projection lock")
143 }
144 }
145
146 func TestCancelSignalsWithoutRuntimeMutex(t *testing.T) {
147 service, runtime := reviewRuntime(t)
148 ctx, _ := bindTestExecution(t, runtime, "model")
149
150 runtime.mu.Lock()
151 done := make(chan CancelReceipt, 1)
152 go func() {
153 receipt, cancelErr := service.CancelSession(runtime.Ref())
154 if cancelErr != nil {
155 t.Errorf("cancel: %v", cancelErr)
156 }
157 done <- receipt
158 }()
159 select {
160 case receipt := <-done:
161 if !receipt.Accepted || receipt.Phase != RuntimeCancelling {
162 t.Fatalf("receipt = %+v", receipt)
163 }
164 if !errors.Is(ctx.Err(), context.Canceled) {
165 t.Fatalf("activity context = %v", ctx.Err())
166 }
167 case <-time.After(5 * time.Second):
168 runtime.mu.Unlock()
169 t.Fatal("cancel waited for the runtime commit mutex")
170 }
171 runtime.mu.Unlock()
172 }
173
174 func TestUnknownRequiredPrefixDoesNotTruncateTail(t *testing.T) {
175 _, runtime := reviewRuntime(t)
176 store := runtime.Session().Handle().(*Store)
177 if _, err := runtime.Session().Append(t.Context(), Batch{OperationID: "known", Events: []Event{{Kind: "diagnostic"}}}); err != nil {
178 t.Fatal(err)
179 }
180 if err := runtime.close(t.Context()); err != nil {
181 t.Fatal(err)
182 }
183 path := filepath.Join(store.dir, currentLogName)
184 data, err := os.ReadFile(path)
185 if err != nil {
186 t.Fatal(err)
187 }
188 unknown := Commit{
189 SchemaVersion: SchemaVersion, Codec: Codec, RecordType: "commit", ID: "future-commit",
190 OperationID: "future-operation", OperationHash: "future-hash", FirstSequence: 2,
191 EventCount: 1, WriterGeneration: store.Manifest().WriterGeneration, CreatedAt: time.Now().UTC(),
192 Events: []Event{{ID: "future-event", Sequence: 2, Kind: "future/required"}},
193 }
194 var encoded bytes.Buffer
195 if _, err := encodeV4Commits(t.Context(), &encoded, contentStoreForSessionDir(store.dir), []Commit{unknown}); err != nil {
196 t.Fatal(err)
197 }
198 data = append(data, encoded.Bytes()...)
199 data = append(data, []byte(`{"torn":`)...)
200 if err := os.WriteFile(path, data, 0600); err != nil {
201 t.Fatal(err)
202 }
203 if _, err := Open(store.dir, store.SessionID()); !errors.Is(err, ErrUnsupportedVersion) {
204 t.Fatalf("open = %v", err)
205 }
206 after, err := os.ReadFile(path)
207 if err != nil || !bytes.Equal(after, data) {
208 t.Fatalf("unsupported log was changed: %v", err)
209 }
210 }
211
212 func TestSnapshotCannotMutateAcceptedMessageMetadata(t *testing.T) {
213 _, runtime := reviewRuntime(t)
214 payload, err := json.Marshal(map[string]any{"message": provider.Message{ID: "m1", Role: provider.RoleUser, Images: []string{"original"}}})
215 if err != nil {
216 t.Fatal(err)
217 }
218 if _, err := runtime.Session().AppendBatch(t.Context(), "input", []Event{{Kind: "message/complete", Payload: payload}}); err != nil {
219 t.Fatal(err)
220 }
221 snapshot := runtime.Session().Snapshot()
222 snapshot.Projection.Messages[0].Images[0] = "changed"
223 snapshot.Projection.ModelMessages[0].Images[0] = "changed-again"
224 if got := runtime.Session().Snapshot().Projection.Messages[0].Images[0]; got != "original" {
225 t.Fatalf("observer mutated accepted message: %q", got)
226 }
227 }
228
229 func TestOldRuntimeDisposerCannotCloseSuccessor(t *testing.T) {
230 service, old := reviewRuntime(t)
231 oldOwner := owner(t, service, old)
232 if err := oldOwner.Close(t.Context()); err != nil {
233 t.Fatal(err)
234 }
235 binding, err := service.Open(t.Context(), old.Ref())
236 if err != nil {
237 t.Fatal(err)
238 }
239 next := binding.Runtime()
240 t.Cleanup(func() { _ = binding.Release(context.Background()) })
241 // The delayed disposer still holds its own grant for the retired instance.
242 if err := oldOwner.Close(t.Context()); err != nil {
243 t.Fatal(err)
244 }
245 if got, ok := service.Runtime(next.Ref()); !ok || got != next {
246 t.Fatal("old disposer removed successor")
247 }
248 if _, err := next.Session().AppendBatch(t.Context(), "still-open", []Event{{Kind: "diagnostic", Optional: true}}); err != nil {
249 t.Fatal(err)
250 }
251 }
252
253 func TestClientBindingOwnsDetachButNotRuntimeClose(t *testing.T) {
254 service, runtime := reviewRuntime(t)
255 service.idleTTL = 20 * time.Millisecond
256 first, err := service.Bind(runtime)
257 if err != nil {
258 t.Fatal(err)
259 }
260 second, err := service.Bind(runtime)
261 if err != nil {
262 t.Fatal(err)
263 }
264 if err := owner(t, service, runtime).Close(t.Context()); !errors.Is(err, ErrRuntimeBound) {
265 t.Fatalf("close bound runtime = %v", err)
266 }
267 if err := first.Release(t.Context()); err != nil {
268 t.Fatal(err)
269 }
270 if got, ok := service.Runtime(runtime.Ref()); !ok || got != runtime {
271 t.Fatal("one client detached the runtime used by another client")
272 }
273 if err := second.Release(t.Context()); err != nil {
274 t.Fatal(err)
275 }
276 if got, ok := service.Runtime(runtime.Ref()); !ok || got != runtime {
277 t.Fatal("idle runtime was not retained for quick rebinding")
278 }
279 deadline := time.Now().Add(time.Second)
280 for time.Now().Before(deadline) {
281 if _, ok := service.Runtime(runtime.Ref()); !ok {
282 return
283 }
284 time.Sleep(time.Millisecond)
285 }
286 t.Fatal("idle runtime remained published after its retention period")
287 }
288
289 func TestIdleRuntimeCacheBudgetRetiresLeastRecentlyUsedRuntime(t *testing.T) {
290 service, first := reviewRuntime(t)
291 service.idleTTL = time.Hour
292 service.idleBudget = 64 << 10
293 firstBinding, err := service.Bind(first)
294 if err != nil {
295 t.Fatal(err)
296 }
297 second, err := service.Create(t.Context(), CreateOptions{SessionID: "budget-second"})
298 if err != nil {
299 t.Fatal(err)
300 }
301 t.Cleanup(func() {
302 for _, ref := range []SessionRef{first.Ref(), second.Ref()} {
303 if _, live := service.Runtime(ref); live {
304 _ = service.Close(context.Background(), ref)
305 }
306 }
307 })
308 secondBinding, err := service.Bind(second)
309 if err != nil {
310 t.Fatal(err)
311 }
312 if err := firstBinding.Release(t.Context()); err != nil {
313 t.Fatal(err)
314 }
315 if err := secondBinding.Release(t.Context()); err != nil {
316 t.Fatal(err)
317 }
318 deadline := time.Now().Add(time.Second)
319 for time.Now().Before(deadline) {
320 _, firstLive := service.Runtime(first.Ref())
321 _, secondLive := service.Runtime(second.Ref())
322 if !firstLive && secondLive {
323 return
324 }
325 time.Sleep(time.Millisecond)
326 }
327 t.Fatal("idle cache budget did not retire the least recently used runtime")
328 }
329
330 func TestLastClientDetachDoesNotCancelActiveRuntime(t *testing.T) {
331 service, runtime := reviewRuntime(t)
332 service.idleTTL = 20 * time.Millisecond
333 binding, err := service.Bind(runtime)
334 if err != nil {
335 t.Fatal(err)
336 }
337 ctx, exec := bindTestExecution(t, runtime, "model")
338 if err := binding.Release(t.Context()); err != nil {
339 t.Fatal(err)
340 }
341 if ctx.Err() != nil {
342 t.Fatalf("client detach cancelled host-owned activity: %v", ctx.Err())
343 }
344 if got, ok := service.Runtime(runtime.Ref()); !ok || got != runtime {
345 t.Fatal("active runtime retired when its last client detached")
346 }
347 exec.Finish()
348 if _, ok := service.Runtime(runtime.Ref()); !ok {
349 t.Fatal("completed runtime was not retained for quick rebinding")
350 }
351 deadline := time.Now().Add(time.Second)
352 for time.Now().Before(deadline) {
353 if _, ok := service.Runtime(runtime.Ref()); !ok {
354 return
355 }
356 time.Sleep(time.Millisecond)
357 }
358 t.Fatal("unbound runtime did not retire after its retention period")
359 }
360
361 func TestOwnerCloseFencesConcurrentClientBinding(t *testing.T) {
362 service, sessionRuntime := reviewRuntime(t)
363 sessionRuntime.mu.Lock()
364 closed := make(chan error, 1)
365 grant := owner(t, service, sessionRuntime)
366 go func() { closed <- grant.Close(t.Context()) }()
367 deadline := time.After(5 * time.Second)
368 for {
369 service.mu.Lock()
370 retiring := service.retiring[sessionRuntime] != nil
371 service.mu.Unlock()
372 if retiring {
373 break
374 }
375 select {
376 case <-deadline:
377 sessionRuntime.mu.Unlock()
378 t.Fatal("owner close did not publish its retirement fence")
379 default:
380 runtime.Gosched()
381 }
382 }
383 if _, err := service.Bind(sessionRuntime); !errors.Is(err, ErrRuntimeRetiring) {
384 sessionRuntime.mu.Unlock()
385 t.Fatalf("bind during owner close = %v", err)
386 }
387 sessionRuntime.mu.Unlock()
388 if err := <-closed; err != nil {
389 t.Fatal(err)
390 }
391 }
392
393 func TestTitleCanReturnToEarlierValue(t *testing.T) {
394 service, runtime := reviewRuntime(t)
395 for _, title := range []string{"A", "B", "A"} {
396 if err := service.SetTitle(t.Context(), runtime.Ref(), title); err != nil {
397 t.Fatal(err)
398 }
399 if got := runtime.Session().Snapshot().Projection.Title; got != title {
400 t.Fatalf("title = %q, want %q", got, title)
401 }
402 }
403 }
404
405 func TestCloseFailureUnregistersReleasedWriter(t *testing.T) {
406 service, runtime := reviewRuntime(t)
407 store := runtime.Session().Handle().(*Store)
408 failure := errors.New("disk unavailable")
409 store.writeFn = func(context.Context, io.Writer, []byte) error { return failure }
410 if _, err := runtime.Session().AppendBatch(t.Context(), "pending", []Event{{Kind: "diagnostic", Optional: true}}); err != nil {
411 t.Fatal(err)
412 }
413 if err := service.Close(t.Context(), runtime.Ref()); !errors.Is(err, failure) {
414 t.Fatalf("close = %v", err)
415 }
416 if _, ok := service.Runtime(runtime.Ref()); ok {
417 t.Fatal("closed writer remains attachable")
418 }
419 if err := service.Close(t.Context(), runtime.Ref()); !errors.Is(err, failure) {
420 t.Fatalf("repeat close = %v", err)
421 }
422 binding, err := service.Open(t.Context(), runtime.Ref())
423 if err != nil {
424 t.Fatal(err)
425 }
426 t.Cleanup(func() { _ = binding.Release(context.Background()) })
427 if binding.Runtime() == runtime {
428 t.Fatal("open returned released writer")
429 }
430 }
431
432 func TestRewindBeforeFirstTurnPreservesInitialization(t *testing.T) {
433 service, runtime := reviewRuntime(t)
434 if _, err := runtime.Session().AppendBatch(t.Context(), "config", []Event{{Kind: "session/config", Payload: []byte(`{"modelRef":"test/model"}`)}}); err != nil {
435 t.Fatal(err)
436 }
437 if _, err := runtime.Session().Append(t.Context(), Batch{OperationID: "input", TurnID: "first", Events: []Event{{Kind: "turn/start"}, {Kind: "turn/end", Payload: []byte(`{"status":"completed"}`)}}}); err != nil {
438 t.Fatal(err)
439 }
440 child, err := service.Rewind(t.Context(), runtime.Ref(), "first", "rewound")
441 if err != nil {
442 t.Fatal(err)
443 }
444 t.Cleanup(func() { _ = service.Close(context.Background(), child.Ref()) })
445 if got := child.Session().Snapshot().Projection.ModelRef; got != "test/model" {
446 t.Fatalf("model = %q", got)
447 }
448 }
449
450 func TestFinishedActivityCancelsItsContext(t *testing.T) {
451 _, runtime := reviewRuntime(t)
452 ctx, exec := bindTestExecution(t, runtime, "model")
453 exec.cancel()
454 exec.Finish()
455 if !errors.Is(ctx.Err(), context.Canceled) {
456 t.Fatal("finished activity retains live cancellation context")
457 }
458 }
459
460 func TestForkCopiesOwnedAttachments(t *testing.T) {
461 service, runtime := reviewRuntime(t)
462 dir := runtime.Session().Handle().(*Store).dir
463 asset := filepath.Join(dir, "attachments", "input.txt")
464 if err := os.MkdirAll(filepath.Dir(asset), 0700); err != nil {
465 t.Fatal(err)
466 }
467 if err := os.WriteFile(asset, []byte("owned context"), 0600); err != nil {
468 t.Fatal(err)
469 }
470 if _, err := runtime.Session().Append(t.Context(), Batch{OperationID: "input", TurnID: "first", Events: []Event{{Kind: "turn/start"}, {Kind: "turn/end", Payload: []byte(`{"status":"completed"}`)}}}); err != nil {
471 t.Fatal(err)
472 }
473 child, err := service.Fork(t.Context(), runtime.Ref(), "first", "child")
474 if err != nil {
475 t.Fatal(err)
476 }
477 t.Cleanup(func() { _ = service.Close(context.Background(), child.Ref()) })
478 if err := service.Delete(t.Context(), runtime.Ref()); err != nil {
479 t.Fatal(err)
480 }
481 data, err := os.ReadFile(filepath.Join(child.Session().Handle().(*Store).dir, "attachments", "input.txt"))
482 if err != nil || string(data) != "owned context" {
483 t.Fatalf("child attachment = %q, %v", data, err)
484 }
485 }
486
486 lines GO