package main import ( "bytes" "encoding/json" "errors" "fmt" "strings" "testing" "unicode/utf8" "textmachine/backend/internal/llm" "textmachine/backend/internal/pipeline" "textmachine/backend/internal/store" ) // render_test.go pins the human/JSON output contracts of the extracted renders // (package №4): sentinel returns (exit 2), «what to do» advice, sections only when // data is non-empty, report err-tail, dry-run redrive. func okLedger() (float64, float64, error) { return 0.123456, 0, nil } func TestRenderTranslateOKAndLedger(t *testing.T) { var b strings.Builder res := &pipeline.BookResult{BookID: "b1", TotalUSD: 0.01, Chunks: []pipeline.ChunkOutcome{{ Chapter: 1, ChunkIdx: 0, Disposition: pipeline.DispOK, FinalText: "ТЕКСТ", Stages: []pipeline.StageResult{{Stage: "draft", Model: "m", Disposition: pipeline.DispOK, Usage: llm.Usage{PromptTokens: 10, CompletionTokens: 5}, Attempts: 1, FinishReason: "stop"}}, }}} if err := renderTranslate(&b, res, okLedger); err != nil { t.Fatal(err) } out := b.String() for _, want := range []string{ "=== CHAPTER 1 CHUNK 0 — ok ===", "ТЕКСТ", "TOTAL (this run): $0.010000 — chunks 1, flags 0", "Book ledger: committed=$0.123456 reserved=$0.000000", } { if !strings.Contains(out, want) { t.Fatalf("translate output must contain %q, got:\n%s", want, out) } } } func TestRenderTranslateFlaggedSentinel(t *testing.T) { var b strings.Builder res := &pipeline.BookResult{BookID: "b1", Flagged: 1, Chunks: []pipeline.ChunkOutcome{{ Chapter: 2, ChunkIdx: 1, Disposition: pipeline.DispFlagged, FlagReason: pipeline.FlagSoftRefusal, }}} err := renderTranslate(&b, res, okLedger) var flagged *pipeline.CompletedWithFlags if !errors.As(err, &flagged) || flagged.Flagged != 1 { t.Fatalf("flagged run must return the exit-2 sentinel, got: %v", err) } if !strings.Contains(b.String(), "[FLAG soft_refusal]") { t.Fatalf("flagged chunk banner missing:\n%s", b.String()) } } func TestRenderTranslateLedgerErrorAfterTotals(t *testing.T) { // Frozen ordering of the partial output: the totals line is already printed, // a ledger read error aborts AFTER it (hoisting the read ahead of the render would // change the bytes of the partial output — extraction risk-map, item 4). var b strings.Builder res := &pipeline.BookResult{BookID: "b1"} err := renderTranslate(&b, res, func() (float64, float64, error) { return 0, 0, errors.New("boom") }) if err == nil || err.Error() != "boom" { t.Fatalf("ledger error must propagate, got %v", err) } if !strings.Contains(b.String(), "TOTAL") || strings.Contains(b.String(), "Book ledger") { t.Fatalf("totals must be printed, ledger line must not:\n%s", b.String()) } } func TestRenderReportColumnsAndErrTail(t *testing.T) { var b strings.Builder rows := []store.RequestLogView{ {TS: "2026-07-10 17:00:00", Chapter: 3, ChunkIdx: 2, Stage: "draft", Role: "translator", ModelRequested: "fake-model", ModelActual: "", Err: "connect: refused", OK: 0}, {TS: "2026-07-10 17:00:05", Chapter: 3, ChunkIdx: 2, Stage: "draft", Role: "translator", ModelRequested: "fake-model", ModelActual: "fake-model", Degraded: "cjk_artifact", OK: 0}, } if err := renderReport(&b, func() ([]store.RequestLogView, error) { return rows, nil }, func() ([]store.ChunkStatus, error) { return nil, nil }, func() ([]store.RetrievalState, error) { return nil, nil }, okLedger); err != nil { t.Fatal(err) } out := b.String() // Package №4: for a failed call model_actual is empty — the row must carry // the requested model, its position, and the error text (post-mortem without sqlite3). if !strings.Contains(out, "fake-model(req)") { t.Fatalf("failed call must show the requested model, got:\n%s", out) } if !strings.Contains(out, "connect: refused") || !strings.Contains(out, "cjk_artifact") { t.Fatalf("err/degraded tail missing:\n%s", out) } // Flag/memory sections are not printed without data. if strings.Contains(out, "FLAGS") || strings.Contains(out, "MEMORY") { t.Fatalf("empty sections must not print headers:\n%s", out) } } // TestErrTailTruncatesOnRuneBoundary pins the D23.4 fix: errTail carries raw provider // body snippets (CJK/Cyrillic), so a >120-byte tail must be cut on a RUNE boundary — a // bare byte slice s[:120] would split a multibyte rune into invalid UTF-8. The existing // column test does not exercise the boundary (its strings are short/ASCII), so a revert of // the fix survived it — this closes that gap. Mutation: replace the RuneStart walk-back in // errTail with `s = s[:120] + "…"` and this goes red. func TestErrTailTruncatesOnRuneBoundary(t *testing.T) { // One ASCII byte then 100 Cyrillic runes (2 bytes each): rune starts fall at offset 0 // then the ODD offsets, so byte 120 (even) lands on a continuation byte — a mid-rune cut. long := "x" + strings.Repeat("я", 100) // 201 bytes, well over the 120 cap got := errTail("", long) if !utf8.ValidString(got) { t.Fatalf("errTail produced invalid UTF-8 — a byte-boundary cut split a rune: %q", got) } if strings.ContainsRune(got, '�') { t.Fatalf("errTail leaked a replacement char (broken rune): %q", got) } if !strings.HasSuffix(got, "…") { t.Fatalf("a truncated tail must end with the ellipsis, got %q", got) } // A short tail is returned verbatim (degraded + err joined, no truncation, no ellipsis). if s := errTail("cjk_artifact", "boom"); s != "cjk_artifact boom" { t.Fatalf("short tail must be returned verbatim, got %q", s) } } func TestRenderReportSections(t *testing.T) { var b strings.Builder flags := []store.ChunkStatus{ {Chapter: 1, ChunkIdx: 0, Stage: "draft", Disposition: "ok"}, {Chapter: 2, ChunkIdx: 1, Stage: "draft", Disposition: "flagged", FlagReason: "hard_refusal", Attempts: 1, Detail: "provider refused"}, } states := []store.RetrievalState{{Chapter: 2, ChunkIdx: 1, NExactHits: 3, NPostcheckMiss: 1, PostcheckDetail: `[{"src":"鈴木"}]`, NStyleFlags: 2, StyleDetail: `{"yo":2}`}} if err := renderReport(&b, func() ([]store.RequestLogView, error) { return nil, nil }, func() ([]store.ChunkStatus, error) { return flags, nil }, func() ([]store.RetrievalState, error) { return states, nil }, okLedger); err != nil { t.Fatal(err) } out := b.String() for _, want := range []string{"=== FLAGS (disposition ≠ ok) ===", "hard_refusal", "=== MEMORY (retrieval-state) ===", "post-check-misses=1", `[{"src":"鈴木"}]`, "— total 2 ===", "Book ledger"} { if !strings.Contains(out, want) { t.Fatalf("report must contain %q, got:\n%s", want, out) } } if strings.Contains(out, "1 0") && strings.Contains(out, "ok") && strings.Count(out, "draft") > 1 { t.Fatalf("ok rows must not enter the flag section:\n%s", out) } } func TestRenderReportPartialOutputOnMidAuditError(t *testing.T) { // Frozen behavior (self-review №4): store reads are interleaved with printing, // a failure AFTER the first section leaves it on stdout (the audit does not go silently empty). var b strings.Builder rows := []store.RequestLogView{{TS: "t", Chapter: 1, Stage: "draft", Role: "translator", ModelActual: "m", OK: 1}} err := renderReport(&b, func() ([]store.RequestLogView, error) { return rows, nil }, func() ([]store.ChunkStatus, error) { return nil, errors.New("chunk_status corrupted") }, func() ([]store.RetrievalState, error) { t.Fatal("must not be reached"); return nil, nil }, okLedger) if err == nil || err.Error() != "chunk_status corrupted" { t.Fatalf("mid-audit error must propagate, got %v", err) } if !strings.Contains(b.String(), "draft") { t.Fatalf("the request_log table printed before the failure must remain on the writer:\n%s", b.String()) } } func TestRenderStatusJSONSchemaAndSentinel(t *testing.T) { var b strings.Builder rep := &pipeline.StatusReport{BookID: "b1", TotalUnits: 4, Done: 3, Flagged: 1} err := renderStatusJSON(&b, rep) var flagged *pipeline.CompletedWithFlags if !errors.As(err, &flagged) { t.Fatalf("--json with flags must return the exit-2 sentinel (minor 1d), got %v", err) } var decoded map[string]any if jerr := json.Unmarshal([]byte(b.String()), &decoded); jerr != nil { t.Fatalf("output must be valid JSON: %v", jerr) } if decoded["book_id"] != "b1" { t.Fatalf("stable schema field book_id missing: %v", decoded) } // Clean book → nil error → exit 0. if err := renderStatusJSON(&strings.Builder{}, &pipeline.StatusReport{TotalUnits: 4, Done: 4}); err != nil { t.Fatalf("clean book must exit 0, got %v", err) } } func TestRenderStatusHumanDriftAndAdvice(t *testing.T) { var b strings.Builder rep := &pipeline.StatusReport{ BookID: "b1", Snapshot: "abcdef0123456789", TotalUnits: 10, Done: 7, Flagged: 3, GlossaryMissFlagged: 1, SnapshotDrift: true, ConfigDrift: true, } err := renderStatusHuman(&b, rep, "book.yaml") var flagged *pipeline.CompletedWithFlags if !errors.As(err, &flagged) || flagged.Flagged != 3 { t.Fatalf("flagged status must return the exit-2 sentinel, got %v", err) } out := b.String() for _, want := range []string{ "snapshot abcdef012345", "⚠ SNAPSHOT-DRIFT", "⚠ CONFIG-DRIFT", // The advice is split (minor 1d): re-drivable separately from glossary_miss (redrive is a no-op there). "2 flagged chunk(s) need attention — re-attack: tmctl redrive --config book.yaml", "1 chunk(s) flagged by the post-check gate (glossary_miss)", } { if !strings.Contains(out, want) { t.Fatalf("status must contain %q, got:\n%s", want, out) } } } func TestRenderRedriveDryRunAndSentinel(t *testing.T) { var b strings.Builder sum := &pipeline.RedriveSummary{DryRun: true, Targets: []pipeline.RedriveTarget{ {Chapter: 1, ChunkIdx: 2, FlagReason: "cjk_artifact", Stages: []string{"draft", "edit"}}}} if err := renderRedrive(&b, "b1", sum, nil, okLedger); err != nil { t.Fatalf("dry-run must exit 0, got %v", err) } if !strings.Contains(b.String(), "[dry-run: nothing reset, no calls made]") { t.Fatalf("dry-run banner missing:\n%s", b.String()) } // No targets → informational exit 0. b.Reset() if err := renderRedrive(&b, "b1", &pipeline.RedriveSummary{}, nil, okLedger); err != nil { t.Fatal(err) } if !strings.Contains(b.String(), "nothing to re-attack") { t.Fatalf("no-targets banner missing:\n%s", b.String()) } // Live re-run that still has flags → sentinel; a flaky ledger read is // swallowed (no ledger line, exit code unchanged) — frozen behavior. b.Reset() sum = &pipeline.RedriveSummary{Targets: []pipeline.RedriveTarget{{Chapter: 1, ChunkIdx: 2, FlagReason: "x", Stages: []string{"draft"}}}} res := &pipeline.BookResult{BookID: "b1", Flagged: 1, Chunks: make([]pipeline.ChunkOutcome, 3)} err := renderRedrive(&b, "b1", sum, res, func() (float64, float64, error) { return 0, 0, errors.New("flaky") }) var flagged *pipeline.CompletedWithFlags if !errors.As(err, &flagged) { t.Fatalf("re-run with flags must return the exit-2 sentinel, got %v", err) } if strings.Contains(b.String(), "Book ledger") { t.Fatalf("flaky ledger read must swallow the ledger line, not fail:\n%s", b.String()) } } // TestRenderSignatureStopShowsTheTable pins the operator surface D39.36 specified and the CLI did not // have: the stop prints the term rows (src · proposed dst · origin · freq · spread), the draft variants // that disagree, a source context, both artifact paths, and — when the list is long — how many rows it // did NOT show. A silent truncation would read as "that was the whole bank". func TestRenderSignatureStopShowsTheTable(t *testing.T) { var rows []pipeline.BankStopRow rows = append(rows, pipeline.BankStopRow{ Src: "方源", Dst: "Фан Юань", Origin: "both", Type: "name", Freq: 12, Spread: 2, Variants: []pipeline.BankStopVariant{{Dst: "Фан Юань", Chunks: 3}, {Dst: "Фань Юань", Chunks: 1}}, Contexts: []string{"方源来到青茅山"}, }) for i := 0; i < 25; i++ { rows = append(rows, pipeline.BankStopRow{Src: fmt.Sprintf("术%d", i), Origin: "mined", Freq: 5}) } var b bytes.Buffer renderSignatureStop(&b, &pipeline.WaveSignatureStop{ Terms: len(rows), SignaturePath: "/tmp/b.db.mined-signature.yaml", TablePath: "/tmp/b.db.bank-stop.txt", Rows: rows, }) out := b.String() for _, want := range []string{ "方源", "Фан Юань", "both", "Фань Юань ×1", "方源来到青茅山", "/tmp/b.db.mined-signature.yaml", "/tmp/b.db.bank-stop.txt", "6 more term(s)", "--verify-bank", } { if !strings.Contains(out, want) { t.Fatalf("the stop banner must carry %q:\n%s", want, out) } } // An unconsolidated term prints an explicit dash, never an empty column that reads as a rendering. var b2 bytes.Buffer renderSignatureStop(&b2, &pipeline.WaveSignatureStop{ Terms: 1, SignaturePath: "p", Rows: []pipeline.BankStopRow{{Src: "花海", Origin: "mined", Freq: 5}}, }) if !strings.Contains(b2.String(), "—") { t.Fatalf("a term with no proposed dst must show a dash:\n%s", b2.String()) } } // TestRenderSignatureStopOrdersTheReviewLeastSureFirst pins the ONE use the role's stated confidence is // allowed to have (D39.102): it orders the review list, an ordinal inside one model's reply, and nothing // else. Without it the number is printed and never used — a column that claims to answer «what do I read // first» and does not. The full sidecar keeps source-key order; only this capped view is the review surface. func TestRenderSignatureStopOrdersTheReviewLeastSureFirst(t *testing.T) { rows := []pipeline.BankStopRow{ {Src: "уверенный", Dst: "д", Origin: "mined", Freq: 9, Conf: 95}, {Src: "неконсолидированный", Origin: "mined", Freq: 9, Conf: -1}, {Src: "неуверенный", Dst: "д", Origin: "mined", Freq: 9, Conf: 20}, {Src: "безмолвный", Dst: "д", Origin: "mined", Freq: 9, Conf: -1}, } var b bytes.Buffer renderSignatureStop(&b, &pipeline.WaveSignatureStop{Terms: len(rows), SignaturePath: "p", Rows: rows}) out := b.String() order := []string{"неуверенный", "уверенный", "безмолвный", "неконсолидированный"} at := -1 for _, name := range order { i := strings.Index(out, name) if i < 0 { t.Fatalf("row %q missing:\n%s", name, out) } if i < at { t.Fatalf("review order broken at %q — want least-confident first, then silence, then unconsolidated:\n%s", name, out) } at = i } if !strings.Contains(out, "least-confident first") { t.Fatalf("the banner must say what its order means, or a sorted table reads as arbitrary:\n%s", out) } } // TestRenderTranslateNamesTheVolumeCeiling pins the operator-facing half of the "the stop must NAME its // ceiling" rule. A volume-bounded run exits 0 like any completed run, so without this line an operator // looking at "chunks 2" on a hundred-unit book cannot tell "the volume I bought is finished" from // "something went quietly wrong" — the silent-limit complaint the design took from OpenHands. The line // has to carry all three actionable numbers and to say it is a completion, not a pause. func TestRenderTranslateNamesTheVolumeCeiling(t *testing.T) { var b strings.Builder res := &pipeline.BookResult{BookID: "b1", TotalUSD: 0.01, Volume: &pipeline.VolumeStop{MaxUnits: 2, Delivered: 1, Reworked: 1, Free: 3, LeftFresh: 5, LeftRework: 2}} if err := renderTranslate(&b, res, okLedger); err != nil { t.Fatal(err) } out := b.String() for _, want := range []string{"VOLUME CEILING", "--max-units 2", "2 paying output unit(s)", "1 NEW unit(s) delivered", "1 already-delivered unit(s) re-made", "3 rode along at $0", "5 unit(s) NEVER delivered", "2 already delivered but not yet re-made", "COMPLETION, not a pause", // The invitation must be about the units that were never delivered, and only those. "5 output unit(s) of this book have never been delivered"} { if !strings.Contains(out, want) { t.Errorf("the volume stop must say %q; got:\n%s", want, out) } } // A run that was NOT bounded must say nothing at all — otherwise every ordinary run reads as cut short. var b2 strings.Builder if err := renderTranslate(&b2, &pipeline.BookResult{BookID: "b1"}, okLedger); err != nil { t.Fatal(err) } if strings.Contains(b2.String(), "VOLUME CEILING") { t.Errorf("an unbounded run must not mention a ceiling:\n%s", b2.String()) } } // TestRenderTranslateNeverSellsWhatIsAlreadyOwned pins the operator-facing half of the same defect. On a // book every unit of which has been delivered, the stop must not invite a purchase: the units it held // back are unrefreshed, not unbought, and the first version of this line offered them as "run again with // --max-units to buy more". func TestRenderTranslateNeverSellsWhatIsAlreadyOwned(t *testing.T) { var b strings.Builder res := &pipeline.BookResult{BookID: "b1", Volume: &pipeline.VolumeStop{MaxUnits: 1, Delivered: 0, Reworked: 1, LeftFresh: 0, LeftRework: 3}} if err := renderTranslate(&b, res, okLedger); err != nil { t.Fatal(err) } out := b.String() if strings.Contains(out, "have never been delivered") { t.Fatalf("the stop offered chapters the reader already owns:\n%s", out) } for _, want := range []string{ "every unit of this book HAS been delivered", "That is a re-pass, not a purchase of new book", "delivered NO new unit", } { if !strings.Contains(out, want) { t.Errorf("want %q in:\n%s", want, out) } } } // TestRenderTranslateReportsBothRemaindersWhenBothExist pins the case an if/else would have swallowed: // a book with BOTH never-delivered units and delivered-but-superseded ones. Printing only the first // leaves an operator who has both believing the one number was the whole picture. func TestRenderTranslateReportsBothRemaindersWhenBothExist(t *testing.T) { var b strings.Builder res := &pipeline.BookResult{BookID: "b1", Volume: &pipeline.VolumeStop{MaxUnits: 2, Delivered: 2, LeftFresh: 4, LeftRework: 6}} if err := renderTranslate(&b, res, okLedger); err != nil { t.Fatal(err) } out := b.String() if !strings.Contains(out, "4 output unit(s) of this book have never been delivered") { t.Errorf("the buyable remainder is missing:\n%s", out) } if !strings.Contains(out, "6 already-delivered unit(s) carry a superseded snapshot") { t.Errorf("the re-make remainder was dropped — an if/else over the two would do exactly this:\n%s", out) } // A run that DID deliver must not carry the "delivered NO new unit" warning. if strings.Contains(out, "delivered NO new unit") { t.Errorf("this run delivered 2 units; it must not claim otherwise:\n%s", out) } } // TestTheRePaymentHintDoesNotPromiseWhatTheGrantWillNotDo pins the acceptance hunter's finding 6. The // hint offers rebill_output_units as "the granularity --max-units counts in", which is true and, on its // own, misleading: the grant goes to NEVER-DELIVERED units first, so on a book that still has any, sizing // --max-units from the re-payment figure delivers new units and re-makes nothing. The caveat lives in the // code; it has to live where the operator reads. func TestTheRePaymentHintDoesNotPromiseWhatTheGrantWillNotDo(t *testing.T) { var b strings.Builder rep := &pipeline.StatusReport{BookID: "b1", ConfigDrift: true, RebillUnits: 9, RebillOutputUnits: 3, RebillUSD: 0.02, RebillBasis: pipeline.RebillBasisPending} if err := renderStatusHuman(&b, rep, ""); err != nil { t.Fatal(err) } out := b.String() if !strings.Contains(out, "3 OUTPUT unit(s)") { t.Fatalf("the sale-sized figure must be shown:\n%s", out) } if !strings.Contains(out, "does NOT buy those units back") { t.Fatalf("the hint must say the grant goes to never-delivered units first, or it promises a re-pass it will not perform:\n%s", out) } } // TestTheRePaymentLineCarriesItsBasis pins the figure's basis on the surface an operator reads: what // rebill_usd is made of, that it errs upward, and how much of it is priced across a model move or carried // at last season's money — in the consent gate's own words (StatusReport.RebillFigureBasis), so the status // line and the refusal `translate` prints describe one figure in one wording (D39.150 п.1, D39.181 п.1). // // Mutation this catches: drop RebillFigureBasis() from the RE-PAYMENT line and both assertions fire. func TestTheRePaymentLineCarriesItsBasis(t *testing.T) { var b strings.Builder rep := &pipeline.StatusReport{BookID: "b1", ConfigDrift: true, RebillUnits: 9, RebillOutputUnits: 3, RebillUSD: 0.02, RebillBasis: pipeline.RebillBasisPending, RebillHistoricalRows: 2, RebillModelMovedRows: 1} if err := renderStatusHuman(&b, rep, ""); err != nil { t.Fatal(err) } out := b.String() if !strings.Contains(out, rep.RebillFigureBasis()) { t.Fatalf("the RE-PAYMENT line must carry the figure's basis in the gate's own words:\n%s", out) } for _, want := range []string{"errs upward", "1 of the 9 unit(s) were bought from a model their stage no longer resolves to", "2 of the 9 unit(s) carry, wholly or in part, the amount they were originally billed at"} { if !strings.Contains(out, want) { t.Errorf("the RE-PAYMENT line must say %q:\n%s", want, out) } } } // TestDriftDoesNotClaimARePaymentItNeverComputed is the trap the orchestrator named when he ratified A7: // the config-drift flag used to end its own sentence with «= re-paying for the book», which is a claim // about MONEY made by a boolean that computes none. // // The two come apart exactly when status learned to see a DROPPED stage (backlog row 239): projectRebill // skips rows whose stage the current pipeline no longer runs, so that drift is real and its re-payment is // genuinely zero. Under the old sentence the operator was told he would re-pay for a book that will not be // re-paid for — one lie replaced by another, which is what «правка A7 идёт ВМЕСТЕ с базисом» was about. // // Mutation this catches: put the money clause back on the drift line and the first assertion fires. func TestDriftDoesNotClaimARePaymentItNeverComputed(t *testing.T) { rep := &pipeline.StatusReport{ BookID: "b", TotalUnits: 2, Done: 2, ConfigDrift: true, ConfigDriftBasis: pipeline.DriftBasisDrift, RebillUnits: 0, RebillUSD: 0, RebillBasis: pipeline.RebillBasisPending, } var b strings.Builder _ = renderStatusHuman(&b, rep, "book.yaml") out := b.String() if !strings.Contains(out, "⚠ CONFIG-DRIFT") { t.Fatalf("the drift itself must still be announced:\n%s", out) } if strings.Contains(out, "re-paying for the book") { t.Fatalf("the drift flag must not assert a spend it never computed — the projection says zero "+ "here, and the money lines are what speak about money:\n%s", out) } if !strings.Contains(out, "NOTHING already billed is re-paid") { t.Fatalf("drift with a zero re-payment is a real state and must be named, not left to be inferred "+ "from a missing line:\n%s", out) } } // TestAnUnknownDriftBasisIsSaidOutLoud is the A12 half at the surface an operator actually reads: a // `config_drift:false` that means «could not check» must not read as «checked, clean». func TestAnUnknownDriftBasisIsSaidOutLoud(t *testing.T) { rep := &pipeline.StatusReport{ BookID: "b", TotalUnits: 2, Done: 1, ConfigDrift: false, ConfigDriftBasis: pipeline.DriftBasisUnknown, RebillBasis: pipeline.RebillBasisPending, } var b strings.Builder _ = renderStatusHuman(&b, rep, "book.yaml") out := b.String() if !strings.Contains(out, "CONFIG-DRIFT UNKNOWN") { t.Fatalf("an unestablished drift verdict must say so; silence here reads as «no drift»:\n%s", out) } clean := &pipeline.StatusReport{ BookID: "b", TotalUnits: 2, Done: 1, ConfigDrift: false, ConfigDriftBasis: pipeline.DriftBasisNone, RebillBasis: pipeline.RebillBasisPending, } var cb strings.Builder _ = renderStatusHuman(&cb, clean, "book.yaml") if strings.Contains(cb.String(), "CONFIG-DRIFT UNKNOWN") { t.Fatal("a book whose drift WAS checked and is clean must not carry the unknown caveat") } } // TestTheHumanManifestCallsTheBookLevelFigureACeiling pins the one line an operator reads before // deciding to buy, and it exists because acceptance (F3) found the previous wording claiming the // opposite of what the number is. // // ⛔ THE BOOK-LEVEL PART IS A BOUND, NOT A FORECAST. The terminology passes are limited by their // configured budgets and their input does not exist until the drafts do, so the engine publishes what it // will NOT exceed. On a book of a few chapters that figure can dominate the total, and an operator // reading «$X of it is book-level and paid once» as a bill concludes a short book is dearer per chapter // than a long one — which is backwards, and is a conclusion drawn from OUR sentence, not from the number. // // ⚠ The line is also ABSENT when there is nothing to bound: a configuration with no terminology phase // prices no book-level spend, and printing «$0.000000 is a ceiling» would invent a phase that never runs. func TestTheHumanManifestCallsTheBookLevelFigureACeiling(t *testing.T) { price := &pipeline.BookPrice{ExpectedUSD: 3.5, BookOnceUSD: 2.0, StepMaxUSD: 0.25, SourceChars: 4000} m := &pipeline.BookManifest{Structure: "detected", Price: price} var b bytes.Buffer if err := renderManifest(&b, m, "book.manifest.json", false); err != nil { t.Fatalf("renderManifest: %v", err) } out := b.String() // The total says what it is and nothing more: the old form folded the bound into the same sentence // as the expectation, which is where the two got confused. if !strings.Contains(out, "expected cost: $3.500000 for the whole book (4000 source chars)") { t.Errorf("the total must name itself and the size it was computed from:\n%s", out) } if strings.Contains(out, "paid once)") { t.Errorf("the bound must not ride inside the total's parentheses — that is the wording F3 refused:\n%s", out) } // The bound says it is a bound, in the operator's own words rather than by implication. if !strings.Contains(out, "$2.000000") || !strings.Contains(out, "CEILING") { t.Errorf("the book-level figure must be named a ceiling:\n%s", out) } if !strings.Contains(out, "not a forecast") { t.Errorf("a reader must not have to infer that a bound is not a bill:\n%s", out) } // And the number that decides whether ANY ceiling admits work stays on its own line. if !strings.Contains(out, "smallest workable ceiling: $0.250000") { t.Errorf("the largest indivisible reservation must survive the rewording:\n%s", out) } // ⛔ NOTHING TO BOUND ⇒ NOTHING SAID. A run with no terminology phase must not be told about a // ceiling on passes it will never make. var b2 bytes.Buffer m2 := &pipeline.BookManifest{Structure: "none", Price: &pipeline.BookPrice{ExpectedUSD: 1.5, StepMaxUSD: 0.25, SourceChars: 4000}} if err := renderManifest(&b2, m2, "book.manifest.json", false); err != nil { t.Fatalf("renderManifest: %v", err) } if strings.Contains(b2.String(), "CEILING") { t.Errorf("a book with no book-level spend must not be told about a ceiling on it:\n%s", b2.String()) } } // TestRenderSignatureStopShowsBothDisagreements pins the review screen's two markers together. They are // different decisions for the owner — the run contradicting itself, and the book already rendering the // surface another way — and a screen that prints one and hides the other sends him to the sidecar to find // out that the term he is signing is already in the bank. func TestRenderSignatureStopShowsBothDisagreements(t *testing.T) { rows := []pipeline.BankStopRow{{ Src: "方源", Dst: "Фан Юань", Origin: "banknote", Freq: 9, Conf: 10, Contradicts: []string{`方源空窍→"Пустота Фан Юаня"`}, BankHolds: []string{`unsigned draft "方源"→"Странник" [ch 1..20]`}, }} var b bytes.Buffer renderSignatureStop(&b, &pipeline.WaveSignatureStop{Terms: len(rows), SignaturePath: "p", Rows: rows}) out := b.String() for _, want := range []string{"contradicts:", "Пустота Фан Юаня", "bank holds:", `unsigned draft "方源"→"Странник"`} { if !strings.Contains(out, want) { t.Errorf("the review screen must carry %q:\n%s", want, out) } } // CONTROL: a row with neither marker prints neither, so the assertions above are the fields answering. var quiet bytes.Buffer renderSignatureStop(&quiet, &pipeline.WaveSignatureStop{Terms: 1, SignaturePath: "p", Rows: []pipeline.BankStopRow{{Src: "方源", Dst: "Фан Юань", Origin: "banknote", Freq: 9, Conf: 10}}}) if strings.Contains(quiet.String(), "contradicts:") || strings.Contains(quiet.String(), "bank holds:") { t.Errorf("control: an unmarked row must print neither marker:\n%s", quiet.String()) } }