| 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 |