package store import ( "errors" "fmt" "strings" "testing" ) // outbox_test.go pins the two properties that put the run-event outbox in SQLite instead of in a // variable: a sequence number that a rollback gives back (the reader treats a hole as fatal), and bytes // that survive to be re-projected identically (the reader treats a changed payload at a known seq as // corruption). func line(seq int64) []byte { return []byte(fmt.Sprintf(`{"seq":%d}`, seq)) } func enqueue(t *testing.T, s *Store, runID string, want int64) { t.Helper() if err := s.EnqueueEvent(runID, func(seq int64) ([]byte, error) { if seq != want { t.Errorf("next seq for %s = %d, want %d", runID, seq, want) } return line(seq), nil }); err != nil { t.Fatal(err) } } func TestSequenceNumbersAreDenseAndPerRun(t *testing.T) { s, _ := openTemp(t) enqueue(t, s, "run-a", 1) enqueue(t, s, "run-a", 2) // A second process numbers from 1 again in the SAME book: the ratified idempotency key is // (engine_run_id, seq), so a resumed run is a new stream, not a continuation of the old numbers. enqueue(t, s, "run-b", 1) enqueue(t, s, "run-a", 3) got, err := s.PendingEvents("run-a", 0, 100) if err != nil { t.Fatal(err) } if len(got) != 3 { t.Fatalf("want run-a's three lines, got %d", len(got)) } for i, e := range got { if e.Seq != int64(i+1) { t.Fatalf("run-a seq %d at position %d — the stream must be dense and ordered", e.Seq, i) } } } func TestARolledBackEventConsumesNoSequenceNumber(t *testing.T) { // The hole is what makes this worth a transaction: the reader's ErrStreamGap is fatal, so a number // handed out to a render that then failed must go back. s, _ := openTemp(t) enqueue(t, s, "run", 1) boom := errors.New("render failed") if err := s.EnqueueEvent("run", func(int64) ([]byte, error) { return nil, boom }); !errors.Is(err, boom) { t.Fatalf("want the render error back, got %v", err) } enqueue(t, s, "run", 2) // NOT 3 } func TestAStoredLineIsHandedBackByteForByte(t *testing.T) { // Re-projection after a failed journal write must produce the identical line: the reader compares a // re-read line against the sha256 it stored, and a fresher render at the same seq is a payload // conflict, which quarantines the projection. s, _ := openTemp(t) original := []byte(`{"seq":1,"type":"spend","time":"2026-08-14T10:00:00Z","data":{"committed_micro_usd":7}}`) if err := s.EnqueueEvent("run", func(int64) ([]byte, error) { return original, nil }); err != nil { t.Fatal(err) } for i := 0; i < 2; i++ { got, err := s.PendingEvents("run", 0, 100) if err != nil { t.Fatal(err) } if len(got) != 1 || string(got[0].Line) != string(original) { t.Fatalf("read %d: %+v, want %q", i, got, original) } } } func TestTheAnnounceLedgerRecordsDeliveryAndNotIntent(t *testing.T) { // The order is the design: an event is ledgered only once its line is ON THE FILE. A row that was // enqueued and never delivered leaves no ledger entry, so the next process announces it again — // at-least-once, which is what the ratified contract asks for at a boundary no transaction spans. s, _ := openTemp(t) announcing, seq, err := s.EnqueueOnce("run", "unit:draft:1:0", func(seq int64) ([]byte, error) { return line(seq), nil }) if err != nil || !announcing || seq != 1 { t.Fatalf("first announcement: announcing=%v seq=%d err=%v", announcing, seq, err) } // Not yet delivered ⇒ not yet ledgered. if err := s.MarkAnnounced("run", map[int64]string{seq: "unit:draft:1:0"}); err != nil { t.Fatal(err) } announcing, _, err = s.EnqueueOnce("run", "unit:draft:1:0", func(seq int64) ([]byte, error) { return line(seq), nil }) if err != nil { t.Fatal(err) } if announcing { t.Fatal("a delivered announcement must not be made twice — a reader increments on it") } // A DIFFERENT unit is unaffected, and numbering continues where it left off. announcing, seq, err = s.EnqueueOnce("run", "unit:edit:1:0", func(seq int64) ([]byte, error) { return line(seq), nil }) if err != nil || !announcing || seq != 2 { t.Fatalf("second unit: announcing=%v seq=%d err=%v", announcing, seq, err) } } func TestAnUndeliveredAnnouncementIsRetriedByTheNextRunAndADeliveredOneIsNot(t *testing.T) { // The crash case, at the storage layer: one unit's line reached the file, another's did not. After the // next process clears the buffer, the delivered one is still ledgered and the undelivered one is free // to be announced again. s, _ := openTemp(t) _, delivered, err := s.EnqueueOnce("dead-run", "unit:draft:1:0", func(seq int64) ([]byte, error) { return line(seq), nil }) if err != nil { t.Fatal(err) } if _, _, err := s.EnqueueOnce("dead-run", "unit:draft:2:0", func(seq int64) ([]byte, error) { return line(seq), nil }); err != nil { t.Fatal(err) } if err := s.MarkAnnounced("dead-run", map[int64]string{delivered: "unit:draft:1:0"}); err != nil { t.Fatal(err) } if err := s.ForgetEvents("live-run"); err != nil { t.Fatal(err) } if announcing, _, err := s.EnqueueOnce("live-run", "unit:draft:1:0", func(seq int64) ([]byte, error) { return line(seq), nil }); err != nil || announcing { t.Fatalf("the delivered unit must stay silent: announcing=%v err=%v", announcing, err) } if announcing, _, err := s.EnqueueOnce("live-run", "unit:draft:2:0", func(seq int64) ([]byte, error) { return line(seq), nil }); err != nil || !announcing { t.Fatalf("the unit whose line never landed must be announced again: announcing=%v err=%v", announcing, err) } } func TestPendingEventsStartAfterTheCursor(t *testing.T) { s, _ := openTemp(t) for i := int64(1); i <= 4; i++ { enqueue(t, s, "run", i) } got, err := s.PendingEvents("run", 2, 100) if err != nil { t.Fatal(err) } if len(got) != 2 || got[0].Seq != 3 || got[1].Seq != 4 { t.Fatalf("want seq 3,4 after the cursor at 2, got %+v", got) } } func TestForgetEventsKeepsOnlyTheCurrentRun(t *testing.T) { // The outbox is a projection buffer, not an archive — otherwise a book resumed a hundred times keeps // a hundred runs' worth of lines nothing will ever read. s, _ := openTemp(t) enqueue(t, s, "old", 1) enqueue(t, s, "new", 1) if err := s.ForgetEvents("new"); err != nil { t.Fatal(err) } if got, err := s.PendingEvents("old", 0, 100); err != nil || len(got) != 0 { t.Fatalf("the previous run's lines must be gone, got %+v (%v)", got, err) } if got, err := s.PendingEvents("new", 0, 100); err != nil || len(got) != 1 { t.Fatalf("this run's lines must survive, got %+v (%v)", got, err) } } func TestTheSpendEventIsCommittedByTheSettleThatEarnedIt(t *testing.T) { s, _ := openTemp(t) job := mustSnapshotAndJob(t, s) caps := Ceilings{BookUSD: 1.0, DayUSD: 2.0} res, verdict, err := s.Reserve("book", 0.10, caps) if err != nil || verdict != ReserveOK { t.Fatalf("reserve: %v %v", verdict, err) } cp := Checkpoint{RequestHash: "h1", JobID: job.ID, Stage: "draft", Role: "translator", ModelRequested: "m", ModelActual: "m", ResponseText: "текст", UsageJSON: "{}", CostUSD: 0.04} var sawCommitted float64 spend := &SpendLine{RunID: "run", Line: func(seq int64, committedUSD float64) ([]byte, error) { sawCommitted = committedUSD return line(seq), nil }} if err := s.SettleWithCheckpoint(res, 0.04, cp, spend); err != nil { t.Fatal(err) } // The figure is read INSIDE the transaction, AFTER the debit: an event that reported the pre-settle // total would under-report the book's spend by exactly the call it is announcing. if sawCommitted != 0.04 { t.Fatalf("the event saw committed=%v, want the post-settle 0.04", sawCommitted) } if got, err := s.PendingEvents("run", 0, 100); err != nil || len(got) != 1 { t.Fatalf("the settle must have booked its event, got %+v (%v)", got, err) } } func TestASettleThatRollsBackLeavesNeitherCheckpointNorEvent(t *testing.T) { // This is the whole claim of "the event line is written in the SAME transaction as the checkpoint": // the two cannot disagree, in either direction. (The driver never lets a render failure reach here — // losing a paid checkpoint to protect an indicator would be the wrong way round — but the storage // layer's guarantee is what that policy rests on.) s, _ := openTemp(t) job := mustSnapshotAndJob(t, s) res, _, err := s.Reserve("book", 0.10, Ceilings{BookUSD: 1.0}) if err != nil { t.Fatal(err) } cp := Checkpoint{RequestHash: "h-doomed", JobID: job.ID, Stage: "draft", Role: "translator", ModelRequested: "m", ModelActual: "m", ResponseText: "текст", UsageJSON: "{}", CostUSD: 0.04} boom := errors.New("render failed") spend := &SpendLine{RunID: "run", Line: func(int64, float64) ([]byte, error) { return nil, boom }} if err := s.SettleWithCheckpoint(res, 0.04, cp, spend); !errors.Is(err, boom) { t.Fatalf("want the render error, got %v", err) } got, err := s.GetCheckpoint("h-doomed") if err != nil { t.Fatal(err) } if got != nil { t.Fatal("the checkpoint must have rolled back with the event") } committed, _, err := s.SpentUSD("book") if err != nil { t.Fatal(err) } if committed != 0 { t.Fatalf("the debit must have rolled back too, committed=%v", committed) } if lines, err := s.PendingEvents("run", 0, 100); err != nil || len(lines) != 0 { t.Fatalf("no event may survive the transaction that failed, got %+v (%v)", lines, err) } } func TestADuplicateSettleBooksNoSecondSpendEvent(t *testing.T) { // A duplicate settle books no money, so it must announce none: the counter is cumulative and a second // line at the same total is noise the reader would still have to hash and store. s, _ := openTemp(t) job := mustSnapshotAndJob(t, s) cp := Checkpoint{RequestHash: "h1", JobID: job.ID, Stage: "draft", Role: "translator", ModelRequested: "m", ModelActual: "m", ResponseText: "текст", UsageJSON: "{}", CostUSD: 0.04} spend := &SpendLine{RunID: "run", Line: func(seq int64, _ float64) ([]byte, error) { return line(seq), nil }} for i := 0; i < 2; i++ { res, _, err := s.Reserve("book", 0.10, Ceilings{BookUSD: 1.0}) if err != nil { t.Fatal(err) } if err := s.SettleWithCheckpoint(res, 0.04, cp, spend); err != nil { t.Fatal(err) } } lines, err := s.PendingEvents("run", 0, 100) if err != nil { t.Fatal(err) } if len(lines) != 1 { t.Fatalf("want one spend event for one billed call, got %d", len(lines)) } } func TestTheAnnounceLedgerLookupUsesItsIndex(t *testing.T) { // The uniqueness index on once_key is PARTIAL, and SQLite uses a partial index only when the query // implies its predicate SYNTACTICALLY — with a bind parameter it cannot prove ?1 <> '' while planning. // The bare equality therefore plans as a full SCAN, once per announced unit: quadratic over a book, on // the wave's own path. Reproduced on modernc.org/sqlite before the fix; this is the pin. s, _ := openTemp(t) rows, err := s.r.Query("EXPLAIN QUERY PLAN "+onceKeyLookup, "unit:x:1:0") if err != nil { t.Fatal(err) } defer rows.Close() plan := "" for rows.Next() { var a, b, c int var detail string if err := rows.Scan(&a, &b, &c, &detail); err != nil { t.Fatal(err) } plan += detail + "\n" } if err := rows.Err(); err != nil { t.Fatal(err) } if !strings.Contains(plan, "USING COVERING INDEX events_outbox_once") { t.Fatalf("the announce-ledger lookup does not use its index:\n%s", plan) } if strings.Contains(plan, "SCAN events_outbox") { t.Fatalf("the announce-ledger lookup scans the table:\n%s", plan) } } func TestPendingEventsReturnsAtMostOneBatch(t *testing.T) { // The bound is what keeps a DEGRADED journal from being quadratic: the caller's cursor cannot advance // while the file refuses writes, so an unbounded read would re-materialize the whole growing prefix on // every event, under the emitter's mutex. s, _ := openTemp(t) for i := int64(1); i <= 10; i++ { enqueue(t, s, "run", i) } got, err := s.PendingEvents("run", 0, 4) if err != nil { t.Fatal(err) } if len(got) != 4 || got[0].Seq != 1 || got[3].Seq != 4 { t.Fatalf("want the first four, got %d rows starting at %d", len(got), got[0].Seq) } rest, err := s.PendingEvents("run", got[3].Seq, 100) if err != nil { t.Fatal(err) } if len(rest) != 6 || rest[0].Seq != 5 { t.Fatalf("the next read must continue at 5 and drain, got %d rows", len(rest)) } }