74 lines
3 KiB
Text
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)
|
|
}
|
|
})
|
|
}
|
|
}
|