218 lines
10 KiB
Go
218 lines
10 KiB
Go
package pipeline
|
||
|
||
import (
|
||
"context"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"testing"
|
||
|
||
"textmachine/backend/internal/obs"
|
||
"textmachine/backend/internal/store"
|
||
)
|
||
|
||
// bankrolemoney_test.go: backlog row 194 — the money of the BANK ROLES was outside every projection an
|
||
// operator decides on.
|
||
//
|
||
// The terminologist and the classifier are checkpointed under a synthetic stage at chapter 0 and write no
|
||
// chunk_status row, so any derivation that walks chunk_status cannot see them. On the cold run that was
|
||
// $0.04980482 of $0.43610966 — 11.4% of the run, invisible in the figure shown before deciding to buy more.
|
||
|
||
// TestTheBankContourEntersTheProjectionAndNotTheLedger is the landing AND the proof the ratification was
|
||
// conditioned on: the fix must move the PROJECTION and must not move one micro-dollar of anything the
|
||
// platform reads.
|
||
//
|
||
// The two halves are asserted separately because they are separate risks. `committed_usd` is summed from
|
||
// `spend`, and the platform meters against it (`bookCap = committed + increment`); the money of the bank
|
||
// roles was ALWAYS in there, because spend and checkpoints are written in one transaction. What was blind
|
||
// was the projection alone — so a correct fix changes exactly one of these two numbers.
|
||
//
|
||
// Mutation this catches: drop the `+ r.bankRoleCommittedUSD()` addend and the projection stops covering
|
||
// the contour → the first assertion fires with the exact shortfall.
|
||
func TestTheBankContourEntersTheProjectionAndNotTheLedger(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)
|
||
}
|
||
|
||
// The fixture runs no terminologist, so the contour is planted directly on its own axis — the same
|
||
// place the role writes: a checkpoint under the synthetic stage at chapter 0, with NO chunk_status row.
|
||
// Planting it is what makes the blindness observable at all; a book without bank-role money cannot
|
||
// distinguish a projection that includes the class from one that omits it.
|
||
if err := r.Store.UpsertSnapshot("snap-terminology", "brief", `{"k":1}`); err != nil {
|
||
t.Fatal(err) // jobs reference snapshots; the row has to exist before a job can point at it
|
||
}
|
||
job, err := r.Store.EnsureJob("test-book", 0, terminologyStageName, "snap-terminology")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
const contour = 0.05
|
||
res, verdict, err := r.Store.Reserve("test-book", contour, store.Ceilings{BookUSD: 100, DayUSD: 100})
|
||
if err != nil || verdict != store.ReserveOK {
|
||
t.Fatalf("reserve for the planted bank-role call: %v %v", verdict, err)
|
||
}
|
||
// The REAL money path — reserve → settle+checkpoint — because that is what the role does, and using
|
||
// it is what makes the second half of this test meaningful: the contour lands in `spend` AND in
|
||
// `checkpoints`, exactly as it always has, and the question is only whether the PROJECTION sees it.
|
||
if err := r.Store.SettleWithCheckpoint(res, contour, store.Checkpoint{
|
||
RequestHash: "planted-terminology-batch-0", JobID: job.ID, ChunkIdx: 0,
|
||
Stage: terminologyStageName, Role: roleTerminologist,
|
||
ModelRequested: "fake-model", ModelActual: "fake-model",
|
||
ResponseText: "терм\tterm", UsageJSON: "{}", CostUSD: contour, FinishReason: "stop",
|
||
}, nil); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
before, err := r.Status(ctx)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
// (1) THE PROJECTION MUST COVER IT. The per-unit walk cannot reach a chapter-0 synthetic stage, so a
|
||
// projection that still omits the contour is the defect row 194 names.
|
||
if before.ProjectedBookUSD < contour {
|
||
t.Fatalf("the book has spent $%.6f on its bank roles and the projection an operator decides on is "+
|
||
"$%.6f — a whole class of money missing from the number that invites the next purchase (row 194)",
|
||
contour, before.ProjectedBookUSD)
|
||
}
|
||
|
||
// (2) AND THE LEDGER MUST NOT MOVE. committed_usd is what the platform meters against; this fix is a
|
||
// correction of a projection and must not touch it. The invariant is checked directly rather than
|
||
// trusted: committed is summed from `spend`, and the planted row went to `checkpoints` alone.
|
||
committed, _, err := r.Store.SpentUSD("test-book")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if before.CommittedUSD != committed {
|
||
t.Fatalf("status must report the ledger's own committed figure and nothing else: %.9f vs %.9f",
|
||
before.CommittedUSD, committed)
|
||
}
|
||
usage, err := r.Store.CheckpointUsageForBook("test-book")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
var checkpointed float64
|
||
for _, u := range usage {
|
||
checkpointed += u.CostUSD
|
||
}
|
||
// ⛔ THE CONDITION THE RATIFICATION WAS GIVEN UNDER, asserted rather than argued: the ledger figure the
|
||
// platform meters against is `SUM(checkpoints)` to the last micro-dollar, and it ALREADY held this
|
||
// money — spend and checkpoints are written in one transaction. So the bank contour was never missing
|
||
// from what the account is charged; it was missing from the FORECAST. A fix that moved this equality
|
||
// would be a contract change and would have to stop and ask.
|
||
if d := checkpointed - committed; d > 1e-9 || d < -1e-9 {
|
||
t.Fatalf("committed_usd must remain SUM(checkpoints) — the fix corrects a projection and must not "+
|
||
"touch a figure the platform reads: checkpoints $%.9f, committed $%.9f", checkpointed, committed)
|
||
}
|
||
// And the projection is strictly LARGER than the ledger here, which is the shape a forecast should
|
||
// have and the shape it did not have while a whole class of spend was invisible to it.
|
||
if before.ProjectedBookUSD < committed {
|
||
t.Fatalf("a book forecast below what the book has already been charged is not a forecast: "+
|
||
"projected $%.6f, committed $%.6f", before.ProjectedBookUSD, committed)
|
||
}
|
||
}
|
||
|
||
// TestABookWithNoBankRolesProjectsExactlyAsBefore keeps the fix from being a silent re-definition of the
|
||
// figure for every book: with no bank-role spend the addend is zero, so nothing about the ordinary book's
|
||
// projection moves.
|
||
func TestABookWithNoBankRolesProjectsExactlyAsBefore(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)
|
||
}
|
||
if got := r.bankRoleCommittedUSD(); got != 0 {
|
||
t.Fatalf("this fixture runs no bank role, so the addend must be exactly zero, got %.9f", got)
|
||
}
|
||
rep, err := r.Status(ctx)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if rep.ProjectedBookUSD <= 0 {
|
||
t.Fatal("a translated book still projects a cost")
|
||
}
|
||
}
|
||
|
||
// TestTheContourDoesNotWeakenTheConsentGate is the orchestrator's decision (31.08), pinned.
|
||
//
|
||
// The bank contour belongs in «what will this book cost me» and NOT in the base of the re-payment consent
|
||
// threshold. The threshold is 5% of the projected book cost and it governs RE-PAYMENT: raising its base
|
||
// with money that is never re-paid by a snapshot move raises the bar for ASKING without adding anything
|
||
// the bar is about — a money gate made quietly weaker. The first version of the A10 fix did exactly that
|
||
// by adding the contour before the threshold was taken; the acceptance found it.
|
||
//
|
||
// Mutation this catches: add the contour back before rebillConsentThreshold and the threshold rises with
|
||
// it, so a re-payment that must be consented to slips under the bar → RED.
|
||
func TestTheContourDoesNotWeakenTheConsentGate(t *testing.T) {
|
||
rec := &reqRec{}
|
||
srv := newJSONProvider(rec, draftEdit)
|
||
defer srv.Close()
|
||
// A txt fixture, because the scenario edits the SOURCE in place and this book's source must be the file
|
||
// the edit touches.
|
||
// bookUSD well above the planted contour: the ONLY thing allowed to stop the second run is the consent
|
||
// gate, so a money ceiling cannot masquerade as the property under test.
|
||
bookPath := setupProjectOpts(t, srv.URL, projectOpts{source: "ГЛАВАА\fГЛАВАБ", regenerate: 0, bookUSD: 100})
|
||
ctx := obs.WithReqInfo(context.Background(), obs.ReqInfo{TraceID: obs.NewTraceID()})
|
||
|
||
r := newRunner(t, bookPath)
|
||
if _, err := r.TranslateBook(ctx); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
// A bank contour LARGE next to the book, so that folding it into the threshold's base would visibly
|
||
// move the bar. Planted through the real money path, as the role would.
|
||
if err := r.Store.UpsertSnapshot("snap-terminology-2", "brief", `{"k":1}`); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
job, err := r.Store.EnsureJob("test-book", 0, terminologyStageName, "snap-terminology-2")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
res, verdict, err := r.Store.Reserve("test-book", 1.0, store.Ceilings{BookUSD: 100, DayUSD: 100})
|
||
if err != nil || verdict != store.ReserveOK {
|
||
t.Fatalf("reserve: %v %v", verdict, err)
|
||
}
|
||
if err := r.Store.SettleWithCheckpoint(res, 1.0, store.Checkpoint{
|
||
RequestHash: "planted-big-contour", JobID: job.ID, ChunkIdx: 0,
|
||
Stage: terminologyStageName, Role: roleTerminologist,
|
||
ModelRequested: "fake-model", ModelActual: "fake-model",
|
||
ResponseText: "терм\tterm", UsageJSON: "{}", CostUSD: 1.0, FinishReason: "stop",
|
||
}, nil); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
rep, err := r.Status(ctx)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
r.Close()
|
||
|
||
// The REPORTED projection must carry the contour…
|
||
if rep.ProjectedBookUSD < 1.0 {
|
||
t.Fatalf("«what will this book cost me» must not omit a whole class of spend: %.6f", rep.ProjectedBookUSD)
|
||
}
|
||
// …and the consent gate must NOT have been relaxed by it. An in-place SOURCE edit genuinely re-buys the
|
||
// already-billed rows (backlog row 238), which is the cheapest scenario that really re-pays — a dropped
|
||
// stage would not do, because projectRebill never re-bills rows of a stage the config no longer runs.
|
||
if err := os.WriteFile(filepath.Join(filepath.Dir(bookPath), "source.txt"),
|
||
[]byte("ГЛАВАА ПРАВЛЕНА\fГЛАВАБ"), 0o644); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
r2 := newRunner(t, bookPath)
|
||
defer r2.Close()
|
||
_, err = r2.TranslateBook(ctx)
|
||
if err == nil || !strings.Contains(err.Error(), "RE-PAY") {
|
||
t.Fatalf("a re-payment must still be consented to: a contour folded into the threshold's base "+
|
||
"would have raised the bar above it and let the spend through unasked. got: %v", err)
|
||
}
|
||
}
|