textmachine/backend/internal/pipeline/paidtail_test.go

200 lines
10 KiB
Go

package pipeline
import (
"context"
"testing"
"textmachine/backend/internal/obs"
"textmachine/backend/internal/store"
)
// paidtail_test.go: A0 — what the money bought, and specifically NOT the query that looks obvious.
// TestPaidTailSplitsMoneyByWhatItBought pins the three classes on a hand-built ledger, because the
// classification is pure arithmetic over insertion order and deserves to be tested without a book.
func TestPaidTailSplitsMoneyByWhatItBought(t *testing.T) {
// ch1/chunk0/draft: paid twice — a retry. The first is superseded by the second, which shipped.
// ch2/chunk0/draft: paid once, and its row carries no final hash: it bought nothing shippable.
// ch2/chunk0/edit : a $0 derived projection — not a call, and must not appear anywhere.
usage := []store.CheckpointUsage{
{Chapter: 1, ChunkIdx: 0, Stage: "draft", CostUSD: 0.10},
{Chapter: 1, ChunkIdx: 0, Stage: "draft", CostUSD: 0.20},
{Chapter: 2, ChunkIdx: 0, Stage: "draft", CostUSD: 0.05},
{Chapter: 2, ChunkIdx: 0, Stage: "edit", CostUSD: 0},
}
statuses := []store.ChunkStatus{
{Chapter: 1, ChunkIdx: 0, Stage: "draft", FinalHash: "h1"},
{Chapter: 2, ChunkIdx: 0, Stage: "draft", FinalHash: ""},
}
got := paidTail(usage, statuses)
if got.ShippedUSD != 0.20 || got.ShippedCalls != 1 {
t.Errorf("the call that stands and ships is the only shipped money: %+v", got)
}
if got.SupersededUSD != 0.10 || got.SupersededCalls != 1 {
t.Errorf("the earlier paid call at the same position was replaced: %+v", got)
}
if got.WithheldUSD != 0.05 || got.WithheldCalls != 1 {
t.Errorf("a position whose last paid call left no final hash bought nothing shippable: %+v", got)
}
// Float addition, so the comparison is a tolerance rather than an equality — the assertion is that
// the three classes are EXHAUSTIVE over paid checkpoints, not that IEEE-754 sums in a nice order.
if d := got.TotalUSD - 0.35; d > 1e-9 || d < -1e-9 {
t.Errorf("the three classes must be exhaustive over PAID checkpoints: %+v", got)
}
if d := (got.ShippedUSD + got.SupersededUSD + got.WithheldUSD) - got.TotalUSD; d > 1e-9 || d < -1e-9 {
t.Errorf("the parts must sum to the whole: %+v", got)
}
if got.WorstPosition != "ch1/chunk0/draft" || got.WorstUSD != 0.10 {
t.Errorf("the largest single loss must be a place, not only a number: %+v", got)
}
}
// TestTheDecompositionIsNotTheOkColumn is the finding that shaped this surface, reproduced as the shape
// the cold run actually had.
//
// The obvious slice — `request_log WHERE ok = 0 AND cost_usd > 0` — selected three different things there:
// classifier calls that SUCCEEDED (flagged only because the CJK-echo rule is exempt for the terminologist
// and not for the classifier, so a bilingual term table reads as an echo of its own source), an editor
// call whose text SHIPPED after a cosmetic strip, and money that really did buy nothing. A surface built
// on it would have reported 29.2% more loss than there was — a money figure with a false cause.
//
// Here both of those rows are present and BOTH must land in `shipped`, because both bought bytes that
// stand: the classification asks what the money bought, never what a verdict column says about it.
func TestTheDecompositionIsNotTheOkColumn(t *testing.T) {
usage := []store.CheckpointUsage{
{Chapter: 0, ChunkIdx: 0, Stage: "terminology", CostUSD: 0.009}, // classifier: flagged, succeeded
{Chapter: 1, ChunkIdx: 0, Stage: "edit", CostUSD: 0.018}, // editor: flagged, text shipped
}
statuses := []store.ChunkStatus{
// The classifier writes NO chunk_status row at all (it is a bank role) — so its position is
// unknown to `shipped`, and the classification must not therefore call its money a loss on the
// strength of a missing row it was never going to have.
{Chapter: 1, ChunkIdx: 0, Stage: "edit", FinalHash: "stripped-hash"},
}
got := paidTail(usage, statuses)
if got.ShippedUSD != 0.018 {
t.Errorf("a cosmetic strip still SHIPS its text, so its money is not a loss: %+v", got)
}
// ⛔ THE ASSERTION THAT USED TO STAND HERE WAS WRONG, and it pinned the defect rather than the cure:
// it required the classifier's SUCCESSFUL call to be reported as `withheld`, on the reasoning that a
// bank role «cannot be shown to have shipped». That is a category error dressed as caution — the bank
// roles buy the book's TERMINOLOGY and never a chunk of its text, so «did it ship» is undefined for
// them, not false. Left as it was, the surface called the whole glossary contour a loss and overstated
// by 59.8% on the cold run's own ledger — worse than the naive `ok = 0` slice this file rejects.
// Found by the acceptance; the CHANGE OF THIS ASSERTION is sanctioned explicitly by the orchestrator
// (the test pinned behaviour the acceptance ruled incorrect), and that sanction is recorded in the
// pack report so it is not read as a test bent to fit a fix.
if got.BankUSD != 0.009 || got.BankCalls != 1 {
t.Errorf("a bank role's standing call bought the BOOK'S TERMINOLOGY — a product, not a loss: %+v", got)
}
if got.WithheldUSD != 0 {
t.Errorf("nothing here bought nothing: %+v", got)
}
if got.LostUSD() != 0 {
t.Errorf("both calls bought something real, so the reported loss must be zero: %+v", got)
}
if got.SupersededUSD != 0 {
t.Errorf("nothing was replaced here: %+v", got)
}
}
// TestTheReportPublishesWhatTheMoneyBought is the end-to-end half: the split must reach the $0 surface an
// operator reads, not merely exist in a struct.
//
// Mutation this catches: drop the paidTail call from Quality() and the report goes back to publishing a
// TOTAL and nothing about what it bought — which is the defect.
func TestTheReportPublishesWhatTheMoneyBought(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()
if _, err := r.TranslateBook(ctx); err != nil {
t.Fatal(err)
}
q, err := r.QualityReport()
if err != nil {
t.Fatal(err)
}
if q.PaidTail == nil {
t.Fatal("a book that spent money must publish what that money bought — the total alone is the " +
"surface the cold run found blind (A0)")
}
if q.PaidTail.TotalUSD <= 0 || q.PaidTail.ShippedUSD <= 0 {
t.Fatalf("a clean book's money bought shipped text: %+v", *q.PaidTail)
}
// And the decomposition must agree with the ledger it claims to decompose — otherwise it is a second,
// drifting definition of the book's spend.
committed, _, err := r.Store.SpentUSD("test-book")
if err != nil {
t.Fatal(err)
}
if diff := q.PaidTail.TotalUSD - committed; diff > 1e-9 || diff < -1e-9 {
t.Fatalf("the decomposition must sum to the committed ledger, got %.9f vs %.9f", q.PaidTail.TotalUSD, committed)
}
}
// TestAHealthyGlossaryPassIsNotReportedAsALoss is the acceptance's blocker, turned into a landing.
//
// The bank roles are checkpointed under a synthetic stage at chapter 0 and write NO chunk_status row, so
// the first version of this surface could never see them as shipped and put the WHOLE terminology contour
// into «bought nothing shippable». Measured on the cold run's ledger that is $0.04980482 of a $0.133
// «loss» — a 59.8% overstatement, worse than the naive `ok = 0` slice this file was built to replace. A
// live probe on a HEALTHY book printed «85.7% did NOT become shipped text» about a glossary that worked.
//
// Mutation this catches: drop the `u.Stage == terminologyStageName` arm and the contour falls back into
// withheld → LostUSD becomes non-zero on a book where nothing was lost → RED.
func TestAHealthyGlossaryPassIsNotReportedAsALoss(t *testing.T) {
// One clean draft+edit position that ships, and one bank-role call that stands. Nothing was lost.
usage := []store.CheckpointUsage{
{Chapter: 1, ChunkIdx: 0, Stage: "draft", CostUSD: 0.02},
{Chapter: 1, ChunkIdx: 0, Stage: "edit", CostUSD: 0.03},
{Chapter: 0, ChunkIdx: 0, Stage: terminologyStageName, CostUSD: 0.011},
{Chapter: 0, ChunkIdx: 1, Stage: terminologyStageName, CostUSD: 0.009},
}
statuses := []store.ChunkStatus{
{Chapter: 1, ChunkIdx: 0, Stage: "draft", FinalHash: "d"},
{Chapter: 1, ChunkIdx: 0, Stage: "edit", FinalHash: "e"},
}
got := paidTail(usage, statuses)
if got.LostUSD() != 0 {
t.Fatalf("this book lost NOTHING: every call bought either text or terminology. Reporting the "+
"glossary as a loss is the false-cause defect this surface exists to remove: %+v", got)
}
if d := got.BankUSD - 0.02; d > 1e-9 || d < -1e-9 || got.BankCalls != 2 {
t.Fatalf("both bank-role calls stand and bought the book's terminology: %+v", got)
}
if d := got.ShippedUSD - 0.05; d > 1e-9 || d < -1e-9 {
t.Fatalf("the two text positions shipped: %+v", got)
}
if got.WorstPosition != "" {
t.Fatalf("there is no «largest single loss» on a book with no losses — naming one would point an "+
"operator at a healthy glossary batch: %q at $%.6f", got.WorstPosition, got.WorstUSD)
}
if d := (got.ShippedUSD + got.BankUSD + got.SupersededUSD + got.WithheldUSD) - got.TotalUSD; d > 1e-9 || d < -1e-9 {
t.Fatalf("the four classes must still be exhaustive: %+v", got)
}
}
// TestAReboughtGlossaryBatchIsStillALoss keeps the new class from becoming an amnesty: the terminology
// contour is re-bought in full whenever the drafted set grows (backlog row 233), and that money genuinely
// bought nothing the second time. Only the call that STANDS is the bank's product.
func TestAReboughtGlossaryBatchIsStillALoss(t *testing.T) {
usage := []store.CheckpointUsage{
{Chapter: 0, ChunkIdx: 0, Stage: terminologyStageName, CostUSD: 0.011}, // first purchase
{Chapter: 0, ChunkIdx: 0, Stage: terminologyStageName, CostUSD: 0.013}, // re-bought, replaces it
}
got := paidTail(usage, nil)
if d := got.SupersededUSD - 0.011; d > 1e-9 || d < -1e-9 || got.SupersededCalls != 1 {
t.Fatalf("the replaced batch bought nothing that survives: %+v", got)
}
if d := got.BankUSD - 0.013; d > 1e-9 || d < -1e-9 || got.BankCalls != 1 {
t.Fatalf("only the standing batch is the bank's product: %+v", got)
}
if d := got.LostUSD() - 0.011; d > 1e-9 || d < -1e-9 {
t.Fatalf("a re-bought contour IS a loss, and the class must not amnesty it: %+v", got)
}
}