textmachine/platform/docs/p8-review/axis3-queue/refute_abandon_predictability_test.go.txt

74 lines
3 KiB
Text

package runs
// P8-REVIEW axis 3, REFUTER probe against the finder's F5 ("the second guard of `run abandon` cannot
// be predicted from the operator's table").
//
// The guard is `unit != "" || baseline != nil` (pgstore/runs.go:465). The table prints UNIT for the
// first and SPENT for the second, and SPENT is "?" EXACTLY when the baseline is NULL — the column is
// `case when a.spend_baseline_micro_usd is null then null else ... end` (runs.go:374-375), so the
// mapping is a bijection, not an inference. The operator's runbook states both halves of the guard
// in prose: deploy/README.md:247 "Команда ОТКАЖЕТ, если попытка ещё называет юнит ИЛИ несёт базовую
// линию траты".
//
// This asserts the bijection over all four states of the two columns.
import (
"errors"
"testing"
"textmachine/platform/internal/pgstore"
)
func TestRefuteTheAbandonRefusalIsExactlyWhatTheTwoPrintedColumnsSay(t *testing.T) {
for _, tc := range []struct {
name string
sql string
wantUnit string
wantSpnt string
}{
{"a unit and a baseline", `update run_attempts set reconcile_failures=5 where run_id=$1`, "named", "a figure"},
{"no unit, baseline kept (ReleaseSpawnClaim)", `update run_attempts set unit_name=null, reconcile_failures=5 where run_id=$1`, "(none)", "a figure"},
{"a unit, no baseline (pre-column attempt)", `update run_attempts set spend_baseline_micro_usd=null, reconcile_failures=5 where run_id=$1`, "named", "?"},
{"neither: never reached the engine", `update run_attempts set unit_name=null, spend_baseline_micro_usd=null, reconcile_failures=5 where run_id=$1`, "(none)", "?"},
} {
t.Run(tc.name, func(t *testing.T) {
f := newFixture(t, "20", 500)
run, err := f.svc.Start(f.ctx, StartRequest{UserID: "u1", BookID: f.bookID(t), CeilingChapters: 10})
if err != nil {
t.Fatal(err)
}
if err := f.svc.Spawn(f.ctx, run.ID); err != nil {
t.Fatal(err)
}
if _, err := f.store.Pool().Exec(f.ctx, tc.sql, run.ID); err != nil {
t.Fatal(err)
}
rows, err := f.store.StalledRuns(f.ctx, StalledAfter)
if err != nil {
t.Fatal(err)
}
if len(rows) != 1 {
t.Fatalf("%d rows", len(rows))
}
unit, spent := "named", "?"
if rows[0].UnitName == "" {
unit = "(none)"
}
if rows[0].SpentMicroUSD != nil {
spent = "a figure"
}
if unit != tc.wantUnit || spent != tc.wantSpnt {
t.Fatalf("the table printed UNIT=%s SPENT=%s, want UNIT=%s SPENT=%s", unit, spent, tc.wantUnit, tc.wantSpnt)
}
err = f.store.AbandonRun(f.ctx, run.ID, "the host is gone", false, f.now)
refused := errors.Is(err, pgstore.ErrRunMayHaveAProcess)
// This is what the runbook tells the operator to read off the two columns.
predicted := unit != "(none)" || spent != "?"
t.Logf("UNIT=%-6s SPENT=%-8s -> runbook predicts refusal=%v, handle refused=%v (%v)",
unit, spent, predicted, refused, err)
if predicted != refused {
t.Fatalf("NOT REFUTED: the table said %v and the handle did %v", predicted, refused)
}
})
}
}