123 lines
5.6 KiB
Go
123 lines
5.6 KiB
Go
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)
|
||
}
|
||
}
|