package pipeline import ( "bytes" "context" "encoding/json" "errors" "fmt" "io" "log/slog" "net/http" "net/http/httptest" "path/filepath" "reflect" "strings" "sync" "testing" "textmachine/backend/internal/chunk/chunktest" "textmachine/backend/internal/membank" "textmachine/backend/internal/obs" "textmachine/backend/internal/store" ) // writeFakeCompletion emits the same completion body newJSONProvider does, for the tests that need to // control the HTTP status per request and therefore cannot use that helper. func writeFakeCompletion(w http.ResponseWriter, text, finish string) { 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) } // volumeBook builds an n-chapter epub. With the default two-stage pipeline each chapter is one draft // chunk and therefore exactly one OUTPUT UNIT, so "units" and "chapters" coincide and the assertions // below read as the purchase does. func volumeBook(t *testing.T, providerURL string, n int) string { t.Helper() var eps []chunktest.Chapter var spine []string for i := 1; i <= n; i++ { id := fmt.Sprintf("c%d", i) eps = append(eps, chunktest.Chapter{ID: id, Href: id + ".xhtml", Body: fmt.Sprintf("
静かな図書館の朝%d。
", i)}) spine = append(spine, id) } // waveWorkers > 1 on purpose: the admission must hold under the parallel fan-out, not only in a // sequential walk. A design that counted slots inside the workers would be racy exactly here. return setupProjectOpts(t, providerURL, projectOpts{epub: eps, spine: spine, regenerate: 1, waveWorkers: 4}) } // chaptersWithRows reports which chapters left any chunk_status row behind — i.e. which units the run // actually STARTED. It is the instrument for "stopped before N+1 rather than after it": a unit that began // and was cut off would still have written a row. func chaptersWithRows(t *testing.T, s *store.Store, bookID string) map[int]bool { t.Helper() rows, err := s.ChunkStatusesForBook(bookID) if err != nil { t.Fatal(err) } out := map[int]bool{} for _, cs := range rows { out[cs.Chapter] = true } return out } // TestTheVolumeCeilingStopsAtWhatWasBought is the §4.2 post-hoc invariant, and all three of its clauses // are asserted separately because each can fail on its own: the run stops AT N, it says it stopped on // VOLUME (in a way a money stop could not be confused with), and it stops BEFORE beginning N+1 rather // than after paying for it. // // The third clause is the one that matters most and the one external systems get wrong: LiteLLM's // post-hoc token check always overshoots its ceiling by one call, because it looks after the call rather // than before it. Here the evidence is direct — unit N+1 has no chunk_status row at all, and the provider // call count is exactly what N units cost and not one call more. func TestTheVolumeCeilingStopsAtWhatWasBought(t *testing.T) { const chapters, granted = 5, 2 rec := &reqRec{} srv := newJSONProvider(rec, draftEdit) defer srv.Close() bookPath := volumeBook(t, srv.URL, chapters) ctx := obs.WithReqInfo(context.Background(), obs.ReqInfo{TraceID: obs.NewTraceID()}) r := newRunner(t, bookPath) defer r.Close() r.MaxUnits = granted res, err := r.TranslateBook(ctx) if err != nil { t.Fatalf("a volume stop must not be an error — it is a completion: %v", err) } // (1) It stopped AT N. if len(res.Chunks) != granted { t.Fatalf("the run shipped %d unit(s), want exactly the %d that were granted", len(res.Chunks), granted) } if res.ExitCode() != 0 { t.Fatalf("exit code %d: a volume stop is a COMPLETION, and the frozen exit dictionary must not gain a word", res.ExitCode()) } // (2) It NAMES the ceiling, distinguishably. if res.Volume == nil { t.Fatalf("the run stopped short and said nothing about why — the silent-limit defect this pack exists to avoid") } if res.Volume.MaxUnits != granted || res.Volume.Delivered != granted || res.Volume.LeftFresh != chapters-granted { t.Fatalf("the stop misreports itself: %+v (want max=%d delivered=%d left_fresh=%d)", *res.Volume, granted, granted, chapters-granted) } // Nothing had ever been delivered, so nothing can be REWORK — and saying so is the point of the // split: a purchase that delivers nothing new must not be reportable as though it did. if res.Volume.Reworked != 0 || res.Volume.LeftRework != 0 { t.Fatalf("a book nothing has run has nothing to re-make: %+v", *res.Volume) } if res.Volume.Free != 0 { t.Fatalf("nothing had been run before, so nothing could ride free; got %d", res.Volume.Free) } // (3) It stopped BEFORE beginning N+1, not after. started := chaptersWithRows(t, r.Store, "test-book") for ch := 1; ch <= granted; ch++ { if !started[ch] { t.Fatalf("chapter %d was granted but never ran: %v", ch, started) } } for ch := granted + 1; ch <= chapters; ch++ { if started[ch] { t.Fatalf("chapter %d is past the ceiling and still left a row — the ceiling was checked AFTER the unit began, which is the overshoot-by-one this design exists to prevent", ch) } } // The direct money evidence: two stages per unit, so N units cost exactly 2N calls. One more would // mean a unit past the ceiling reached a provider. if want := granted * 2; rec.count() != want { t.Fatalf("%d provider call(s) for %d granted unit(s); want exactly %d (draft+edit each) — anything more is work past the ceiling", rec.count(), granted, want) } } // TestAVolumeStopIsNotAMoneyStop pins the distinction the two ceilings owe the operator. They are // different answers with different remedies: money means the run was cut short mid-work and is resumable // once there is more of it; volume means the run did precisely what was bought. Reporting them alike // would either strand a paid-for run as "paused" or show a satisfied buyer a service error. func TestAVolumeStopIsNotAMoneyStop(t *testing.T) { rec := &reqRec{} srv := newJSONProvider(rec, draftEdit) defer srv.Close() ctx := obs.WithReqInfo(context.Background(), obs.ReqInfo{TraceID: obs.NewTraceID()}) // The MONEY ceiling: an amount too small for the book's first call. moneyBook := volumeBook(t, srv.URL, 3) rm := newRunner(t, moneyBook) rm.CeilingUSD = 0.0000001 _, moneyErr := rm.TranslateBook(ctx) rm.Close() var halt *CeilingHalt if !errors.As(moneyErr, &halt) { t.Fatalf("a money ceiling must still halt through its own typed sentinel, got %v", moneyErr) } // The VOLUME ceiling on an equivalent book: no error at all, and a report that says which ceiling. volBook := volumeBook(t, srv.URL, 3) rv := newRunner(t, volBook) defer rv.Close() rv.MaxUnits = 1 res, err := rv.TranslateBook(ctx) if err != nil { t.Fatalf("a volume stop must not travel as an error: %v", err) } if res.Volume == nil { t.Fatalf("the volume stop lost its name") } // And the money ceiling stayed out of it: nothing about the volume stop touched the ledger's semantics. if rv.CeilingUSD != 0 { t.Fatalf("the volume ceiling must not set a money ceiling; CeilingUSD = %v", rv.CeilingUSD) } } // TestAResumeDoesNotSpendItsVolumeOnFreeUnits is the repin/resume half of the counting rule, executed. // A second purchase of two units on a book whose first two are already done must deliver units THREE and // FOUR — not re-walk the first two and deliver nothing. If free work counted, the buyer would pay again // for what they already own, which is the whole defect the ceiling is meant to close. func TestAResumeDoesNotSpendItsVolumeOnFreeUnits(t *testing.T) { const chapters = 5 rec := &reqRec{} srv := newJSONProvider(rec, draftEdit) defer srv.Close() bookPath := volumeBook(t, srv.URL, chapters) ctx := obs.WithReqInfo(context.Background(), obs.ReqInfo{TraceID: obs.NewTraceID()}) r1 := newRunner(t, bookPath) r1.MaxUnits = 2 res1, err := r1.TranslateBook(ctx) if err != nil { t.Fatal(err) } r1.Close() if len(res1.Chunks) != 2 { t.Fatalf("first purchase: want 2 units, got %d", len(res1.Chunks)) } callsAfterFirst := rec.count() r2 := newRunner(t, bookPath) defer r2.Close() r2.MaxUnits = 2 res2, err := r2.TranslateBook(ctx) if err != nil { t.Fatal(err) } if res2.Volume == nil { t.Fatalf("the second purchase also stops on the ceiling and must say so") } // The two already-owned units rode along for free and did NOT consume the new grant. if res2.Volume.Free != 2 { t.Fatalf("%d unit(s) rode free, want the 2 already paid for — a $0 resume must not consume the ceiling", res2.Volume.Free) } if res2.Volume.Delivered != 2 || res2.Volume.Reworked != 0 { t.Fatalf("the second purchase must DELIVER 2 new units and re-make none: %+v", *res2.Volume) } if res2.Volume.LeftFresh != 1 || res2.Volume.LeftRework != 0 { t.Fatalf("1 of the 5 was never delivered and nothing is awaiting a re-make: %+v", *res2.Volume) } started := chaptersWithRows(t, r2.Store, "test-book") for ch := 1; ch <= 4; ch++ { if !started[ch] { t.Fatalf("chapter %d should exist after two purchases of two: %v", ch, started) } } if started[5] { t.Fatalf("chapter 5 was never bought and must not have run") } // The second run paid for exactly two units and replayed the first two for nothing. if fresh := rec.count() - callsAfterFirst; fresh != 4 { t.Fatalf("the second purchase made %d provider call(s), want 4 (two units × two stages) — the free units must cost none", fresh) } } // TestRetriesDoNotBurnTheVolumeCeiling is the other half of the counting rule. A unit that stutters and // is retried is still ONE unit of book. If a retry consumed the ceiling, the buyer of two chapters would // receive one because the engine's own quality tail — measured swinging between 1.5% and 23% of calls — // happened to land on their purchase. That tail is a fact about MONEY and is bounded by the money ceiling. func TestRetriesDoNotBurnTheVolumeCeiling(t *testing.T) { var mu sync.Mutex stuttered := false rec := &reqRec{} srv := newJSONProvider(rec, func(body string) (string, string) { if !isEditBody(body) { mu.Lock() first := !stuttered stuttered = true mu.Unlock() if first { // A truncated draft: `length` is in the retryable set, so runStage retries the same // chunk×stage with a bigger budget and still writes ONE row for it. return "ЧЕРНОВИК", "length" } } return draftEdit(body) }) defer srv.Close() bookPath := volumeBook(t, srv.URL, 4) ctx := obs.WithReqInfo(context.Background(), obs.ReqInfo{TraceID: obs.NewTraceID()}) r := newRunner(t, bookPath) defer r.Close() r.MaxUnits = 2 res, err := r.TranslateBook(ctx) if err != nil { t.Fatal(err) } if !stuttered { t.Fatalf("the fixture never produced a retry, so it proves nothing") } if len(res.Chunks) != 2 { t.Fatalf("a retried unit is still one unit: want 2 shipped, got %d", len(res.Chunks)) } if res.Volume == nil || res.Volume.Delivered != 2 { t.Fatalf("the retry must not have consumed a unit of the grant: %+v", res.Volume) } // The extra provider call is real and paid for — it is simply not a unit of BOOK. if rec.count() <= 4 { t.Fatalf("precondition: the retry should have cost an extra call, got %d", rec.count()) } } // TestNoCeilingChangesNothing pins the default. A run without --max-units must behave exactly as it did // before this existed — including not paying for the scope computation, which is why planVolume returns a // nil scope rather than an all-admitting one. func TestNoCeilingChangesNothing(t *testing.T) { rec := &reqRec{} srv := newJSONProvider(rec, draftEdit) defer srv.Close() bookPath := volumeBook(t, srv.URL, 3) ctx := obs.WithReqInfo(context.Background(), obs.ReqInfo{TraceID: obs.NewTraceID()}) r := newRunner(t, bookPath) defer r.Close() scope, err := r.planVolume(context.Background(), nil, nil) if err != nil || scope != nil { t.Fatalf("an unset ceiling must plan nothing at all, got scope=%v err=%v", scope, err) } res, err := r.TranslateBook(ctx) if err != nil { t.Fatal(err) } if len(res.Chunks) != 3 { t.Fatalf("an unbounded run must translate the whole book, got %d units", len(res.Chunks)) } if res.Volume != nil { t.Fatalf("an unbounded run must not claim it was cut short: %+v", res.Volume) } } // TestAGrantLargerThanTheBookIsAnOrdinaryCompletion pins the boundary the `bound()` guard exists for: a // run granted more than there is left has not been cut short, and must not report as though it had. func TestAGrantLargerThanTheBookIsAnOrdinaryCompletion(t *testing.T) { rec := &reqRec{} srv := newJSONProvider(rec, draftEdit) defer srv.Close() bookPath := volumeBook(t, srv.URL, 2) ctx := obs.WithReqInfo(context.Background(), obs.ReqInfo{TraceID: obs.NewTraceID()}) r := newRunner(t, bookPath) defer r.Close() r.MaxUnits = 50 res, err := r.TranslateBook(ctx) if err != nil { t.Fatal(err) } if len(res.Chunks) != 2 { t.Fatalf("the whole book should have run, got %d units", len(res.Chunks)) } if res.Volume != nil { t.Fatalf("a grant larger than the book is not a volume stop: %+v", res.Volume) } } // TestABoundedRunCannotOutrunTheConsentThreshold pins the correction to a defect this pack shipped: the // consent threshold must not be defeatable by splitting the run. // // The shipped version scoped the re-payment AMOUNT to the admitted units and left the THRESHOLD // book-wide. That reads as symmetrical and is not, because with a volume ceiling the CALLER chooses how // small each run is: `--resnapshot --max-units 1` in a loop re-pays the whole book while every single run // stays under the threshold, and consent is never asked once. That is exactly the silent re-purchase Р6 // exists to prevent, and "the change is inert without the flag" — the argument the narrowing was accepted // on, mine — is true and beside the point, since with the flag it opens the door the gate IS. // // The correction splits the two questions by what each one is ABOUT. "Must a human be asked at all" is // the book's policy and is judged against the book's whole drift, which a small purchase cannot shrink. // "--accept-rebill=X", by contrast, is the caller's instruction about SPEND, so it is measured against // what this run will actually be charged — the platform funds that cap from the run's own hold, and // measuring it against work the run will not do would refuse a caller whose money fully covers the bill. func TestABoundedRunCannotOutrunTheConsentThreshold(t *testing.T) { rec := &reqRec{} srv := newJSONProvider(rec, draftEdit) defer srv.Close() var eps []chunktest.Chapter var spine []string for i := 1; i <= 4; i++ { id := fmt.Sprintf("c%d", i) // 静か occurs in every chapter, so the signed term changes EVERY unit's injected bytes: all four // units genuinely re-pay rather than re-pin, which is what makes the arithmetic below discriminate. eps = append(eps, chunktest.Chapter{ID: id, Href: id + ".xhtml", Body: fmt.Sprintf("静かな図書館の朝%d。
", i)}) spine = append(spine, id) } // One unit re-pays one edit row ≈ $0.00182; four units ≈ $0.00728. The threshold sits between them, // which is precisely the gap a splitting caller would walk through. bookPath := setupProjectOpts(t, srv.URL, projectOpts{ epub: eps, spine: spine, regenerate: 1, waveWorkers: 4, rebillConsentUSD: 0.005, }) dir := filepath.Dir(bookPath) ctx := obs.WithReqInfo(context.Background(), obs.ReqInfo{TraceID: obs.NewTraceID()}) r1 := newRunner(t, bookPath) if _, err := r1.TranslateBook(ctx); err != nil { t.Fatalf("the baseline run must complete: %v", err) } r1.Close() doc := decisionsDocFor(t, dir, "test-book", membank.Decision{ Action: membank.ActionApprove, Src: "静か", Dst: "тихий"}) if _, err := ApplyBankDecisions(ctx, bookPath, doc, false); err != nil { t.Fatalf("bank-apply: %v", err) } // THE ATTACK: a purchase small enough that its own slice is far under the threshold. It must still be // stopped, because the BOOK's drift is over it. callsBefore := rec.count() rA := newRunner(t, bookPath) rA.Resnapshot = true rA.MaxUnits = 1 _, errA := rA.TranslateBook(ctx) rA.Close() if errA == nil { t.Fatalf("a run bounded to one unit walked past the consent gate; repeated four times it re-buys the whole book without ever asking") } if !strings.Contains(errA.Error(), "RE-PAY") { t.Fatalf("want the re-payment gate's refusal, got: %v", errA) } // The refusal has to show BOTH numbers, or it teaches the operator the wrong thing: the book's total // is what the threshold judged, and the run's slice is what they would actually be charged. if !strings.Contains(errA.Error(), "THIS run, bounded by --max-units") { t.Fatalf("the refusal must separate the book's drift from this run's slice, got: %v", errA) } if fresh := rec.count() - callsBefore; fresh != 0 { t.Fatalf("the gate refuses BEFORE any reservation; %d call(s) were made", fresh) } // AND THE OTHER DIRECTION, which is why the narrowing was attempted at all: a NAMED cap is an // instruction about spend, so it is measured against what THIS run re-pays (~$0.00182) and not against // the book's $0.00728. A caller whose money covers their own bill is not refused. rB := newRunner(t, bookPath) defer rB.Close() rB.Resnapshot = true rB.MaxUnits = 1 rB.AcceptRebill = RebillConsent{Given: true, Capped: true, CapUSD: 0.002} resB, errB := rB.TranslateBook(ctx) if errB != nil { t.Fatalf("a cap of $0.002 covers this run's own re-payment of ~$0.00182 and must be honoured; measuring it against the whole book would refuse a caller who can pay: %v", errB) } // ⚠ AND THE SPLIT EARNS ITSELF HERE. Every unit of this book was delivered by the baseline run, so // this purchase delivers NOTHING new — it re-makes one chapter the reader already has. Reported as a // single "granted" count it would have read exactly like a delivery of one chapter. if resB.Volume == nil || resB.Volume.Reworked != 1 || resB.Volume.Delivered != 0 { t.Fatalf("this purchase re-makes an already-delivered unit and delivers nothing new: %+v", resB.Volume) } if resB.Volume.LeftFresh != 0 || resB.Volume.LeftRework != 3 { t.Fatalf("nothing is left UNDELIVERED here; 3 delivered units await a re-make: %+v", resB.Volume) } if fresh := rec.count() - callsBefore; fresh != 1 { t.Fatalf("the consented run re-pays one unit = one edit call; it made %d", fresh) } } // TestTheVolumeCeilingHoldsOnADraftOnlyPipeline pins the ceiling on the OTHER pipeline shape. It matters // because the output unit is not the same object in the two: an editor pipeline groups draft chunks into // coarse edit units, while a draft-only pipeline ships one unit per draft chunk (outputUnits). A ceiling // that only held for the shape the fixtures happen to use would silently sell a different amount of book // on a deployment configured the other way. func TestTheVolumeCeilingHoldsOnADraftOnlyPipeline(t *testing.T) { rec := &reqRec{} srv := newJSONProvider(rec, draftEdit) defer srv.Close() var eps []chunktest.Chapter var spine []string for i := 1; i <= 4; i++ { id := fmt.Sprintf("c%d", i) eps = append(eps, chunktest.Chapter{ID: id, Href: id + ".xhtml", Body: fmt.Sprintf("静かな図書館の朝%d。
", i)}) spine = append(spine, id) } bookPath := setupProjectOpts(t, srv.URL, projectOpts{ epub: eps, spine: spine, regenerate: 1, waveWorkers: 4, draftOnly: true, }) ctx := obs.WithReqInfo(context.Background(), obs.ReqInfo{TraceID: obs.NewTraceID()}) r := newRunner(t, bookPath) defer r.Close() r.MaxUnits = 2 res, err := r.TranslateBook(ctx) if err != nil { t.Fatal(err) } if len(res.Chunks) != 2 { t.Fatalf("a draft-only run granted 2 units must ship 2, got %d", len(res.Chunks)) } if res.Volume == nil || res.Volume.Delivered != 2 || res.Volume.LeftFresh != 2 { t.Fatalf("the stop misreports itself on a draft-only pipeline: %+v", res.Volume) } // One stage per unit here, so two units cost exactly two calls — and nothing past the ceiling ran. if rec.count() != 2 { t.Fatalf("%d provider call(s); a draft-only unit is one call, so 2 granted units cost 2", rec.count()) } started := chaptersWithRows(t, r.Store, "test-book") if started[3] || started[4] { t.Fatalf("chapters past the ceiling must not have started: %v", started) } } // TestAnUnreproducibleHashIsNeverFree pins the DIRECTIONAL invariant volume.go calls load-bearing: // "every uncertainty resolves to «this unit will cost», never to «this unit is free»". The asymmetry is // the whole safety of the ceiling — judging a paying unit free lets the run spend PAST what was bought, // while the opposite merely stops a run one unit early. // // The dangerous branch is the one where the wire bytes cannot be REPRODUCED at all (an absent hash), // which is a different state from "reproduced and different". renderedContentHashes omits a position // whose inputs it cannot rebuild — a member's stored draft text gone, a chunk boundary moved by a // re-split — and an absent entry must read as "cannot conclude", i.e. paying. A condition written // `ok && h != cs.ContentHash` passes the whole battery while inverting exactly this case. func TestAnUnreproducibleHashIsNeverFree(t *testing.T) { r := &Runner{} draftNames := map[string]bool{"draft": true} editNames := map[string]bool{"edit": true} const snap = "SNAP" row := store.ChunkStatus{Chapter: 1, ChunkIdx: 0, Stage: "draft", Disposition: string(DispOK), ContentHash: "h1", SnapshotID: snap} rows := []store.ChunkStatus{row} present := map[chunkKey]map[string]string{{1, 0}: {"draft": "h1"}} // Baseline: reproduced AND equal, on the current snapshot — genuinely free. if !r.rowsResumeFree(rows, draftNames, editNames, snap, snap, present) { t.Fatalf("a row whose bytes reproduce identically on the current snapshot resumes for $0") } // Reproduced and DIFFERENT — pays. differs := map[chunkKey]map[string]string{{1, 0}: {"draft": "OTHER"}} if r.rowsResumeFree(rows, draftNames, editNames, snap, snap, differs) { t.Fatalf("the wire bytes moved; the unit will be called for and must not be judged free") } // NOT REPRODUCED AT ALL — the branch this test exists for. The stage key is missing. noStage := map[chunkKey]map[string]string{{1, 0}: {}} if r.rowsResumeFree(rows, draftNames, editNames, snap, snap, noStage) { t.Fatalf("the bytes could not be reproduced, so nothing is known — judging the unit FREE lets the run spend past the ceiling") } // NOT REPRODUCED AT ALL — the position itself is absent. if r.rowsResumeFree(rows, draftNames, editNames, snap, snap, map[chunkKey]map[string]string{}) { t.Fatalf("an absent position must read as «cannot conclude», i.e. paying") } // A `skipped` row never reached a provider and never will: it is re-derived from the flag above it. skipped := []store.ChunkStatus{{Chapter: 1, ChunkIdx: 0, Stage: "edit", Disposition: string(DispSkipped), ContentHash: "whatever", SnapshotID: "STALE"}} if !r.rowsResumeFree(skipped, draftNames, editNames, snap, snap, map[chunkKey]map[string]string{}) { t.Fatalf("a skipped stage costs nothing even with no hash and a stale snapshot") } // A stage this pipeline no longer runs cannot cost anything either. retired := []store.ChunkStatus{{Chapter: 1, ChunkIdx: 0, Stage: "gone", Disposition: string(DispOK), ContentHash: "x", SnapshotID: "STALE"}} if !r.rowsResumeFree(retired, draftNames, editNames, snap, snap, map[chunkKey]map[string]string{}) { t.Fatalf("a retired stage is never run, so it is never a cost") } } // TestTheCeilingAdmitsEveryMemberOfAUnit closes the coverage hole every other fixture in this package // leaves open: they all build one-sentence chapters, so every editUnit has exactly ONE member and the // per-member loop in planVolume's admit() is never exercised. On a real book a chapter is longer than one // draft chunk, and that is the shape volume.go's own comment describes — "an editor pipeline groups draft // chunks into coarse edit units". // // What the hole hides is not a cosmetic slip. Admission is asked TWICE with two different keys: the draft // wave asks per member chunk (scope.allows) and the edit wave asks per unit (scope.allowsUnit). If admit() // marked only the unit's leader, the non-leader members would never be drafted while the unit still read // as admitted — so the edit wave would run over a unit whose drafts are missing, and the buyer would // receive a truncated chapter they had paid for in full, silently. func TestTheCeilingAdmitsEveryMemberOfAUnit(t *testing.T) { rec := &reqRec{} srv := newJSONProvider(rec, draftEdit) defer srv.Close() // A fine DRAFT cut under a coarse EDIT ceiling is what produces multi-member units: each chapter // splits into several draft chunks that regroup into one edit unit. const seg = "\nsegmentation:\n draft_budget_out: 24\n edit_ceiling_out: 8000\n fertility: { cjk: 1.1978, other: 0.3852 }\n" body := "静かな図書館の朝。鈴木は本を読んだ。外では雨が降っていた。彼は窓を見た。時間は静かに過ぎた。
" var eps []chunktest.Chapter var spine []string for i := 1; i <= 3; i++ { id := fmt.Sprintf("c%d", i) eps = append(eps, chunktest.Chapter{ID: id, Href: id + ".xhtml", Body: body}) spine = append(spine, id) } bookPath := setupProjectOpts(t, srv.URL, projectOpts{ epub: eps, spine: spine, regenerate: 1, waveWorkers: 4, gatesYAML: seg, }) ctx := obs.WithReqInfo(context.Background(), obs.ReqInfo{TraceID: obs.NewTraceID()}) r := newRunner(t, bookPath) defer r.Close() r.MaxUnits = 1 res, err := r.TranslateBook(ctx) if err != nil { t.Fatal(err) } if res.Volume == nil || res.Volume.Delivered != 1 { t.Fatalf("one unit was granted and delivered: %+v", res.Volume) } // The fixture must actually produce a MULTI-member unit, or this test proves nothing about the loop. rows, err := r.Store.ChunkStatusesForBook("test-book") if err != nil { t.Fatal(err) } drafted := map[int]map[int]bool{} for _, cs := range rows { if cs.Stage != "draft" { continue } if drafted[cs.Chapter] == nil { drafted[cs.Chapter] = map[int]bool{} } drafted[cs.Chapter][cs.ChunkIdx] = true } // How many chunks the CUT produced for chapter 1 — read from the manifest, which is persisted before // the waves and is therefore unaffected by the ceiling. Counting drafted rows instead would let a // defect that skips members disguise itself as a thin fixture. manifestChunks, _, err := r.readModelChunks() if err != nil { t.Fatal(err) } var ch1 []int for _, c := range manifestChunks { if c.Chapter == 1 { ch1 = append(ch1, c.ChunkIdx) } } if len(ch1) < 2 { t.Fatalf("fixture is degenerate: the cut gave chapter 1 %d draft chunk(s), so the multi-member path is untested", len(ch1)) } // EVERY member of the admitted unit was drafted — not just its leader. for _, idx := range ch1 { if !drafted[1][idx] { t.Fatalf("chapter 1 has %d draft chunks in the manifest but member chunk %d was never drafted: the unit was admitted for the edit wave while part of its draft was skipped, so the buyer gets a truncated chapter they paid for in full", len(ch1), idx) } } // And the unit's edit really ran over the whole thing: one shipped unit, no dropped members. if len(res.Chunks) != 1 { t.Fatalf("one admitted unit ships one output unit, got %d", len(res.Chunks)) } if res.Chunks[0].DroppedMembers != 0 { t.Fatalf("the edit lost %d member(s) of a unit that was paid for in full: %+v", res.Chunks[0].DroppedMembers, res.Chunks[0]) } // Nothing outside the granted unit began. if len(drafted[2]) != 0 || len(drafted[3]) != 0 { t.Fatalf("chapters past the ceiling started anyway: ch2=%d ch3=%d chunks", len(drafted[2]), len(drafted[3])) } } // TestAFlaggedMemberWithNoEditRowIsNotFree pins what a unit whose draft flagged for one member and whose // EDIT row does not exist yet IS — the state every book is left in by a bank-signing stop, a Ctrl-C, or // any abort in the edit wave. resolveChunkState calls such a unit terminal the moment any row is flagged, // and a plan that borrowed that answer judged it FREE: admitted without a slot, reported as riding along // at $0, then charged a full editor call that no grant authorised. Found by this pack's own adversarial // pass, reproduced before it was fixed. // // What the unit is instead: STARTED and never shipped — rows on file for its draft positions, none for // its edit — so it is CARRIED (volume.go, unitCarried): finished outside the grant, paid for, counted in // Delivered and named apart. It is never free, and it never takes a second slot; the grant here goes to // a unit nobody has started (backlog row 232, second half; the measurement is in volumeslot_test.go). // // It also pins the CARRY'S OWN BOUND: two units were started, the grant is one, so exactly one of them // rides along and the other waits like any undelivered unit. Unbounded, this admission would let a run // that drafted a whole book hand every one of its units to the next purchase. func TestAFlaggedMemberWithNoEditRowIsNotFree(t *testing.T) { var mu sync.Mutex editWorks := false rec := &reqRec{} srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { body, _ := io.ReadAll(req.Body) rec.record(string(body)) s := string(body) if isEditBody(s) { mu.Lock() ok := editWorks mu.Unlock() if !ok { // Run 1's editor never completes, so no edit chunk_status row is ever written — exactly // the store state an interrupted edit wave leaves. w.WriteHeader(http.StatusInternalServerError) return } writeFakeCompletion(w, "ОТРЕДАКТИРОВАННЫЙ ПЕРЕВОД", "stop") return } if strings.Contains(s, "壊れた時計") { writeFakeCompletion(w, "", "stop") // an empty draft flags the member return } writeFakeCompletion(w, "ЧЕРНОВИК ПЕРЕВОДА", "stop") })) defer srv.Close() const seg = "\nsegmentation:\n draft_budget_out: 24\n edit_ceiling_out: 8000\n fertility: { cjk: 1.1978, other: 0.3852 }\n" body := "静かな図書館の朝。鈴木は本を読んだ。壊れた時計があった。外では雨が降っていた。彼は窓を見た。
" clean := "静かな図書館の朝。鈴木は本を読んだ。外では雨が降っていた。彼は窓を見た。時間は過ぎた。
" bookPath := setupProjectOpts(t, srv.URL, projectOpts{ epub: []chunktest.Chapter{ {ID: "c1", Href: "c1.xhtml", Body: body}, {ID: "c2", Href: "c2.xhtml", Body: clean}, {ID: "c3", Href: "c3.xhtml", Body: clean}, {ID: "c4", Href: "c4.xhtml", Body: clean}, }, spine: []string{"c1", "c2", "c3", "c4"}, regenerate: 0, waveWorkers: 1, gatesYAML: seg, }) ctx := obs.WithReqInfo(context.Background(), obs.ReqInfo{TraceID: obs.NewTraceID()}) // Run 1, granted two units: their drafts land (one member of unit 1 flagged empty), the editor dies, // so NO edit row is written anywhere and chapters 3–4 are never started. r1 := newRunner(t, bookPath) r1.MaxUnits = 2 if _, err := r1.TranslateBook(ctx); err == nil { t.Fatalf("precondition: the editor was supposed to fail this run") } rows, err := r1.Store.ChunkStatusesForBook("test-book") if err != nil { t.Fatal(err) } r1.Close() flagged, edits, drafted := 0, 0, map[int]bool{} for _, cs := range rows { if cs.Stage == "edit" { edits++ } if cs.Disposition == string(DispFlagged) { flagged++ } drafted[cs.Chapter] = true } if flagged == 0 || edits != 0 || len(drafted) != 2 { t.Fatalf("precondition: want a flagged draft member, NO edit row and two chapters drafted, got flagged=%d edit rows=%d drafted=%v", flagged, edits, drafted) } // Run 2, granted ONE unit. The two started units are not free — their edits have never run — and they // take no slot either; but the carry is bounded by the grant, so ONE of them rides along, the grant // buys chapter 3, and the other started unit and chapter 4 both wait. mu.Lock() editWorks = true mu.Unlock() callsBefore := rec.count() r2 := newRunner(t, bookPath) defer r2.Close() r2.MaxUnits = 1 res, err := r2.TranslateBook(ctx) if err != nil { t.Fatal(err) } if res.Volume == nil { t.Fatalf("chapter 4 had to be deferred, so the run must report a volume stop; it reported none — a unit was judged free and paid for anyway, or the grant was overrun") } v := *res.Volume if v.Free != 0 { t.Fatalf("a unit with no edit row is NOT free — its editor has never run and will be paid for: %+v", v) } if v.Delivered != 2 || v.Carried != 1 || v.Reworked != 0 || v.LeftFresh != 2 || v.LeftRework != 0 { t.Fatalf("one started unit rides along (the carry is bounded by the grant of one), the grant buys a new unit, and two units wait: %+v", v) } if len(res.Chunks) != 2 { t.Fatalf("OVERSPEND: %d output unit(s) were paid for under --max-units 1 with one unit carried", len(res.Chunks)) } // One editor call for the carried unit, a full chain for the new one, nothing for the two that wait. want := 1 + len(chunksOfChapter(t, r2, 3)) + 1 if fresh := rec.count() - callsBefore; fresh != want { t.Fatalf("the run made %d provider call(s), want %d (the carried unit's edit + chapter 3's drafts and edit)", fresh, want) } } // TestTheCeilingRefusesAPipelineWhoseUnitsAreTwoThings pins the refusal for the one pipeline shape in // which "output unit" stops naming a single object. // // config validates stage ROLES but imposes no ORDER, so [draft(translator), edit(editor), // polish(translator)] loads. finalStageWave() then answers waveDraft — the last role is a translator — // so outputUnits returns one singleton per DRAFT CHUNK, while the edit wave still groups those chunks // through buildEditUnits. A ceiling granted in singletons would admit one member's chunk and then let the // edit wave run the whole multi-member unit anyway, feeding the editor a unit whose source is complete // and whose draft is missing a member — a full-price call over half-made input, charged as the one unit // that was bought. It would also make the platform's chapters→units conversion stop being exact, which is // the entire reason for measuring in units. Refused loudly rather than silently miscounted. func TestTheCeilingRefusesAPipelineWhoseUnitsAreTwoThings(t *testing.T) { rec := &reqRec{} srv := newJSONProvider(rec, draftEdit) defer srv.Close() // gatesYAML is appended immediately after the stage list, so this continues it with a THIRD stage // whose role is a translator — putting the shipping stage back in the draft wave. const polish = " - { name: polish, role: translator, model: fake-model, prompt_override: prompts/translator.md, prompt_version: v-test, temperature: 0.3, reasoning: \"off\" }\n" bookPath := setupProjectOpts(t, srv.URL, projectOpts{ regenerate: 1, source: suzukiSource, gatesYAML: polish, }) r := newRunner(t, bookPath) defer r.Close() // Precondition: the fixture really is the incoherent shape, or the test proves nothing. if r.finalStageWave() != waveDraft || len(r.waveStagesIndexed(waveEdit)) == 0 { t.Fatalf("fixture drifted: want a translator-last pipeline that still has editor stages, got finalWave=%v editStages=%d", r.finalStageWave(), len(r.waveStagesIndexed(waveEdit))) } // With no ceiling the shape is none of this pack's business and must be left exactly as it was. r.MaxUnits = 0 if scope, err := r.planVolume(context.Background(), nil, nil); err != nil || scope != nil { t.Fatalf("an unbounded run must be untouched by this refusal: scope=%v err=%v", scope, err) } // With one, the ceiling cannot mean one thing, so it refuses. r.MaxUnits = 1 _, err := r.planVolume(context.Background(), nil, nil) if err == nil { t.Fatalf("a ceiling counted in draft chunks while the edit wave works in grouped units must refuse, not guess") } if !strings.Contains(err.Error(), "cannot bound this pipeline") { t.Fatalf("the refusal must say which shape it is turning away, got: %v", err) } } // TestAFullyDeliveredBookOffersNothingLeftToBuy pins the defect that made re-payment look like a product. // // The trajectory: a book translated to 100%, then a bank edit. Every unit is now "not free", so the old // single counter admitted them, called them granted, and reported the rest as "remain in the book" — which // the CLI presented as what is left to BUY. Purchase one said 29 remained, purchase two said 28, and not // one new chapter existed at any point. exit 0 and `ready` both agreed it had gone well. // // The counters must now say the true thing: nothing was DELIVERED, N units were RE-MADE, and the number // of never-delivered units is ZERO — so there is nothing to invite anyone to buy. func TestAFullyDeliveredBookOffersNothingLeftToBuy(t *testing.T) { rec := &reqRec{} srv := newJSONProvider(rec, draftEdit) defer srv.Close() bookPath := volumeBook(t, srv.URL, 4) dir := filepath.Dir(bookPath) ctx := obs.WithReqInfo(context.Background(), obs.ReqInfo{TraceID: obs.NewTraceID()}) r1 := newRunner(t, bookPath) if _, err := r1.TranslateBook(ctx); err != nil { t.Fatal(err) } r1.Close() // Every chapter contains 静か, so signing it moves every unit's injected bytes: the whole book becomes // re-payable, and none of it becomes new. doc := decisionsDocFor(t, dir, "test-book", membank.Decision{ Action: membank.ActionApprove, Src: "静か", Dst: "тихий"}) if _, err := ApplyBankDecisions(ctx, bookPath, doc, false); err != nil { t.Fatal(err) } r2 := newRunner(t, bookPath) defer r2.Close() r2.Resnapshot = true r2.MaxUnits = 1 r2.AcceptRebill = RebillConsent{Given: true} // the operator consented; that is not what is under test res, err := r2.TranslateBook(ctx) if err != nil { t.Fatal(err) } v := res.Volume if v == nil { t.Fatalf("three units were held back, so the run must report a volume stop") } if v.Delivered != 0 { t.Fatalf("this book was already complete: no purchase over it can DELIVER anything, yet %d was reported", v.Delivered) } if v.Reworked != 1 { t.Fatalf("one already-delivered unit was re-made; got %d", v.Reworked) } if v.LeftFresh != 0 { t.Fatalf("⚠ %d unit(s) reported as never-delivered on a fully translated book — this is the number the CLI turns into «buy more», and it would be selling chapters the reader already owns", v.LeftFresh) } if v.LeftRework != 3 { t.Fatalf("three delivered units still await a re-make; got %d", v.LeftRework) } // And the sentence the stop carries has to say it in words, not only in fields. (The operator-facing // invitation is the CLI's own line and is pinned in cmd/tmctl/render_test.go, where it lives.) line := v.String() if !strings.Contains(line, "0 NEW unit(s) delivered") { t.Fatalf("the stop must state that nothing new was delivered: %s", line) } if !strings.Contains(line, "0 unit(s) NEVER delivered") { t.Fatalf("the stop must state that nothing is left undelivered: %s", line) } if !strings.Contains(line, "unrefreshed, NOT unbought") { t.Fatalf("the remainder must be named for what it is: %s", line) } } // TestARunThatRePaysNothingIsNotAskedForConsent pins the early return the consent gate needs in order not // to refuse legitimate work — a branch a mutation showed nothing was holding. // // The gate judges its THRESHOLD against the whole book, precisely so a caller cannot shrink it by // splitting the run. That is right, and it has an edge: a bounded run whose granted units are all FRESH // re-pays nothing at all, and demanding consent from it would be asking a caller to approve a spend that // is not going to happen — the other half of Р6, and the failure this pack was already caught committing // once in the opposite direction. // // The state is built rather than waited for: the book is translated whole, the first two units' rows are // then reset (which is exactly what a redrive does to a unit), and a term that occurs only in the LATER // chapters is signed. That leaves units 1–2 never-delivered and units 3–4 delivered-and-superseded, so a // purchase of two units is granted only fresh work while the book's drift sits over the threshold. func TestARunThatRePaysNothingIsNotAskedForConsent(t *testing.T) { rec := &reqRec{} srv := newJSONProvider(rec, draftEdit) defer srv.Close() var eps []chunktest.Chapter var spine []string for i := 1; i <= 4; i++ { id := fmt.Sprintf("c%d", i) body := fmt.Sprintf("朝%d。鈴木は歩いた。
", i) if i >= 3 { // 静か occurs ONLY in the later chapters, so signing it supersedes those units and leaves the // earlier ones alone. body = fmt.Sprintf("静かな朝%d。鈴木は歩いた。
", i) } eps = append(eps, chunktest.Chapter{ID: id, Href: id + ".xhtml", Body: body}) spine = append(spine, id) } bookPath := setupProjectOpts(t, srv.URL, projectOpts{ epub: eps, spine: spine, regenerate: 1, waveWorkers: 1, rebillConsentUSD: 0.002, }) dir := filepath.Dir(bookPath) ctx := obs.WithReqInfo(context.Background(), obs.ReqInfo{TraceID: obs.NewTraceID()}) r1 := newRunner(t, bookPath) if _, err := r1.TranslateBook(ctx); err != nil { t.Fatal(err) } // Un-deliver the first two units, the way a redrive does. for ch := 1; ch <= 2; ch++ { if err := r1.Store.ResetChunkStages("test-book", ch, 0, []string{"draft", "edit"}); err != nil { t.Fatal(err) } } r1.Close() doc := decisionsDocFor(t, dir, "test-book", membank.Decision{ Action: membank.ActionApprove, Src: "静か", Dst: "тихий"}) if _, err := ApplyBankDecisions(ctx, bookPath, doc, false); err != nil { t.Fatal(err) } // Precondition: the book's drift really is over the threshold, so an UNBOUNDED run would be refused. rGate := newRunner(t, bookPath) rGate.Resnapshot = true _, gateErr := rGate.TranslateBook(ctx) rGate.Close() if gateErr == nil || !strings.Contains(gateErr.Error(), "RE-PAY") { t.Fatalf("precondition: the book's drift must exceed the threshold, got %v", gateErr) } // The bounded run is granted the two FRESH units only, so it re-pays nothing and must not be stopped. r2 := newRunner(t, bookPath) defer r2.Close() r2.Resnapshot = true r2.MaxUnits = 2 res, err := r2.TranslateBook(ctx) if err != nil { t.Fatalf("this run re-pays nothing — every unit it was granted is new work — so there is no concrete spend to consent to, and refusing it denies legitimate work: %v", err) } if res.Volume == nil || res.Volume.Delivered != 2 || res.Volume.Reworked != 0 { t.Fatalf("the grant should have gone entirely to fresh delivery: %+v", res.Volume) } if res.Volume.LeftRework != 2 { t.Fatalf("the two superseded units are still waiting to be re-made: %+v", res.Volume) } } // TestTheWireHashMemoDiesWithItsBank pins the SAFETY half of the reproduction memo. The memo itself is // only speed — a bounded run used to redo the full-book re-render up to three times (once in planVolume, // once per projection in the consent gate) with nothing changed between them. But every hash in it is // rendered against the materialized bank, so a memo that outlived its bank would hand a caller hashes for // a bank the run no longer has, and the free estimate would quote a number the paid run does not charge — // the exact class of silent wrongness this pack exists to remove. Its lifetime must therefore be the // bank's, and materializeBanks is the one place a bank changes (a mid-run re-seed included). func TestTheWireHashMemoDiesWithItsBank(t *testing.T) { rec := &reqRec{} srv := newJSONProvider(rec, draftEdit) defer srv.Close() bookPath := setupProjectOpts(t, srv.URL, projectOpts{ regenerate: 1, source: suzukiSource, glossarySeed: suzukiSeed, }) ctx := obs.WithReqInfo(context.Background(), obs.ReqInfo{TraceID: obs.NewTraceID()}) r := newRunner(t, bookPath) defer r.Close() if _, err := r.TranslateBook(ctx); err != nil { t.Fatal(err) } chunks, withText, err := r.readModelChunks() if err != nil { t.Fatal(err) } _ = chunks full, err := withText() if err != nil { t.Fatal(err) } sel := precomputeSticky(full, r.baseMemory, r.Pipeline.Context.GlossaryTokenBudget) first := r.cachedRenderedContentHashes(full, sel) if len(first) == 0 { t.Fatalf("the fixture produced no positions, so the memo is untested") } // Same bank → the very SAME map, not merely an equal one. Identity is the assertion that separates a // memo from a function that happens to be deterministic: an equal-value check would pass on code that // re-renders the whole book every call, which is exactly the cost this exists to remove. again := r.cachedRenderedContentHashes(full, sel) if reflect.ValueOf(again).Pointer() != reflect.ValueOf(first).Pointer() { t.Fatalf("the second call built a NEW map: the reproduction is being redone, not reused") } if !sameMap(again, first) { t.Fatalf("the memo changed under the same bank") } // A new materialization must throw it away. Anything else serves hashes belonging to a bank that is // no longer the run's. if err := r.projectFoldedMemory(); err != nil { t.Fatal(err) } if r.contentHashes != nil { t.Fatalf("the memo survived a re-materialization of the bank: its hashes are now about a bank this run does not have") } } // sameMap reports whether two hash maps hold the same positions and values. func sameMap(a, b map[chunkKey]map[string]string) bool { if len(a) != len(b) { return false } for k, av := range a { bv, ok := b[k] if !ok || len(av) != len(bv) { return false } for stage, h := range av { if bv[stage] != h { return false } } } return true } // TestAVolumeCeilingOnAMiningBookWarnsAboutTheNextPurchase pins the one warning this pack owes an // operator, on the one composition it makes worse. // // A bounded purchase drafts only part of the book, so the bank-mining stop consolidates over less than // the whole and writes a SMALLER auto-bank than the next purchase will. The next purchase folds the // larger one, which moves the edit-wave snapshot the previous purchase's edit jobs are pinned to — and // the job guard treats that exactly like a config change, so the run stops loudly without --resnapshot. // None of that is new machinery; what IS new is that a volume ceiling turns a rare interrupted-run corner // into the ordinary shape of selling a book in parts. The engine cannot fix the guard (ratified Р6 // contour, another pack's subject), so the least it owes is to say so before it happens — and a warning // nothing tests is a warning that can silently stop being emitted. func TestAVolumeCeilingOnAMiningBookWarnsAboutTheNextPurchase(t *testing.T) { rec := &reqRec{} // A plain provider, not the mining-stop one: this test is about the PLANNING warning, which is // emitted before either wave, so the stop behaviour is beside the point and its assertions would only // add an unrelated way to fail. srv := newJSONProvider(rec, draftEdit) defer srv.Close() bookPath := setupMiningStopProject(t, srv.URL, miningStopOpts{}) ctx := obs.WithReqInfo(context.Background(), obs.ReqInfo{TraceID: obs.NewTraceID()}) var logBuf bytes.Buffer r := newRunner(t, bookPath) defer r.Close() r.Log = slog.New(slog.NewTextHandler(&logBuf, &slog.HandlerOptions{Level: slog.LevelWarn})) r.MaxUnits = 1 // The warning is emitted while PLANNING, before any wave, so the run's own outcome is beside the point // here — what matters is that the operator was told before the money moved. if _, err := r.TranslateBook(ctx); err != nil { t.Logf("run ended with %v (not what this test is about)", err) } if !strings.Contains(logBuf.String(), "book that MINES its bank") { t.Fatalf("a volume ceiling on a mining-configured book must warn that the NEXT purchase moves the edit snapshot and will need --resnapshot; log was:\n%s", logBuf.String()) } if !strings.Contains(logBuf.String(), "--resnapshot") { t.Fatalf("the warning must name the remedy, not just the problem:\n%s", logBuf.String()) } // And it must NOT fire on a mining book with no ceiling — otherwise every ordinary run of every // mining book carries a warning about a situation it is not in. var quiet bytes.Buffer r2 := newRunner(t, setupMiningStopProject(t, srv.URL, miningStopOpts{})) defer r2.Close() r2.Log = slog.New(slog.NewTextHandler(&quiet, &slog.HandlerOptions{Level: slog.LevelWarn})) if _, err := r2.TranslateBook(ctx); err != nil { t.Logf("unbounded run ended with %v", err) } if strings.Contains(quiet.String(), "book that MINES its bank") { t.Fatalf("an unbounded run is not in this situation and must not be warned about it:\n%s", quiet.String()) } } // TestAConsentedSliceIsAlwaysToldWhatTheBookCarries pins the property that makes the capped-consent path // consent rather than a hole — a distinction settled by ratification rather than by argument. // // A caller CAN re-pay a whole book a slice at a time with `--resnapshot --max-units 1 --accept-rebill=X` // looped: every run stays under its own named cap and the book-wide threshold never refuses. That is not // a bypass — it reaches no total a single bare `--accept-rebill` would not reach in one invocation, and // each run names an amount and is charged exactly it. What makes it consent rather than a bypass is that // the caller is TOLD, on every run, that the book carries drift beyond its threshold and is being bought // down a slice at a time. Take that disclosure away and the same code becomes the hole it is not. func TestAConsentedSliceIsAlwaysToldWhatTheBookCarries(t *testing.T) { rec := &reqRec{} srv := newJSONProvider(rec, draftEdit) defer srv.Close() var eps []chunktest.Chapter var spine []string for i := 1; i <= 4; i++ { id := fmt.Sprintf("c%d", i) eps = append(eps, chunktest.Chapter{ID: id, Href: id + ".xhtml", Body: fmt.Sprintf("静かな図書館の朝%d。
", i)}) spine = append(spine, id) } bookPath := setupProjectOpts(t, srv.URL, projectOpts{ epub: eps, spine: spine, regenerate: 1, waveWorkers: 4, rebillConsentUSD: 0.005, }) dir := filepath.Dir(bookPath) ctx := obs.WithReqInfo(context.Background(), obs.ReqInfo{TraceID: obs.NewTraceID()}) r1 := newRunner(t, bookPath) if _, err := r1.TranslateBook(ctx); err != nil { t.Fatal(err) } r1.Close() doc := decisionsDocFor(t, dir, "test-book", membank.Decision{ Action: membank.ActionApprove, Src: "静か", Dst: "тихий"}) if _, err := ApplyBankDecisions(ctx, bookPath, doc, false); err != nil { t.Fatal(err) } var logBuf bytes.Buffer r2 := newRunner(t, bookPath) defer r2.Close() r2.Log = slog.New(slog.NewTextHandler(&logBuf, &slog.HandlerOptions{Level: slog.LevelWarn})) r2.Resnapshot = true r2.MaxUnits = 1 r2.AcceptRebill = RebillConsent{Given: true, Capped: true, CapUSD: 0.002} if _, err := r2.TranslateBook(ctx); err != nil { t.Fatalf("a funded slice must be admitted: %v", err) } out := logBuf.String() // This run's own slice — the amount actually charged. if !strings.Contains(out, "rebill_usd=0.001820") { t.Fatalf("the accepted run must name what IT is charged; log:\n%s", out) } // And the book's whole drift beside it, so the caller can see they are buying it down in slices. for _, want := range []string{"book_rebill_units=4", "book_rebill_usd=0.007280", "threshold_usd=0.005000"} { if !strings.Contains(out, want) { t.Fatalf("a consented slice must be told what the BOOK carries (%q missing) — without it the caller cannot tell one purchase from buying the whole book a slice at a time; log:\n%s", want, out) } } } // TestAPurchaseBuysNewBookBeforeReMakingOldBook pins the ADMISSION ORDER, which nothing else holds. // // It is a rot path rather than a live defect: collapsing planVolume's two passes back into one book-order // pass leaves the whole battery green, so any later session tidying that loop — or adding a fourth unit // class and reordering it — silently restores a measured defect. Under book order a purchase of two units // on a book with two delivered re-makes chapters 1-2, reports Delivered=0 / Reworked=2 / LeftFresh=3, // exits 0, and the platform records `ready`: the buyer paid and the book did not advance by a chapter. // // The fixture puts the signed term ONLY in the early chapters, so exactly the delivered units supersede // and the undelivered tail stays fresh — the ordinary shape after a bank edit lands mid-sale. // // ⚠ Note the second assertion class: under book order this same purchase is not merely misallocated, it // is REFUSED — admitting rework makes the run's own re-payment non-zero and the book's drift is over the // threshold. Delivery-first admits only fresh work, so the consent gate has nothing to ask about and the // run goes through. That the purchase SUCCEEDS is therefore part of what is being pinned. func TestAPurchaseBuysNewBookBeforeReMakingOldBook(t *testing.T) { rec := &reqRec{} srv := newJSONProvider(rec, draftEdit) defer srv.Close() var eps []chunktest.Chapter var spine []string for i := 1; i <= 5; i++ { id := fmt.Sprintf("c%d", i) body := fmt.Sprintf("朝%d。鈴木は歩いた。
", i) if i <= 2 { // 静か occurs ONLY in the chapters the first purchase delivers, so signing it later supersedes // exactly those and leaves chapters 3-5 untouched. body = fmt.Sprintf("静かな朝%d。鈴木は歩いた。
", i) } eps = append(eps, chunktest.Chapter{ID: id, Href: id + ".xhtml", Body: body}) spine = append(spine, id) } bookPath := setupProjectOpts(t, srv.URL, projectOpts{ epub: eps, spine: spine, regenerate: 1, waveWorkers: 1, rebillConsentUSD: 0.002, }) dir := filepath.Dir(bookPath) ctx := obs.WithReqInfo(context.Background(), obs.ReqInfo{TraceID: obs.NewTraceID()}) r1 := newRunner(t, bookPath) r1.MaxUnits = 2 res1, err := r1.TranslateBook(ctx) if err != nil { t.Fatal(err) } r1.Close() if res1.Volume == nil || res1.Volume.Delivered != 2 { t.Fatalf("the first purchase delivers chapters 1-2: %+v", res1.Volume) } doc := decisionsDocFor(t, dir, "test-book", membank.Decision{ Action: membank.ActionApprove, Src: "静か", Dst: "тихий"}) if _, err := ApplyBankDecisions(ctx, bookPath, doc, false); err != nil { t.Fatal(err) } r2 := newRunner(t, bookPath) defer r2.Close() r2.Resnapshot = true r2.MaxUnits = 2 res2, err := r2.TranslateBook(ctx) if err != nil { t.Fatalf("a purchase that spends its grant entirely on NEW book re-pays nothing, so the consent gate has nothing to ask about and the run must go through; under book order this same purchase is refused: %v", err) } v := res2.Volume if v == nil { t.Fatalf("units were held back, so the run must report a volume stop") } if v.Delivered != 2 || v.Reworked != 0 { t.Fatalf("the grant must buy NEW book while the book still has any — got Delivered=%d Reworked=%d; a single book-order pass would report 0 and 2", v.Delivered, v.Reworked) } if v.LeftFresh != 1 || v.LeftRework != 2 { t.Fatalf("one chapter never delivered and two awaiting a re-make: %+v", *v) } // And the chapters that actually ran are the NEW ones, not the old ones re-made. started := chaptersWithRows(t, r2.Store, "test-book") for _, ch := range []int{3, 4} { if !started[ch] { t.Fatalf("chapter %d was never delivered and had the grant available, yet did not run: %v", ch, started) } } if started[5] { t.Fatalf("chapter 5 is past the grant and must not have run") } } // TestTheVolumeCeilingMovesNoRequestByte is review axis 3 (determinism), executed rather than asserted. // // A ceiling is an OPERATOR axis: it decides how much of the book this process does, never what the // process sends. If setting it moved a wave snapshot or a rendered byte, every already-paid unit of the // book would be superseded the moment anyone passed the flag — a ceiling that re-bought the book would be // worse than no ceiling. The property is claimed in Runner.MaxUnits's doc comment; nothing held it. // // Two halves, because either alone is weak: the SNAPSHOT must not move (that is what re-pins jobs and // re-bills waves), and the WIRE BYTES must be identical (that is what the resume fast-path compares). func TestTheVolumeCeilingMovesNoRequestByte(t *testing.T) { // (1) The snapshot is blind to the ceiling. rec0 := &reqRec{} srv0 := newJSONProvider(rec0, draftEdit) defer srv0.Close() r := newRunner(t, volumeBook(t, srv0.URL, 3)) defer r.Close() if err := r.seedGlossary(context.Background()); err != nil { t.Fatal(err) } r.MaxUnits = 0 draftOff, _, err := r.snapshotIDForWave(waveDraft) if err != nil { t.Fatal(err) } editOff, _, err := r.snapshotIDForWave(waveEdit) if err != nil { t.Fatal(err) } r.MaxUnits = 2 draftOn, _, err := r.snapshotIDForWave(waveDraft) if err != nil { t.Fatal(err) } editOn, _, err := r.snapshotIDForWave(waveEdit) if err != nil { t.Fatal(err) } if draftOff != draftOn || editOff != editOn { t.Fatalf("setting a volume ceiling moved a wave snapshot (draft %.12s→%.12s, edit %.12s→%.12s): every already-paid unit of the book would be superseded by passing the flag", draftOff, draftOn, editOff, editOn) } // (2) The bytes a granted unit sends are the bytes it would have sent unbounded — including when the // ceiling serves units OUT of book order, which delivery-before-rework does by design. recFull := &reqRec{} srvFull := newJSONProvider(recFull, draftEdit) defer srvFull.Close() rFull := newRunner(t, volumeBook(t, srvFull.URL, 3)) if _, err := rFull.TranslateBook(context.Background()); err != nil { t.Fatal(err) } rFull.Close() recCut := &reqRec{} srvCut := newJSONProvider(recCut, draftEdit) defer srvCut.Close() rCut := newRunner(t, volumeBook(t, srvCut.URL, 3)) defer rCut.Close() rCut.MaxUnits = 1 if _, err := rCut.TranslateBook(context.Background()); err != nil { t.Fatal(err) } full := map[string]bool{} for _, b := range recFull.all() { full[b] = true } cut := recCut.all() if len(cut) == 0 { t.Fatalf("the bounded run sent nothing, so nothing is compared") } for _, b := range cut { if !full[b] { t.Fatalf("a bounded run sent a request the unbounded run never sent — the ceiling changed what goes on the wire, and every unit it touches would be re-billed:\n%.400s", b) } } } // TestAMoneyHaltStillSaysWhatTheVolumeGrantWas covers the last of the round-2 cosmetics, and it is the // same class as the mining warning: a diagnostic nothing tests is a diagnostic that silently stops being // emitted. // // When the MONEY ceiling stops a run there is no result to carry the volume report, so without this line // the operator is told the run halted on money and nothing about what it had been granted or how far that // got — the first thing anyone asks. The durable progress is in the store either way; this is about the // stderr account of the run being whole rather than half. func TestAMoneyHaltStillSaysWhatTheVolumeGrantWas(t *testing.T) { rec := &reqRec{} srv := newJSONProvider(rec, draftEdit) defer srv.Close() bookPath := volumeBook(t, srv.URL, 4) ctx := obs.WithReqInfo(context.Background(), obs.ReqInfo{TraceID: obs.NewTraceID()}) var logBuf bytes.Buffer r := newRunner(t, bookPath) defer r.Close() r.Log = slog.New(slog.NewTextHandler(&logBuf, &slog.HandlerOptions{Level: slog.LevelWarn})) r.MaxUnits = 2 // the ceiling binds: two units are held back r.CeilingUSD = 0.0000001 // ...but money runs out first _, err := r.TranslateBook(ctx) var halt *CeilingHalt if !errors.As(err, &halt) { t.Fatalf("precondition: the money ceiling must be what stops this run, got %v", err) } out := logBuf.String() if !strings.Contains(out, "the stop below is NOT the volume ceiling") { t.Fatalf("a money halt inside a volume-bounded run must say which ceiling stopped it and what the grant was; log:\n%s", out) } if !strings.Contains(out, "max_units=2") { t.Fatalf("the line must name the grant the run was given:\n%s", out) } // And it must NOT appear when no ceiling was in force — an ordinary money halt has no volume grant to // report, and inventing one would be noise on every halted run in the fleet. var quiet bytes.Buffer r2 := newRunner(t, volumeBook(t, srv.URL, 4)) defer r2.Close() r2.Log = slog.New(slog.NewTextHandler(&quiet, &slog.HandlerOptions{Level: slog.LevelWarn})) r2.CeilingUSD = 0.0000001 if _, err := r2.TranslateBook(ctx); err == nil { t.Fatalf("precondition: this run must also halt on money") } if strings.Contains(quiet.String(), "the stop below is NOT the volume ceiling") { t.Fatalf("an unbounded run has no volume grant to report:\n%s", quiet.String()) } } // TestFreeUnitsAreReJudgedWhenTheBankMovesMidRun pins the fix for the acceptance hunter's blocking // finding: on a mining book the ceiling did not hold, and the report called the overspend free. // // THE CHAIN. planVolume classifies before the draft wave. Between the two waves sits the bank-mining // stop, whose auto-continue branch RE-SEEDS the bank (mining.go) — after which waverun recomputes the // edit-wave snapshot. Units classified FREE are admitted OUTSIDE the grant, because free work costs // nothing. So every one of them was judged against a snapshot the run itself then replaced, and any whose // injected bytes the new bank changes gets a fresh PAID editor call no grant authorised — reported as // "rode along at $0". Measured by the hunter: four calls under a grant of one, $0.007280 called free. // // The test drives rescopeEditWave directly rather than waiting for a fixture in which the auto-bank // happens to change a delivered unit's bytes. That is deliberate: the defect is that nothing GUARANTEES // the two snapshots agree, so the invariant — "a unit admitted without a slot is re-judged against the // snapshot the wave really uses" — is what must hold for every book, including ones no fixture reaches. func TestFreeUnitsAreReJudgedWhenTheBankMovesMidRun(t *testing.T) { rec := &reqRec{} srv := newJSONProvider(rec, draftEdit) defer srv.Close() bookPath := volumeBook(t, srv.URL, 4) ctx := obs.WithReqInfo(context.Background(), obs.ReqInfo{TraceID: obs.NewTraceID()}) r := newRunner(t, bookPath) defer r.Close() if _, err := r.TranslateBook(ctx); err != nil { t.Fatal(err) } chunks, withText, err := r.readModelChunks() if err != nil { t.Fatal(err) } _ = chunks full, err := withText() if err != nil { t.Fatal(err) } sel := precomputeSticky(full, r.baseMemory, r.Pipeline.Context.GlossaryTokenBudget) units := r.outputUnits(full) if len(units) < 3 { t.Fatalf("fixture needs several units, got %d", len(units)) } // A scope in the exact shape planVolume leaves: every unit judged FREE (the book is fully delivered // and nothing has moved yet) and admitted without consuming any grant. newScope := func(max int) *volumeScope { s := &volumeScope{ admitted: map[chunkKey]bool{}, leader: map[chunkKey]bool{}, class: map[chunkKey]unitClass{}, editBlocked: map[chunkKey]bool{}, stop: VolumeStop{MaxUnits: max}, editSnapshot: "SNAPSHOT-AT-PLAN-TIME", } for _, u := range units { key := chunkKey{u.Chapter, u.FirstChunkIdx} s.admitted[key] = true s.class[key] = unitFree s.stop.Free++ for _, m := range u.Members { s.leader[chunkKey{m.Chapter, m.ChunkIdx}] = true } } return s } // (1) The snapshot did NOT move: nothing may be re-judged, and the ordinary path pays nothing for // this machinery existing. same := newScope(1) same.editSnapshot = "X" if err := r.rescopeEditWave(ctx, same, units, full, sel, "X"); err != nil { t.Fatal(err) } if same.stop.Free != len(units) || same.stop.Paid() != 0 || len(same.editBlocked) != 0 { t.Fatalf("an unmoved snapshot must change nothing: %+v", same.stop) } // (2) The snapshot MOVED. Every unit's edit row is now on a superseded snapshot that is NOT a // re-pinnable bank-only move (the id is unrelated), so none of them is free any more. With a grant of // one, exactly one may take a slot and the rest must be HELD BACK — not run for free. moved := newScope(1) if err := r.rescopeEditWave(ctx, moved, units, full, sel, "A-DIFFERENT-SNAPSHOT"); err != nil { t.Fatal(err) } if moved.stop.Free != 0 { t.Fatalf("units whose bytes the new bank changed are not free; %d still counted as riding along at $0 — that is the overspend reported as free", moved.stop.Free) } if moved.stop.Paid() != 1 { t.Fatalf("the grant is one unit, so exactly one may be paid for; got %d — the ceiling did not hold", moved.stop.Paid()) } if moved.stop.LeftRework != len(units)-1 { t.Fatalf("the rest must be held back as already-delivered-not-yet-re-made, got LeftRework=%d of %d units", moved.stop.LeftRework, len(units)) } // And the held-back ones must actually be refused by the wave's own admission check. held := 0 for _, u := range units { if !moved.allowsUnit(u) { held++ } } if held != len(units)-1 { t.Fatalf("%d unit(s) were blocked in the report but %d are actually refused by allowsUnit — the accounting and the gate disagree", moved.stop.LeftRework, held) } } // TestTheEditWaveRefusesAStalePlan pins the WIRING of the re-plan, which the behavioural test above // cannot: that test calls rescopeEditWave directly, so deleting its call site in the wave driver left the // whole battery green while the ceiling stopped holding. The invariant is now the production code's own — // the wave refuses to run a scope planned against a snapshot it is not using — so the re-plan cannot be // removed without the run saying so. func TestTheEditWaveRefusesAStalePlan(t *testing.T) { rec := &reqRec{} srv := newJSONProvider(rec, draftEdit) defer srv.Close() bookPath := volumeBook(t, srv.URL, 3) ctx := obs.WithReqInfo(context.Background(), obs.ReqInfo{TraceID: obs.NewTraceID()}) // An ordinary bounded run must pass the guard — it is an invariant, not a tax. r := newRunner(t, bookPath) defer r.Close() r.MaxUnits = 2 if _, err := r.TranslateBook(ctx); err != nil { t.Fatalf("a normal bounded run must satisfy the invariant: %v", err) } // And a scope carrying a stale plan is refused rather than run. stale := &volumeScope{ admitted: map[chunkKey]bool{}, leader: map[chunkKey]bool{}, class: map[chunkKey]unitClass{}, editBlocked: map[chunkKey]bool{}, stop: VolumeStop{MaxUnits: 1}, editSnapshot: "PLANNED-AGAINST-SOMETHING-ELSE", } if stale.editSnapshot == "" { t.Fatalf("fixture: the stale scope must carry a plan snapshot") } // rescopeEditWave is what repairs it; after it runs the guard's condition is satisfied. if err := r.rescopeEditWave(ctx, stale, nil, nil, nil, "THE-REAL-ONE"); err != nil { t.Fatal(err) } if stale.editSnapshot != "THE-REAL-ONE" { t.Fatalf("the re-plan must adopt the snapshot it re-judged against, else the guard can never pass; got %q", stale.editSnapshot) } } // TestAFlaggedUnitIsNotReportedAsDelivered pins the acceptance hunter's finding 2. A unit can be paid for // and come back FLAGGED with nothing shippable; the plan-time counter called it delivered anyway, so a // purchase of two units one of which flagged printed "2 NEW unit(s) delivered" over a single readable // unit. Admission has to be decided before the work — that is what bounds the money — but a decision to // PAY for a unit is not a fact about a chapter existing, and the report must not conflate them. func TestAFlaggedUnitIsNotReportedAsDelivered(t *testing.T) { var mu sync.Mutex flaggedOne := false rec := &reqRec{} srv := newJSONProvider(rec, func(body string) (string, string) { if !isEditBody(body) && strings.Contains(body, "朝1") { mu.Lock() flaggedOne = true mu.Unlock() return "", "stop" // an empty draft flags the unit and ships nothing } return draftEdit(body) }) defer srv.Close() var eps []chunktest.Chapter var spine []string for i := 1; i <= 5; i++ { id := fmt.Sprintf("c%d", i) eps = append(eps, chunktest.Chapter{ID: id, Href: id + ".xhtml", Body: fmt.Sprintf("朝%d。鈴木は歩いた。
", i)}) spine = append(spine, id) } bookPath := setupProjectOpts(t, srv.URL, projectOpts{ epub: eps, spine: spine, regenerate: 0, waveWorkers: 1, }) ctx := obs.WithReqInfo(context.Background(), obs.ReqInfo{TraceID: obs.NewTraceID()}) r := newRunner(t, bookPath) defer r.Close() r.MaxUnits = 2 res, err := r.TranslateBook(ctx) if err != nil { var flags *CompletedWithFlags if !errors.As(err, &flags) { t.Fatal(err) } } if !flaggedOne { t.Fatalf("fixture never produced the flag, so it proves nothing") } shipped := 0 for _, oc := range res.Chunks { if oc.FinalText != "" { shipped++ } } v := res.Volume if v == nil { t.Fatalf("three units were held back, so a volume stop must be reported") } if v.Delivered != shipped { t.Fatalf("the stop claims %d NEW unit(s) delivered but only %d shipped readable text — a paid-and-flagged unit is not a delivery: %+v", v.Delivered, shipped, *v) } if v.Flagged != 1 { t.Fatalf("one unit was paid for and flagged; the report must say so rather than fold it into delivery: %+v", *v) } if !strings.Contains(v.String(), "PAID FOR BUT FLAGGED") { t.Fatalf("the operator line must name money spent for no text: %s", v.String()) } }