textmachine/backend/internal/pipeline/retrystopmark_test.go
2026-09-15 14:18:58 +03:00

802 lines
38 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package pipeline
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"sync"
"testing"
"textmachine/backend/internal/chunk"
"textmachine/backend/internal/llm"
"textmachine/backend/internal/obs"
"textmachine/backend/internal/runevents"
"textmachine/backend/internal/store"
)
// retrystopmark_test.go: the acceptance battery for backlog row 291 — a USD ceiling that refuses the
// RETRY of an attempt this book ALREADY PAID FOR stops the run like any other ceiling, and the position
// it stopped over gets a MARK instead of reading as never started.
//
// Every test here is $0: the provider is an httptest server, no vendor key is touched, and the ceiling
// that drives the whole fixture is derived from the engine's OWN estimate of its own calls.
// The two chapter markers. Each fixture chapter is fixtureChapterRunes of ONE Han character, which is
// what lets the fake provider tell the chapters apart: the body carries the source text verbatim.
const (
cleanChapterRune = "文"
retriedChapterRune = "字"
)
// retryStopSource is two single-chunk chapters: the first answers cleanly, the second answers
// finish=length on its FIRST call so the attempt axis asks for a doubled budget, which is the purchase
// this fixture's ceiling refuses.
func retryStopSource() string {
return strings.Repeat(cleanChapterRune, fixtureChapterRunes) + "。\n\n" +
strings.Repeat(retriedChapterRune, fixtureChapterRunes) + "。"
}
// The two drafts the provider answers with. They differ so a test can say WHICH call's text a finished
// unit is holding — «the retry finished the unit» is otherwise indistinguishable from «attempt 0 was
// served from its checkpoint».
const (
draftTruncated = "ЧЕРНОВИК ОБОРВАН НА ПОТОЛКЕ"
draftAfterRetry = "ЧЕРНОВИК ПЕРЕВОДА ПОСЛЕ РЕТРАЯ"
)
// lengthOncePerChapter answers finish=length for the FIRST call on the retried chapter and `stop` for
// every other call, counting calls per chapter. The count is the test's only instrument for «no fresh
// paid call happened», so it is taken per chapter rather than in total.
type lengthOncePerChapter struct {
srv *httptest.Server
mu sync.Mutex
n map[string]int
}
func newLengthOncePerChapter(t *testing.T) *lengthOncePerChapter {
t.Helper()
p := &lengthOncePerChapter{n: map[string]int{}}
p.srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
marker := cleanChapterRune
if strings.Contains(string(body), strings.Repeat(retriedChapterRune, 8)) {
marker = retriedChapterRune
}
p.mu.Lock()
p.n[marker]++
nth := p.n[marker]
p.mu.Unlock()
text, finish := draftAfterRetry, "stop"
if marker == retriedChapterRune && nth == 1 {
// A short, varied completion: the degeneration detector must NOT claim a repetition loop
// (degenerateLoop needs 30+ words), or the verdict would be loop_degenerate — deterministic,
// NOT retryable — and this fixture would never reach the retry it is about.
text, finish = draftTruncated, "length"
}
tb, _ := json.Marshal(text)
fmt.Fprintf(w, `{"id":"fake","model":"fake-model","choices":[{"message":{"content":%s},"finish_reason":%q}],
"usage":{"prompt_tokens":1000,"completion_tokens":500,"prompt_tokens_details":{"cached_tokens":200}}}`,
tb, finish)
}))
t.Cleanup(p.srv.Close)
return p
}
func (p *lengthOncePerChapter) calls(marker string) int {
p.mu.Lock()
defer p.mu.Unlock()
return p.n[marker]
}
func (p *lengthOncePerChapter) total() int {
p.mu.Lock()
defer p.mu.Unlock()
return p.n[cleanChapterRune] + p.n[retriedChapterRune]
}
// retryStopFixture builds the project and returns it together with the manifest, asserting the SHAPE the
// whole battery depends on: TWO output units, the retried one second. A fixture that silently cut
// differently would still run — and would measure a ceiling refusing something else.
//
// ⚠ The two units are two CHUNKS of one chapter rather than two chapters, and that is measured rather
// than chosen: this fixture's source language ships no langpack, so no heading rule fires and the ingest
// reads the whole file as chapter 1 (the first version of this file asserted two chapters and said so —
// `chapters [1 1]`). It makes no difference to the subject: a draft-only pipeline makes every CHUNK its
// own output unit (outputUnits), and chunk_status is keyed per chunk.
func retryStopFixture(t *testing.T, providerURL string) (string, []chunk.Chunk) {
t.Helper()
bookPath := setupProjectOpts(t, providerURL, projectOpts{
source: retryStopSource(),
// The retry axis must be OPEN (a budget of one regeneration) — with 0 the flagged attempt is
// terminal and no reservation for a second attempt is ever asked for.
regenerate: 1,
// Draft-only: the draft IS the shipping stage, so the fixture's money is two calls plus the
// refused retry and nothing else.
draftOnly: true,
})
r := newRunner(t, bookPath)
defer r.Close()
manifest, err := r.bookChunks()
if err != nil {
t.Fatal(err)
}
if len(manifest) != 2 || manifest[0].ChunkIdx == manifest[1].ChunkIdx {
t.Fatalf("fixture must cut into 2 units, got %d chunk(s) at indices %v", len(manifest), indicesOf(manifest))
}
if !strings.Contains(manifest[1].Text, retriedChapterRune) {
t.Fatalf("the RETRIED chapter must be the second unit, or the ceiling arithmetic below is about the wrong call: chunk 1 text starts %.12q", manifest[1].Text)
}
return bookPath, manifest
}
func indicesOf(chunks []chunk.Chunk) [][2]int {
out := make([][2]int, 0, len(chunks))
for _, c := range chunks {
out = append(out, [2]int{c.Chapter, c.ChunkIdx})
}
return out
}
// retryStopCeiling is a ceiling that admits every ATTEMPT 0 of this fixture and refuses the DOUBLED
// retry. It is derived from the engine's own arithmetic — callEstimateUSD over the messages runStage
// renders and the budget maxTokensForAttempt hands it — rather than written as a literal or re-assembled
// from the same ingredients a second time, because two derivations of one number drift and then the
// fixture is quietly about a different situation.
//
// ⚠ The money the engine COMMITS per call is smaller than what it RESERVES (EstimateUSD is deliberately
// pessimistic), so the window is wide: [committed of one call + est0, committed of two + est1). Both ends
// are printed and the premise «the window exists» is asserted, because a pricing or sizing change that
// closed it would otherwise make this whole file pass while measuring nothing.
func retryStopCeiling(t *testing.T, r *Runner, manifest []chunk.Chunk) (ceiling, est0, est1 float64) {
// The retried unit is the SECOND of two, so its attempt 0 is admitted with one call's spend already
// committed and its re-attack refused with two.
return ceilingThatRefusesTheReattack(t, r, manifest[1], 1)
}
// ceilingThatRefusesTheReattack is the window for ANY fixture of this shape, in the engine's own
// arithmetic: `admitted` is how many of this fixture's calls are already committed when the position's
// attempt 0 asks for its reservation.
func ceilingThatRefusesTheReattack(t *testing.T, r *Runner, ch chunk.Chunk, admitted int) (ceiling, est0, est1 float64) {
t.Helper()
// The draft stage sizes and renders from the SOURCE; the edit stage from the draft, which is why the
// twin below takes both rather than two copies of this arithmetic existing.
return ceilingThatRefusesTheReattackAt(t, r, 0, ch, ch.Text, admitted)
}
// ceilingThatRefusesTheReattackAt is the same window for ANY stage of the pipeline: `sizingText` is what
// that stage's budget is sized from and what its template receives as the prior draft (runStage's own
// rule — the source for the translator, the previous stage's text after it).
func ceilingThatRefusesTheReattackAt(t *testing.T, r *Runner, stageIdx int, ch chunk.Chunk, sizingText string, admitted int) (ceiling, est0, est1 float64) {
t.Helper()
st := r.Pipeline.Stages[stageIdx]
vars := RenderVars{Book: r.Book, Text: ch.Text}
if stageIdx > 0 {
vars.Draft = sizingText
}
msgs, err := MessagesWithInjection(r.templates[st.Name], vars, "")
if err != nil {
t.Fatal(err)
}
base := r.baseMaxTokensFor(st, EstimateTokens(sizingText))
est0 = r.callEstimateUSD(st, st.ResolvedModel, msgs, base)
est1 = r.callEstimateUSD(st, st.ResolvedModel, msgs, maxTokensForAttempt(base, 1))
lo := float64(admitted)*fakeCallUSD + est0 // this position's attempt 0 must be admitted
hi := float64(admitted+1)*fakeCallUSD + est1 // its doubled re-attack must NOT be
if lo >= hi {
t.Fatalf("no ceiling can admit attempt 0 and refuse the re-attack: lo=%.6f hi=%.6f (est0=%.6f est1=%.6f call=%.6f)", lo, hi, est0, est1, fakeCallUSD)
}
ceiling = (lo + hi) / 2
t.Logf("fixture window: est0=$%.6f est1=$%.6f committed/call=$%.6f ⇒ ceiling=$%.6f admits attempt 0 (needs ≤$%.6f) and refuses the re-attack (needs $%.6f)",
est0, est1, fakeCallUSD, ceiling, lo, hi)
return ceiling, est0, est1
}
func draftRowOf(t *testing.T, r *Runner, rows []store.ChunkStatus, ch chunk.Chunk) *store.ChunkStatus {
t.Helper()
for i := range rows {
if rows[i].Chapter == ch.Chapter && rows[i].ChunkIdx == ch.ChunkIdx && rows[i].Stage == r.Pipeline.Stages[0].Name {
return &rows[i]
}
}
return nil
}
// TestTheRefusedRetryLeavesTheUnitAMarkAndNotAnEmptyPosition is pin 1 of row 291, and the measurement is
// the one the tree itself uses to describe the earlier incident of this class (stagerun.go, «THE MARK FOR
// A STOPPED POSITION IS ATTACHED TO EVERY EXIT»): money booked against a position with NO chunk_status
// row at all. Before this pack the refused retry left exactly that — committed > 0 and zero rows for the
// unit — so the unit read `pending`, indistinguishable from one nobody had started, while its first
// attempt was paid for and on disk.
func TestTheRefusedRetryLeavesTheUnitAMarkAndNotAnEmptyPosition(t *testing.T) {
prov := newLengthOncePerChapter(t)
bookPath, manifest := retryStopFixture(t, prov.srv.URL)
ctx := obs.WithReqInfo(context.Background(), obs.ReqInfo{TraceID: obs.NewTraceID()})
r := newRunner(t, bookPath)
defer r.Close()
ceiling, _, _ := retryStopCeiling(t, r, manifest)
r.CeilingUSD = ceiling
_, err := r.TranslateBook(ctx)
var halt *CeilingHalt
if !errors.As(err, &halt) {
t.Fatalf("a ceiling that cannot afford the retry must stop the run as a ceiling halt, got %v", err)
}
// The premise, asserted and printed: the refusal fell on the RETRY, not on a fresh attempt 0. Two
// calls reached the provider — one per chapter — and the third, the retry, was never dialled.
if got, retried := prov.total(), prov.calls(retriedChapterRune); got != 2 || retried != 1 {
t.Fatalf("fixture did not reach the retry refusal: provider saw %d call(s) (%d on the retried chapter); want 2 and 1", got, retried)
}
committed, reserved, serr := r.Store.SpentUSD("test-book")
if serr != nil {
t.Fatal(serr)
}
rows, rerr := r.Store.ChunkStatusesForBook("test-book")
if rerr != nil {
t.Fatal(rerr)
}
row := draftRowOf(t, r, rows, manifest[1])
// The control величина beside the claim: the OTHER chapter's row proves the query and the store are
// answering about an existing subject, so a missing row below is a missing row and not a blind read.
clean := draftRowOf(t, r, rows, manifest[0])
t.Logf("after the stop: committed=$%.6f reserved=$%.6f · chunk_status rows=%d · row for the clean unit=%v · row for the retried unit=%v",
committed, reserved, len(rows), clean != nil, row != nil)
if committed <= 0 {
t.Fatalf("the fixture bought nothing, so there is no paid position to mark: committed=%.6f", committed)
}
if clean == nil {
t.Fatal("the clean unit has no row either — the read is blind and the assertion below would be vacuous")
}
if row == nil {
t.Fatalf("THE HOLE: the book paid $%.6f and the unit whose retry was refused has NO chunk_status row, so every reading model sees it as `pending` — not translated yet", committed)
}
if got, want := FlagReason(row.FlagReason), FlagRetryUnaffordable; got != want {
t.Fatalf("the mark must name the money as the cause, got disposition=%q reason=%q (want %q): a reason that says `length` sends a person to fix a budget formula instead of topping up",
row.Disposition, got, want)
}
if row.Disposition != string(DispFlagged) {
t.Fatalf("the mark's disposition is %q, want %q", row.Disposition, DispFlagged)
}
if row.FinalHash != "" {
t.Fatalf("the mark must not point at an export: final_hash=%q on a position that never produced shippable text", row.FinalHash)
}
if row.CostUSD <= 0 {
t.Fatalf("the mark reports cost_usd=%.6f for a position whose attempt 0 was paid for — the row would say the money went nowhere", row.CostUSD)
}
// The row has to carry what the WARN line cannot: the log dies with the process, and the row is what
// a person reads afterwards. The primary failure is the one fact FlagReason no longer states.
if !strings.Contains(row.Detail, string(FlagLength)) {
t.Fatalf("the mark's detail does not say what the paid attempt answered: %q", row.Detail)
}
if row.Attempts != 1 {
t.Fatalf("the mark counts %d attempt(s) for one paid call: a refused reservation is not an attempt that happened", row.Attempts)
}
}
// TestTheRefusedRetryStopCarriesEnoughToTopUpWith is pin 2, and it asserts the NUMBER rather than the
// presence of the frame: «an event arrived» is true of a shortfall of zero, which tells a buyer nothing.
// The sufficiency half is proved by execution in the resume test below — topping up by exactly this
// figure is what admits the same call.
func TestTheRefusedRetryStopCarriesEnoughToTopUpWith(t *testing.T) {
prov := newLengthOncePerChapter(t)
bookPath, manifest := retryStopFixture(t, prov.srv.URL)
ctx := obs.WithReqInfo(context.Background(), obs.ReqInfo{TraceID: obs.NewTraceID()})
r := newRunner(t, bookPath)
defer r.Close()
ceiling, est0, est1 := retryStopCeiling(t, r, manifest)
r.CeilingUSD = ceiling
_, err := r.TranslateBook(ctx)
var halt *CeilingHalt
if !errors.As(err, &halt) {
t.Fatalf("want a ceiling halt, got %v", err)
}
committed, reserved, serr := r.Store.SpentUSD("test-book")
if serr != nil {
t.Fatal(serr)
}
t.Logf("stop frame: scope=%q shortfall=%d micro-USD · committed=$%.6f reserved=$%.6f ceiling=$%.6f (est0=$%.6f est1=$%.6f)",
halt.Scope, halt.ShortfallMicroUSD, committed, reserved, ceiling, est0, est1)
if halt.Scope != "book" {
t.Fatalf("the shortfall is stated for the BOOK scope only (the day ceiling sums every book in the store), and this stop says scope=%q", halt.Scope)
}
if halt.ShortfallMicroUSD <= 0 {
t.Fatal("the stop states no shortfall: the buyer is told the run paused and not by how much it is short, which is the whole of the metadata this stop is supposed to carry")
}
// The figure must be the distance to the ceiling rather than the price of the call — the one money
// number allowed to leave (D39.203). Reserved is zero here (nothing is in flight at a sequential
// refusal), so the arithmetic is exact and can be asserted rather than bounded.
want := int64((committed + est1 - ceiling) * 1e6)
if halt.ShortfallMicroUSD < want || halt.ShortfallMicroUSD > want+1 {
t.Fatalf("shortfall %d micro-USD, want %d (committed %.6f + denied estimate %.6f ceiling %.6f, rounded up)",
halt.ShortfallMicroUSD, want, committed, est1, ceiling)
}
}
// TestToppingUpByTheStatedShortfallFinishesTheUnit is pin 3 — the three-run sequence, and the ONE pin of
// this file that is green both before and after this pack. It is a GUARD, not a proof: the resume path it
// protects is ratified (D4 — the book pauses durably and a raised ceiling resumes it), and this pack's own
// change (a mark the resume must NOT read as an answer) is exactly the kind of edit that can break it
// silently. Stated here so nobody reads its green as evidence the hole was closed; pin 1 above is what
// reds on the defect.
func TestToppingUpByTheStatedShortfallFinishesTheUnit(t *testing.T) {
prov := newLengthOncePerChapter(t)
bookPath, manifest := retryStopFixture(t, prov.srv.URL)
ctx := obs.WithReqInfo(context.Background(), obs.ReqInfo{TraceID: obs.NewTraceID()})
// RUN 1 — stops on the refused retry.
r1 := newRunner(t, bookPath)
ceiling, _, _ := retryStopCeiling(t, r1, manifest)
r1.CeilingUSD = ceiling
_, err := r1.TranslateBook(ctx)
var halt *CeilingHalt
if !errors.As(err, &halt) {
r1.Close()
t.Fatalf("run 1 must stop on the ceiling, got %v", err)
}
shortfall := halt.ShortfallMicroUSD
callsAfterRun1 := prov.total()
r1.Close()
// RUN 2 — the same ceiling. The position is re-attacked (its mark is not an answer), attempt 0 is
// replayed from its checkpoint for $0, and the retry is refused again: no fresh paid call, no endless
// loop, the same stop.
r2 := newRunner(t, bookPath)
r2.CeilingUSD = ceiling
_, err = r2.TranslateBook(ctx)
var halt2 *CeilingHalt
if !errors.As(err, &halt2) {
r2.Close()
t.Fatalf("run 2 under the same ceiling must stop the same way, got %v", err)
}
if got := prov.total(); got != callsAfterRun1 {
r2.Close()
t.Fatalf("the resume bought %d fresh call(s) under a ceiling that has not moved (was %d, now %d): a position whose money ran out must replay, not re-buy",
got-callsAfterRun1, callsAfterRun1, got)
}
// ⛔ AND THE RE-WRITTEN MARK MUST STILL CARRY THE MONEY. The resume's fresh spend is zero — attempt 0
// came from its checkpoint — so a mark built from «what this run paid» would overwrite the row with
// cost_usd=0 and put chunk_status below SUM(checkpoints) for the position, which is the ledger
// invariant the projection a person decides on is read through.
rows2, r2err := r2.Store.ChunkStatusesForBook("test-book")
if r2err != nil {
r2.Close()
t.Fatal(r2err)
}
row2 := draftRowOf(t, r2, rows2, manifest[1])
if row2 == nil || FlagReason(row2.FlagReason) != FlagRetryUnaffordable || row2.CostUSD <= 0 {
r2.Close()
t.Fatalf("after a resume that bought nothing the mark reads %+v: the row must still name the money reason AND the money already spent on this position", row2)
}
t.Logf("after the resume under the same ceiling: fresh provider calls=%d · the mark still reads reason=%q cost_usd=%.6f attempts=%d",
prov.total()-callsAfterRun1, row2.FlagReason, row2.CostUSD, row2.Attempts)
r2.Close()
// RUN 3 — the ceiling raised by EXACTLY the shortfall the stop stated. This is the sufficiency half
// of pin 2: not «some bigger number works», but «the number the buyer was given works».
raised := ceiling + float64(shortfall)/1e6
r3 := newRunner(t, bookPath)
defer r3.Close()
r3.CeilingUSD = raised
res, err := r3.TranslateBook(ctx)
if err != nil {
t.Fatalf("topping up by the stated shortfall ($%.6f → $%.6f, +%d micro-USD) did not let the run finish: %v", ceiling, raised, shortfall, err)
}
rows, rerr := r3.Store.ChunkStatusesForBook("test-book")
if rerr != nil {
t.Fatal(rerr)
}
row := draftRowOf(t, r3, rows, manifest[1])
committed, _, _ := r3.Store.SpentUSD("test-book")
t.Logf("after the top-up: provider calls=%d (was %d) · committed=$%.6f · row=%+v", prov.total(), callsAfterRun1, committed, row)
if row == nil || row.Disposition != string(DispOK) {
t.Fatalf("the unit was not finished by the resume: row=%+v", row)
}
if got := prov.calls(retriedChapterRune); got != 2 {
t.Fatalf("the retried chapter saw %d call(s), want 2 — attempt 0 replayed for $0 and the retry bought once", got)
}
if res.Flagged != 0 {
t.Fatalf("the finished run still reports %d flagged unit(s)", res.Flagged)
}
// The delivered text must be the RETRY's, not attempt 0's: that is what «the resume finished the
// unit» means, and a checkpoint-served answer would look identical in the counters alone.
found := false
for _, c := range res.Chunks {
if c.Chapter == manifest[1].Chapter && c.ChunkIdx == manifest[1].ChunkIdx {
found = true
if !strings.Contains(c.FinalText, draftAfterRetry) {
t.Fatalf("the finished unit ships %.40q — not the retry's text", c.FinalText)
}
}
}
if !found {
t.Fatalf("the finished run carries no result for ch%d/chunk%d", manifest[1].Chapter, manifest[1].ChunkIdx)
}
}
// TestStopMarkForAsksWhetherAnythingWasEverBought is the DIRECT table over the decision, and it exists
// because the end-to-end tests above cannot reach two of its rows at all: a position that paid for a
// BURNED key and never classified a reply is left behind by a stopped run, not by a ceiling, and arranging
// one through the provider is a race where the rule is a predicate.
//
// ⛔ The row that matters most is «money was spent, nothing was classified». It is the shape a position
// has after a stopped run burned attempt 0: runAttempt walks over burned keys, so the FIRST fresh purchase
// of that position happens at an index ≥ 1 with nothing in hand. A mark written there would be a verdict
// about a unit nobody has translated once — and since a flagged row is an answer a resume serves
// (resolvedForResume), the unit would be terminal, holding the burned money and no text. The predicate
// therefore asks `paidAttempts`, and the two wrong readings of it — the attempt INDEX and «money was
// spent» — are the two rows below that must answer «no mark».
func TestStopMarkForAsksWhetherAnythingWasEverBought(t *testing.T) {
ceiling := func(shortfall int64, scope string) error {
return &CeilingHalt{Scope: scope, ShortfallMicroUSD: shortfall,
err: fmt.Errorf("pipeline: book USD ceiling reached: %w", errReserveCeiling)}
}
stopped := fmt.Errorf("the run ended: %w", context.Canceled)
delivered := &llm.AttemptCutError{Provider: "fake", Cause: llm.CutByParent, Delivered: true}
paid := stoppedPosition{
attempts: 2, paidAttempts: 1, cumCostUSD: 0.00182,
inHand: stageAttempt{attempt: 0, cls: classification{FlagLength, "truncated at max_tokens"}},
}
burnedOnly := stoppedPosition{attempts: 2, paidAttempts: 0, cumCostUSD: 0.00182}
// A position whose FIRST attempt echoed: the echo re-roll is the purchase that was refused, and the
// echo is what the row must remember — its own reason will say only why the re-roll never happened.
echoed := stoppedPosition{
attempts: 2, paidAttempts: 1, cumCostUSD: 0.00182,
inHand: stageAttempt{attempt: 0, cls: classification{FlagCJKArtifact, "CJK share 97% in output (untranslated echo)"}},
firstFlagReason: FlagCJKArtifact,
}
// A burn at index 0, a classified attempt at 1, the refusal at 2: the shape a position has when a
// stopped run left money with no result behind it and the next run bought the answer itself.
afterABurn := stoppedPosition{
attempts: 3, paidAttempts: 1, cumCostUSD: 0.00364,
inHand: stageAttempt{attempt: 1, cls: classification{FlagEmpty, "empty completion (finish=stop)"}},
}
fresh := stoppedPosition{attempts: 1, paidAttempts: 0}
for _, tc := range []struct {
name string
p stoppedPosition
err error
want FlagReason
attempts int
// wantFirst is the superseded first failure the mark must carry. It is asserted because without it
// an echo the book PAID for leaves the echo metric's numerator while staying in its denominator —
// the rate falls on the run that bought the echo (echorecovered_test.go drives that end to end).
wantFirst string
says []string
omits []string
}{
{name: "a ceiling refused the re-attack of a paid attempt", p: paid, err: ceiling(3710, "book"),
want: FlagRetryUnaffordable, attempts: 1,
says: []string{"attempt 0", string(FlagLength), "book", "3710 micro-USD", "raise the ceiling"}},
{name: "the day ceiling states no shortfall", p: paid, err: ceiling(0, "day"),
want: FlagRetryUnaffordable, attempts: 1,
says: []string{"attempt 0", "day"},
omits: []string{"short by"}},
{name: "a re-attack refused at a position that had burned a key first", p: afterABurn, err: ceiling(3710, "book"),
want: FlagRetryUnaffordable, attempts: 2,
says: []string{"attempt 1", string(FlagEmpty)}},
{name: "a re-attack refused over an echo the book had paid for", p: echoed, err: ceiling(3710, "book"),
want: FlagRetryUnaffordable, attempts: 1, wantFirst: string(FlagCJKArtifact),
says: []string{string(FlagCJKArtifact)}},
{name: "a human stopped a delivered call over a position that had already failed once", p: echoed,
err: fmt.Errorf("%w: %w", stopped, delivered),
want: FlagCancelled, attempts: 2, wantFirst: string(FlagCJKArtifact)},
{name: "a human stopped a delivered call", p: paid, err: fmt.Errorf("%w: %w", stopped, delivered),
want: FlagCancelled, attempts: 2, says: []string{"in flight", "the same budget"}},
{name: "a ceiling refused a position that only ever paid for a burned key", p: burnedOnly, err: ceiling(3710, "book")},
{name: "a ceiling refused a fresh attempt 0", p: fresh, err: ceiling(3710, "book")},
{name: "a stop with no delivered call", p: paid, err: stopped},
{name: "an ordinary infra failure", p: paid, err: errors.New("the store is gone")},
} {
t.Run(tc.name, func(t *testing.T) {
mark, marked := stopMarkFor(tc.p, tc.err)
if tc.want == "" {
if marked {
t.Fatalf("a mark was written where the position has nothing to report: %+v", mark)
}
return
}
if !marked {
t.Fatalf("no mark: the position paid $%.6f and the stop would leave it reading as never started", tc.p.cumCostUSD)
}
if mark.reason != tc.want {
t.Fatalf("reason %q, want %q", mark.reason, tc.want)
}
if mark.attempts != tc.attempts {
t.Fatalf("the mark counts %d attempt(s), want %d: a refused reservation is not an attempt that happened", mark.attempts, tc.attempts)
}
if mark.firstFlag != tc.wantFirst {
t.Fatalf("the mark carries first_flag_reason=%q, want %q: the column is how a failure the book PAID for stays countable once the row's own reason says something else",
mark.firstFlag, tc.wantFirst)
}
for _, want := range tc.says {
if !strings.Contains(mark.detail, want) {
t.Fatalf("the detail a person reads afterwards does not say %q: %s", want, mark.detail)
}
}
for _, never := range tc.omits {
if strings.Contains(mark.detail, never) {
t.Fatalf("the detail says %q about a stop that stated no figure — a money number of zero reads as «you are not short of anything»: %s", never, mark.detail)
}
}
})
}
}
// TestACeilingRefusingAFreshAttemptLeavesNoRow is the other side of the predicate, asserted through a run:
// a position that bought NOTHING must stay `pending`, because pending is then the TRUTH. A mark there
// would invent a half-done unit out of a unit nobody started, and it is exactly what the wrong predicate
// (the attempt index, or «money moved in this run») produces.
func TestACeilingRefusingAFreshAttemptLeavesNoRow(t *testing.T) {
prov := newLengthOncePerChapter(t)
bookPath, manifest := retryStopFixture(t, prov.srv.URL)
ctx := obs.WithReqInfo(context.Background(), obs.ReqInfo{TraceID: obs.NewTraceID()})
r := newRunner(t, bookPath)
defer r.Close()
// A ceiling that admits nothing at all: the very first attempt 0 is refused.
r.CeilingUSD = 0.0000001
_, err := r.TranslateBook(ctx)
var halt *CeilingHalt
if !errors.As(err, &halt) {
t.Fatalf("want a ceiling halt, got %v", err)
}
if got := prov.total(); got != 0 {
t.Fatalf("premise broken: the provider saw %d call(s), so something WAS bought and this is no longer the unbought case", got)
}
committed, reserved, serr := r.Store.SpentUSD("test-book")
if serr != nil {
t.Fatal(serr)
}
rows, rerr := r.Store.ChunkStatusesForBook("test-book")
if rerr != nil {
t.Fatal(rerr)
}
t.Logf("after a stop that bought nothing: committed=$%.6f reserved=$%.6f · chunk_status rows=%d (units in the manifest: %d)",
committed, reserved, len(rows), len(manifest))
if committed != 0 {
t.Fatalf("the fixture paid $%.6f — this test is about a position that bought nothing", committed)
}
if len(rows) != 0 {
t.Fatalf("a stop that bought nothing wrote %d chunk_status row(s): %+v — a unit nobody translated must read `pending`, which is the truth, and a flag would invent a half-done unit the resume is free to re-buy", len(rows), rows)
}
// ⛔ THE CONTROL FOR A ZERO, THROUGH THE SAME QUERY. «No row for this position» and «this read cannot
// see rows at all» are the same output, and the manifest count printed above comes from a DIFFERENT
// instrument (bookChunks). So the ceiling is raised and the book run: the same call must now answer
// with rows, which is what makes the zero above a fact about the position rather than about the read.
r.CeilingUSD = 0
if _, err := r.TranslateBook(ctx); err != nil {
t.Fatalf("the control run under the book's own ceiling must finish: %v", err)
}
after, aerr := r.Store.ChunkStatusesForBook("test-book")
if aerr != nil {
t.Fatal(aerr)
}
t.Logf("control: the same query, after a run that was allowed to buy — chunk_status rows=%d (provider calls=%d)", len(after), prov.total())
if len(after) != len(manifest) {
t.Fatalf("the control read %d row(s) for %d unit(s): the zero above may have been this query's answer to everything", len(after), len(manifest))
}
}
// TestARedriveDoesNotBuyThePaidAttemptAgain is a MONEY pin, and the defect it closes was created by this
// very pack: before the mark existed, a position whose re-attack a ceiling refused had NO row, so a
// redrive could not see it. A flagged row is a redrive target by default — and ResetChunkStages DELETES
// the position's checkpoints, including the one holding the ALREADY PAID text of attempt 0. The operator's
// natural gesture after a run full of flags («tmctl redrive», no selector) would then buy that attempt
// again, while the resume needed only the re-attack.
//
// The control is in the same store and through the same query: flip the row's reason to a real verdict and
// the target appears. Without it «no targets» would pass on a selector that matches nothing at all.
func TestARedriveDoesNotBuyThePaidAttemptAgain(t *testing.T) {
prov := newLengthOncePerChapter(t)
bookPath, manifest := retryStopFixture(t, prov.srv.URL)
ctx := obs.WithReqInfo(context.Background(), obs.ReqInfo{TraceID: obs.NewTraceID()})
r1 := newRunner(t, bookPath)
ceiling, _, _ := retryStopCeiling(t, r1, manifest)
r1.CeilingUSD = ceiling
_, err := r1.TranslateBook(ctx)
var halt *CeilingHalt
if !errors.As(err, &halt) {
r1.Close()
t.Fatalf("the run must stop on the ceiling, got %v", err)
}
r1.Close()
r2 := newRunner(t, bookPath)
defer r2.Close()
// The operator's default gesture: every flagged position, whatever the reason.
sum, _, rerr := r2.Redrive(ctx, RedriveSelector{Chapter: -1, ChunkIdx: -1, DryRun: true})
if rerr != nil {
t.Fatal(rerr)
}
st, serr := r2.Status(ctx)
if serr != nil {
t.Fatal(serr)
}
t.Logf("after the stop: redrive targets=%d · status says done=%d flagged=%d in_progress=%d pending=%d",
len(sum.Targets), st.Chapters[0].UnitsDone, st.Chapters[0].UnitsFlagged, st.Chapters[0].UnitsInProgress, st.Chapters[0].UnitsPending)
if len(sum.Targets) != 0 {
t.Fatalf("the redrive targets %+v: its reset DELETES the checkpoints of those positions, and the one behind this mark holds the paid text of attempt 0 — the resume replays it for $0 and buys only the re-attack", sum.Targets)
}
// Asking for the reason BY NAME does not change the answer: the position is not a verdict, and the
// cheap remedy (a raised ceiling and a resume) is the only one that finishes it.
byName, _, nerr := r2.Redrive(ctx, RedriveSelector{Chapter: -1, ChunkIdx: -1, Reason: string(FlagRetryUnaffordable), DryRun: true})
if nerr != nil {
t.Fatal(nerr)
}
if len(byName.Targets) != 0 {
t.Fatalf("named explicitly, the mark became a target again: %+v", byName.Targets)
}
// THE CONTROL, on the same store and through the same query: the position carries a real verdict, and
// the redrive must find it. A «no targets» that held whatever the data said would be vacuous.
row, gerr := r2.Store.GetChunkStatus("test-book", manifest[1].Chapter, manifest[1].ChunkIdx, "draft")
if gerr != nil || row == nil {
t.Fatalf("the marked row is gone: %v %v", row, gerr)
}
verdict := *row
verdict.FlagReason = string(FlagHardRefusal)
if uerr := r2.Store.UpsertChunkStatus(verdict); uerr != nil {
t.Fatal(uerr)
}
ctrl, _, cerr := r2.Redrive(ctx, RedriveSelector{Chapter: -1, ChunkIdx: -1, DryRun: true})
if cerr != nil {
t.Fatal(cerr)
}
t.Logf("control: with the SAME row carrying a real verdict, redrive targets=%d", len(ctrl.Targets))
if len(ctrl.Targets) != 1 {
t.Fatalf("the control found %d target(s) for a genuinely flagged position: the assertion above was about a selector that matches nothing", len(ctrl.Targets))
}
}
// TestAMarkedUnitIsCarriedAndNotChargedAGrantSlot is a MONEY pin over the volume classifier, and the
// defect it closes was created by this pack: the mark gave the position a row, the completeness test
// counted rows rather than ANSWERS, and a unit an earlier run had already paid to start was classified
// `rework` — which TAKES a grant slot (granted()) and is queued behind fresh book. The `--max-units` help
// text promises the opposite in so many words: «a unit an EARLIER run started and never shipped is
// finished outside the grant, so the same unit is never charged a slot twice».
func TestAMarkedUnitIsCarriedAndNotChargedAGrantSlot(t *testing.T) {
prov := newLengthOncePerChapter(t)
bookPath, manifest := retryStopFixture(t, prov.srv.URL)
ctx := obs.WithReqInfo(context.Background(), obs.ReqInfo{TraceID: obs.NewTraceID()})
r1 := newRunner(t, bookPath)
ceiling, _, _ := retryStopCeiling(t, r1, manifest)
r1.CeilingUSD = ceiling
_, err := r1.TranslateBook(ctx)
var halt *CeilingHalt
if !errors.As(err, &halt) {
r1.Close()
t.Fatalf("the run must stop on the ceiling, got %v", err)
}
r1.Close()
r := newRunner(t, bookPath)
defer r.Close()
chunks, cerr := r.bookChunks()
if cerr != nil {
t.Fatal(cerr)
}
statuses, serr := r.Store.ChunkStatusesForBook("test-book")
if serr != nil {
t.Fatal(serr)
}
units := r.outputUnits(chunks)
class, _, kerr := r.classifyUnits(units, chunks, statuses, nil)
if kerr != nil {
t.Fatal(kerr)
}
marked := class[chunkKey{manifest[1].Chapter, manifest[1].ChunkIdx}]
clean := class[chunkKey{manifest[0].Chapter, manifest[0].ChunkIdx}]
t.Logf("volume classes: the marked unit=%d, the delivered one=%d (fresh=%d free=%d rework=%d carried=%d)",
marked, clean, unitFresh, unitFree, unitRework, unitCarried)
if marked != unitCarried {
t.Fatalf("the marked unit classifies %d, want carried (%d): `rework` charges this run's grant for a unit an earlier run already paid to start, and `fresh` would call it new book",
marked, unitCarried)
}
// The control, through the same call: the unit that really was delivered is FREE — so «carried» above
// is a statement about this unit and not what the classifier answers for everything.
if clean != unitFree {
t.Fatalf("the delivered unit classifies %d, want free (%d) — the classifier is answering something else entirely", clean, unitFree)
}
}
// TestTheWaveCountersDoNotTellTheBuyerTheBookIsDone is the MONEY WIRE pin, and it is the widest defect
// this pack created: the wave counter counted a unit as resolved once its rows EXISTED, so the mark made
// it full. Measured before the fix on this fixture — run 1 stopped with one unit marked and published
// `money{units_resolved:1,units_deferred:1}`; the resume under the SAME ceiling, which bought nothing and
// left the same hole, published `draft{done:2,total:2}` and `money{units_resolved:2,units_deferred:0}`.
// A buyer deciding whether to top up was being told the book owed nothing.
func TestTheWaveCountersDoNotTellTheBuyerTheBookIsDone(t *testing.T) {
prov := newLengthOncePerChapter(t)
bookPath, manifest := retryStopFixture(t, prov.srv.URL)
ceiling := 0.0
for run := 1; run <= 2; run++ {
r := newRunner(t, bookPath)
if run == 1 {
ceiling, _, _ = retryStopCeiling(t, r, manifest)
}
r.CeilingUSD = ceiling
_, err := r.TranslateBook(obs.WithReqInfo(context.Background(), obs.ReqInfo{TraceID: obs.NewTraceID()}))
var halt *CeilingHalt
if !errors.As(err, &halt) {
r.Close()
t.Fatalf("run %d must stop on the ceiling, got %v", run, err)
}
r.Close()
}
// Read the journal as BYTES, the way the platform tails it — not by asking the engine's structs.
raw, rerr := os.ReadFile(filepath.Join(filepath.Dir(bookPath), runevents.JournalFile))
if rerr != nil {
t.Fatal(rerr)
}
type money struct {
UnitsResolved int `json:"units_resolved"`
UnitsDeferred int `json:"units_deferred"`
}
var ledgers []money
var progress []string
for _, l := range strings.Split(strings.TrimSpace(string(raw)), "\n") {
if l == "" {
continue
}
var env struct {
Type string `json:"type"`
Data json.RawMessage `json:"data"`
}
if err := json.Unmarshal([]byte(l), &env); err != nil {
t.Fatalf("the stream is not readable NDJSON: %v (%s)", err, l)
}
switch env.Type {
case string(runevents.TypeFinished):
var f struct {
Outcome string `json:"outcome"`
Money *money `json:"money"`
}
if err := json.Unmarshal(env.Data, &f); err != nil {
t.Fatal(err)
}
if f.Money != nil {
ledgers = append(ledgers, *f.Money)
}
case string(runevents.TypeProgress):
progress = append(progress, string(env.Data))
}
}
t.Logf("money ledgers published, in order: %+v", ledgers)
t.Logf("progress frames: %s", strings.Join(progress, " · "))
if len(ledgers) != 2 {
t.Fatalf("want one money ledger per stopped run, got %d — the fixture did not publish what this test reads", len(ledgers))
}
for i, l := range ledgers {
if l.UnitsResolved != 1 || l.UnitsDeferred != 1 {
t.Fatalf("run %d published money{resolved:%d deferred:%d}, want 1 and 1: the unit whose re-attack was refused is NOT resolved — nothing bought it, and telling a buyer the book owes %d units is the number they decide a top-up on",
i+1, l.UnitsResolved, l.UnitsDeferred, l.UnitsDeferred)
}
}
// And the resync channel must agree with the stream: one wave-resolved unit of two.
r := newRunner(t, bookPath)
defer r.Close()
st, serr := r.Status(context.Background())
if serr != nil {
t.Fatal(serr)
}
t.Logf("status says draft=%+v · chapter done=%d flagged=%d in_progress=%d",
st.Progress.Draft, st.Chapters[0].UnitsDone, st.Chapters[0].UnitsFlagged, st.Chapters[0].UnitsInProgress)
if st.Progress.Draft.Done != 1 || st.Progress.Draft.Total != 2 {
t.Fatalf("the resync channel says the draft wave finished %d of %d units: the platform folds this and the stream into ONE column, so they must not disagree",
st.Progress.Draft.Done, st.Progress.Draft.Total)
}
}