package pipeline import ( "context" "fmt" "strings" "testing" "textmachine/backend/internal/obs" ) // status_rebill_projection_test.go: `tmctl status` prices the drift (spec D15.2 §9, taken with the // pre-run hygiene pack 25.07). The boolean ConfigDrift told the operator that the config moved but not // whether continuing costs a cent or the whole book — and the number that answers that already existed // on the refusal path. The claim under test is that status quotes THE SAME number the consent gate // refuses on: two definitions of one quantity is the exact bug projectBookUSD was extracted to remove. // TestStatusPricesTheDriftWithTheGatesOwnNumber runs a book, drifts both waves, and asserts that the // read-only status projection equals what was actually billed AND what the gate names when it refuses. func TestStatusPricesTheDriftWithTheGatesOwnNumber(t *testing.T) { rec := &reqRec{} srv := newJSONProvider(rec, draftEdit) defer srv.Close() bookPath := setupProject(t, srv.URL) ctx := obs.WithReqInfo(context.Background(), obs.ReqInfo{TraceID: obs.NewTraceID()}) r1 := newRunner(t, bookPath) if _, err := r1.TranslateBook(ctx); err != nil { t.Fatal(err) } committed, _, err := r1.Store.SpentUSD("test-book") if err != nil { t.Fatal(err) } r1.Close() // Before any drift the projection must be SILENT: everything resumes at $0, so a status that // reported a re-payment here would be crying wolf on every healthy book. rClean := newRunner(t, bookPath) cleanRep, err := rClean.Status(ctx) rClean.Close() if err != nil { t.Fatal(err) } if cleanRep.RebillUnits != 0 || cleanRep.RebillUSD != 0 { t.Fatalf("an undrifted book must project no re-payment, got %d units / $%v", cleanRep.RebillUnits, cleanRep.RebillUSD) } driftPipelineVersion(t, bookPath) r2 := newRunner(t, bookPath) defer r2.Close() rep, err := r2.Status(ctx) if err != nil { t.Fatal(err) } if !rep.ConfigDrift { t.Fatal("setup: the drift itself was not detected") } if rep.RebillUnits != 2 { // 1 chunk × (draft + edit) t.Errorf("rebill_units = %d, want 2 (both stages of the single chunk)", rep.RebillUnits) } // Every billed row is superseded here, so the projection is the whole spend of run 1. if diff := rep.RebillUSD - committed; diff > 1e-9 || diff < -1e-9 { t.Errorf("rebill_usd = %v, want the whole billed amount %v", rep.RebillUSD, committed) } // The same number, from the enforcement side: the gate's refusal must quote what status showed. err = r2.checkRebillConsent(ctx, chunksOf(t, r2), nil) if err == nil { t.Fatal("the gate must refuse a fully-superseded book without consent") } if want := fmt.Sprintf("~$%.6f", rep.RebillUSD); !strings.Contains(err.Error(), want) { t.Errorf("the refusal does not quote the projection %s:\n%v", want, err) } // ⚠ AND THE SAME AGAIN UNDER A VOLUME CEILING, because that is where the two sides could drift apart // and status.go's docstring promises out loud that they cannot: "status can never quote a different // number than the one translate enforces". A bounded run re-pays only its own slice, but the question // the THRESHOLD answers is about the book — so the figure the refusal quotes must still be the book's, // which is the one status reports. Passing nil here (the shape this pin had while the pack was being // built) tests only the unbounded case and would miss a divergence entirely. // A scope that ADMITS the book's single unit — i.e. a real bounded purchase that genuinely re-pays. // (A scope admitting nothing is a different case and is handled before the threshold: a run that // re-pays nothing has no concrete spend to consent to.) bounded := &volumeScope{ admitted: map[chunkKey]bool{{1, 0}: true}, leader: map[chunkKey]bool{{1, 0}: true}, stop: VolumeStop{MaxUnits: 1}, } err = r2.checkRebillConsent(ctx, chunksOf(t, r2), bounded) if err == nil { t.Fatal("a bounded run over a fully-superseded book must still meet the gate: a threshold a caller can shrink by splitting is not a threshold") } if want := fmt.Sprintf("~$%.6f", rep.RebillUSD); !strings.Contains(err.Error(), want) { t.Errorf("under a volume ceiling the refusal stopped quoting the BOOK figure status reports (%s):\n%v", want, err) } }