77 lines
2.7 KiB
Go
77 lines
2.7 KiB
Go
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))
|
||
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)
|
||
}
|
||
}
|