返回 DeepSeek-Reasonix
receipt_store_test.go
根目录 / internal / extension / receipt_store_test.go
1 package extension
2
3 import (
4 "context"
5 "testing"
6 )
7
8 func TestReceiptStoreIrreversibleNeverRolledBack(t *testing.T) {
9 s := NewReceiptStore()
10 s.Record(EffectReceipt{ID: "msg-1", Class: Irreversible, Generation: 3, CompensationStatus: "rolled_back"})
11 r, ok := s.Get("msg-1")
12 if !ok || r.CompensationStatus != "not_applicable" {
13 t.Fatalf("receipt = %+v", r)
14 }
15 rec := s.AssessRecoverability(3)
16 if rec.Clean || !rec.HasIrreversible {
17 t.Fatalf("recoverability = %+v", rec)
18 }
19 }
20
21 func TestReceiptStoreIngestScope(t *testing.T) {
22 scope := NewEffectScope(7)
23 _ = scope.Track(Effect{
24 ID: "file-write", Owner: "test", Class: Compensatable,
25 Dispose: func(context.Context) error { return nil },
26 Compensate: func(context.Context) error { return nil },
27 })
28 _ = scope.Dispose(context.Background())
29 s := NewReceiptStore()
30 s.IngestScope(scope)
31 if len(s.ForGeneration(7)) != 1 {
32 t.Fatalf("expected 1 receipt, got %d", len(s.ForGeneration(7)))
33 }
34 }
35
36 func TestReceiptStoreKeepsSameIDAcrossGenerations(t *testing.T) {
37 s := NewReceiptStore()
38 s.Record(EffectReceipt{ID: "shared", Owner: "old", Generation: 1, Class: Irreversible})
39 s.Record(EffectReceipt{ID: "shared", Owner: "new", Generation: 2, Class: Irreversible})
40
41 oldReceipts := s.ForGeneration(1)
42 newReceipts := s.ForGeneration(2)
43 if len(oldReceipts) != 1 || oldReceipts[0].Owner != "old" || oldReceipts[0].ID != "shared" {
44 t.Fatalf("old receipts = %+v", oldReceipts)
45 }
46 if len(newReceipts) != 1 || newReceipts[0].Owner != "new" || newReceipts[0].ID == "shared" {
47 t.Fatalf("new receipts = %+v", newReceipts)
48 }
49 if old, ok := s.Get("shared"); !ok || old.Owner != "old" {
50 t.Fatalf("original receipt was overwritten: %+v ok=%v", old, ok)
51 }
52 }
53
54 func TestReceiptStoreBoundsGenerationAndReceiptRetention(t *testing.T) {
55 s := newReceiptStore(2, 2, nil)
56 for gen := uint64(1); gen <= 3; gen++ {
57 for i := range 2 {
58 s.Record(EffectReceipt{ID: itoaU64(gen) + "-" + itoaU64(uint64(i)), Generation: gen, Class: Irreversible})
59 }
60 }
61 if got := len(s.ForGeneration(1)); got != 0 {
62 t.Fatalf("evicted generation receipts = %d, want 0", got)
63 }
64 if rec := s.AssessRecoverability(1); rec.Clean || len(rec.Blocking) == 0 {
65 t.Fatalf("evicted generation must not claim clean recovery: %+v", rec)
66 }
67
68 for i := range 3 {
69 s.Record(EffectReceipt{ID: "current-" + itoaU64(uint64(i)), Generation: 4, Class: Irreversible})
70 }
71 if got := len(s.ForGeneration(4)); got != 2 {
72 t.Fatalf("retained receipts = %d, want 2", got)
73 }
74 if rec := s.AssessRecoverability(4); rec.Clean || len(rec.Blocking) == 0 {
75 t.Fatalf("truncated generation must not claim clean recovery: %+v", rec)
76 }
77 }
78
79 func TestReceiptStoreTruncatedPriorBlocksCleanRecovery(t *testing.T) {
80 s := NewReceiptStore()
81 s.Record(EffectReceipt{
82 ID: "large-prior",
83 Generation: 5,
84 Class: Compensatable,
85 CompensationStatus: "prior_truncated",
86 })
87 if rec := s.AssessRecoverability(5); rec.Clean || len(rec.Blocking) != 1 {
88 t.Fatalf("truncated prior recoverability = %+v", rec)
89 }
90 }
91
91 lines GO