textmachine/backend/internal/pipeline/status_rebill_projection_test.go

99 lines
4.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

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

package pipeline
import (
"context"
"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)
}
}