textmachine/backend/internal/pipeline/phasefits_test.go

123 lines
5.6 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 (
"bytes"
"log/slog"
"strings"
"testing"
)
// phasefits_test.go: A2 — a phase printed its estimate beside its budget, saw that it did not fit, and
// started anyway.
//
// The old shape aborted PART-WAY through: the per-batch gate stopped the pass mid-flight, leaving a
// half-consolidated bank and paid work whose report said nothing about being cut short. On the cold run
// that happened twice and three batches of four were never bought. The fix decides BEFORE the first call.
// TestThePhaseCutsItsPlanBeforeTheFirstCall is the landing.
//
// Mutation this catches: move the budget decision back inside the execution loop (or drop the `fits`
// bound) and the pass runs batches it cannot afford before discovering it — the announcement then arrives
// after money is gone, and the ordering assertion below fires.
func TestThePhaseCutsItsPlanBeforeTheFirstCall(t *testing.T) {
var logBuf bytes.Buffer
rec := &reqRec{}
srv := newJSONProvider(rec, func(body string) (string, string) {
if isTerminologyBody(body) {
return "方源\tФан Юань\n花家\tклан Хуа\n青茅山\tгора Цинмао", "stop"
}
return "Фан Юань пришёл к горе Цинмао." + "\n" + bankBlockForMining, "stop"
})
defer srv.Close()
probe := newVerifyRunner(t, setupMiningStopProject(t, srv.URL, miningStopOpts{terminology: true, batchRunes: 400}))
_ = runToSignatureStop(t, probe)
if probe.lastTerminology.Batches < 2 {
t.Fatalf("the fixture must actually batch, got %d", probe.lastTerminology.Batches)
}
perBatch := probe.lastTerminology.EstimateUSD / float64(probe.lastTerminology.Batches)
probe.Close()
r := newVerifyRunner(t, setupMiningStopProject(t, srv.URL, miningStopOpts{
terminology: true, batchRunes: 400, budgetUSD: perBatch * 1.5, // one batch fits, the rest do not
}))
defer r.Close()
r.Log = slog.New(slog.NewTextHandler(&logBuf, &slog.HandlerOptions{Level: slog.LevelWarn}))
_ = runToSignatureStop(t, r)
res := r.lastTerminology
if res.BatchesDropped == 0 {
t.Fatalf("the fixture must actually exceed the budget: %d batches, none dropped", res.Batches)
}
out := logBuf.String()
// (1) THE DECISION IS ANNOUNCED, and it names every term the operator's next action depends on.
if !strings.Contains(out, "CUT TO WHAT DOES") {
t.Fatalf("a phase that cannot afford its own plan must say so, and say it as a decision rather "+
"than as an interruption:\n%s", out)
}
for _, want := range []string{"batches_planned", "batches_running", "batches_dropped", "budget_usd"} {
if !strings.Contains(out, want) {
t.Fatalf("the cut must name %q — an operator raises the budget or accepts a partial bank, and "+
"both need the numbers:\n%s", want, out)
}
}
// (2) AND THE RESULT SAYS THE BANK IS PARTIAL, because «unanswered» alone reads as a verdict about the
// TERMS when it is partly a verdict about the money.
if !strings.Contains(out, "PARTIALLY consolidated") {
t.Fatalf("a bank the budget cut short must be reported as partial:\n%s", out)
}
}
// TestTheCutIsDecidedBeforeAnyMoneyMoves is the axis the pack called out separately: NO PARTIAL WORK MAY
// BE PAID FOR by a decision made after the fact. It is shown on the fixture rather than argued.
//
// The pass is given a budget BELOW one batch. Under the old shape the gate sat inside the loop and the
// first batch was still attempted — the estimate was checked against the budget, so it refused, but the
// pass had already been entered and the decision belonged to the loop. Here the plan is cut to ZERO
// batches before the loop is entered at all, so the provider is never reached and the bank is untouched.
func TestTheCutIsDecidedBeforeAnyMoneyMoves(t *testing.T) {
// TWO providers: the sizing probe legitimately calls the role (that is how the per-batch estimate is
// obtained at all), and only the BUDGETED run is forbidden to reach it.
permissive := newJSONProvider(&reqRec{}, func(body string) (string, string) {
if isTerminologyBody(body) {
return "方源\tФан Юань", "stop"
}
return "Фан Юань пришёл к горе Цинмао." + "\n" + bankBlockForMining, "stop"
})
defer permissive.Close()
probe := newVerifyRunner(t, setupMiningStopProject(t, permissive.URL, miningStopOpts{terminology: true, batchRunes: 400}))
_ = runToSignatureStop(t, probe)
perBatch := probe.lastTerminology.EstimateUSD / float64(probe.lastTerminology.Batches)
probe.Close()
rec := &reqRec{}
srv := newJSONProvider(rec, func(body string) (string, string) {
if isTerminologyBody(body) {
t.Error("a pass that cannot afford ONE batch must not reach the provider at all")
return "方源\tФан Юань", "stop"
}
return "Фан Юань пришёл к горе Цинмао." + "\n" + bankBlockForMining, "stop"
})
defer srv.Close()
r := newVerifyRunner(t, setupMiningStopProject(t, srv.URL, miningStopOpts{
terminology: true, batchRunes: 400, budgetUSD: perBatch * 0.4, // not even one batch fits
}))
defer r.Close()
_ = runToSignatureStop(t, r)
res := r.lastTerminology
if res.Batches == 0 {
t.Fatal("the fixture must have produced batches to drop")
}
if res.BatchesDropped != res.Batches {
t.Fatalf("nothing fits, so the whole plan is dropped: planned=%d dropped=%d", res.Batches, res.BatchesDropped)
}
if res.CostUSD != 0 {
t.Fatalf("a plan that fits nothing must spend NOTHING — partial work paid for by a decision taken "+
"after the money moved is the defect this pass was rebuilt to remove; got $%.6f", res.CostUSD)
}
if res.Consolidated != 0 {
t.Fatalf("no batch ran, so nothing can have been consolidated: %d", res.Consolidated)
}
}