package pgstore import ( "context" "errors" "testing" "time" "textmachine/platform/internal/money" ) // settle_guards_test.go: the two clauses of the settlement path that nothing executed. // // Both are the class PD-333 and PD-334 named — a caveat on the money path no test ever runs — and // both are unreachable from the ONLY production caller today. That is exactly why they are pinned // here and not left to the caller's own tests: an unreachable guard is one a later edit deletes // without a single test going red, and the next caller inherits the hole. // The negative-spend guard in Settle (register row PD-394). Removing it left the FULL battery green, // all eighteen packages. // // Reachability today is zero and named rather than assumed: `attemptSpend` clamps a backwards meter // to zero, so the reconciler never hands a negative figure down. What holds if the guard goes is the // SCHEMA — a negative spend would write a settlement row with a positive amount, which // `credit_ledger_sign` refuses — so the protection is TRANSITIVE, held by a constraint written for // another purpose. This pin makes the guard hold its own weight. // // Mutation caught: deleting `if spent < 0` from Settle. func TestSettlementRefusesANegativeSpendBeforeItReachesTheLedger(t *testing.T) { s, ctx := testDB(t) now := fundedAccount(t, s, ctx, "u1", "10") seedBook(t, s, ctx, "bk1", "u1", 500) key := "run_neg#1" if err := s.Hold(ctx, "u1", "bk1", key, money.MicroUSD(200_000), now); err != nil { t.Fatal(err) } before := balanceOf(t, s, ctx, "u1") // ⚠ THE GUARD'S OWN ERROR and not merely "an error", which is what the first version of this pin // asserted — and the planted mutation walked straight through it. With the guard deleted, Settle // proceeds into its transaction and the SCHEMA refuses the row (`credit_ledger_sign`), so an // error comes back either way and the pin proved nothing about the guard. That is precisely the // transitive protection PD-394 describes, caught by this pack's own planting. err := s.Settle(ctx, key, money.MicroUSD(-1), BasisComplete, now) if !errors.Is(err, ErrNegativeSpend) { t.Fatalf("Settle answered %v, want %v: a negative spend would CREDIT the account for having "+ "run something, and it must be refused HERE rather than by a constraint written for "+ "another purpose", err, ErrNegativeSpend) } // The refusal has to leave the world alone, which is the half a status check would miss: a guard // that refuses AFTER closing the reservation strands the hold instead of protecting it. if after := balanceOf(t, s, ctx, "u1"); after != before { t.Errorf("the refused settlement moved the balance from %s to %s", before.USD(), after.USD()) } open, err := s.AttemptReservationOpen(ctx, "run_neg", 1) if err != nil { t.Fatal(err) } if !open { t.Error("the refused settlement closed the reservation anyway: the hold is now stranded") } } // The `applied` flag of the settlement's own ledger write (found by this pack; not a register row // when it was written). // // Settle was the odd one of the three places that call appendLedger: `holdTx` answers // ErrDuplicateHold on a spent key, `releaseHold` answers ErrReleaseKeySpent and rolls back — and // Settle discarded the flag. A spent `run_settle` key therefore charged NOTHING while the hold had // already gone back whole, and the caller was told nil: the account keeps the money for a run it // used. Reachability today is zero for the same reason as PD-394's guard, and the sequence that // reaches it is the out-of-band one PD-97 already names. // // The key is spent HERE the only way it can be — by hand — because the code path that would do it // is the one that must not exist. // // Mutation caught: `_, err = appendLedger(...)` in place of the checked form. func TestASettlementWhoseKeyWasSpentIsRefusedRatherThanSilent(t *testing.T) { s, ctx := testDB(t) now := fundedAccount(t, s, ctx, "u1", "10") seedBook(t, s, ctx, "bk1", "u1", 500) key := "run_spent#1" if err := s.Hold(ctx, "u1", "bk1", key, money.MicroUSD(200_000), now); err != nil { t.Fatal(err) } // The settlement key, already posted. This is the state PD-97 describes on the release side: // the money moved once out of band and the reservation is open again on the same attempt id. exec(t, s, ctx, ` insert into credit_ledger (user_id, kind, amount_micro_usd, source, source_id, note, created_at) values ('u1', 'settlement', -1, 'run_settle', $1, 'posted out of band', $2)`, key, now) before := balanceOf(t, s, ctx, "u1") err := s.Settle(ctx, key, money.MicroUSD(50_000), BasisComplete, now) if !errors.Is(err, ErrSettlementKeySpent) { t.Fatalf("Settle answered %v, want %v: a settlement that posts nothing while the hold goes "+ "back whole is a run the account paid for and did not", err, ErrSettlementKeySpent) } // The whole point of REPORTING rather than applying: the transaction rolls back, so the hold that // was about to be returned for free is still reserved and a person can look at it. if after := balanceOf(t, s, ctx, "u1"); after != before { t.Errorf("the refused settlement still moved the balance, %s -> %s: the release went "+ "through and the charge did not", before.USD(), after.USD()) } open, err := s.AttemptReservationOpen(ctx, "run_spent", 1) if err != nil { t.Fatal(err) } if !open { t.Error("the reservation was closed by a settlement that charged nothing") } } func balanceOf(t *testing.T, s *Store, ctx context.Context, user string) money.MicroUSD { t.Helper() b, err := s.Balance(ctx, user) if err != nil { t.Fatal(err) } return b } // `run abandon` on a run with MORE THAN ONE orphaned hold closes them all, and only then records the // run's money as resolved. // // ⚠ The state is built by hand and that is deliberate and labelled: no ordinary path produces it, // because `reopen` refuses to start an attempt while the previous one's reservation is open. But a // run that needs this command is by definition one whose ordinary path came apart — and the failure // mode if only the first is closed is the worst kind: `settled_at` says the money is resolved, the // operator is told the hold came back whole, and a second hold stays debited from the account with // nothing pointing at it. Same reasoning as `TestAReleaseWhoseKeyWasSpentIsRefusedRatherThanSilent`, // which builds its own impossible state for the same reason. // // Mutation caught: reading the orphans with QueryRow (one row) instead of Query; stamping settled_at // before the loop. func TestAbandoningASettlementClosesEveryOrphanedHoldOfTheRun(t *testing.T) { s, ctx := testDB(t) now := fundedAccount(t, s, ctx, "u1", "10") seedBook(t, s, ctx, "bk1", "u1", 500) exec(t, s, ctx, `insert into runs (id, book_id, status, verify_bank, ceiling_chapters, started_at, finished_at, revision) values ('run_two','bk1','failed',false,10,$1,$1,1)`, now) for _, attempt := range []int{1, 2} { key := ReservationKey("run_two", attempt) // reconcile_failures is part of the state and not decoration: the command admits only a // settlement that has actually failed, so a fixture without it would be testing the refusal. exec(t, s, ctx, `insert into run_attempts (run_id, attempt_no, started_at, ended_at, last_offset, engine_run_id, spend_baseline_micro_usd, reconcile_failures) values ('run_two',$1,$2,$2,0,$3,0,6)`, attempt, now, key) if err := s.Hold(ctx, "u1", "bk1", key, money.MicroUSD(200_000), now); err != nil { t.Fatal(err) } } before := balanceOf(t, s, ctx, "u1") verdict, err := s.AbandonRun(ctx, AbandonOrder{RunID: "run_two", Reason: "the engine build was removed", ReleaseHold: false, Now: now}) if err != nil { t.Fatal(err) } if verdict != AbandonedSettlement { t.Errorf("verdict %q, want %q", verdict, AbandonedSettlement) } if after := balanceOf(t, s, ctx, "u1"); after != before+money.MicroUSD(400_000) { t.Errorf("balance %s -> %s: only part of what this run was holding came back, and the "+ "operator was told it came back whole", before.USD(), after.USD()) } for _, attempt := range []int{1, 2} { open, err := s.AttemptReservationOpen(ctx, "run_two", attempt) if err != nil { t.Fatal(err) } if open { t.Errorf("attempt %d still holds the account's credit after the verdict", attempt) } } var settled *time.Time if err := s.pool.QueryRow(ctx, `select settled_at from runs where id = 'run_two'`).Scan(&settled); err != nil { t.Fatal(err) } if settled == nil { t.Error("the run is still recorded as unsettled although every hold of it is closed") } // And a second verdict says the state rather than doing anything. if _, err := s.AbandonRun(ctx, AbandonOrder{RunID: "run_two", Reason: "again", ReleaseHold: false, Now: now}); !errors.Is(err, ErrMoneyAlreadyClosed) { t.Errorf("a second verdict answered %v, want %v", err, ErrMoneyAlreadyClosed) } } // A settlement that has not reached the THRESHOLD is refused by `run abandon`, and refused because // of the MONEY — even though the operator's own table already shows it. // // This command gives the hold back WHOLE. Every settlement is briefly open — an attempt's `ended_at` // is stamped by one transaction and its reservation closed by a later one — so a branch keyed on the // run being finished would let an operator hand back the entire hold of a run that finished normally, // spent real money, and was about to be settled correctly on the next tick. Nothing outside would // ever notice: the run leaves every worklist. // // The admitted set is therefore exactly the set the operator's own table SHOWS as `settling`, which // is the same floor (`reconcile_failures >= 1`) StalledRuns applies. // // Mutation caught: dropping `and a.reconcile_failures >= 1` from abandonSettlement's admission. func TestASettlementThatHasNotFailedIsRefusedRatherThanGivenAway(t *testing.T) { s, ctx := testDB(t) now := fundedAccount(t, s, ctx, "u1", "10") seedBook(t, s, ctx, "bk1", "u1", 500) key := ReservationKey("run_healthy", 1) exec(t, s, ctx, `insert into runs (id, book_id, status, verify_bank, ceiling_chapters, started_at, finished_at, revision) values ('run_healthy','bk1','ready',false,10,$1,$1,1)`, now) // ONE failure: enough for the operator's table to show the row, deliberately NOT enough for the // command that writes the hold off. That gap is the whole point of the two numbers. exec(t, s, ctx, `insert into run_attempts (run_id, attempt_no, started_at, ended_at, last_offset, engine_run_id, spend_baseline_micro_usd, reconcile_failures) values ('run_healthy',1,$1,$1,0,$2,0,1)`, now, key) if err := s.Hold(ctx, "u1", "bk1", key, money.MicroUSD(200_000), now); err != nil { t.Fatal(err) } before := balanceOf(t, s, ctx, "u1") if _, err := s.AbandonRun(ctx, AbandonOrder{RunID: "run_healthy", Reason: "looks stuck to me", ReleaseHold: false, Now: now}); !errors.Is(err, ErrSettlementNotStuck) { t.Fatalf("AbandonRun answered %v, want %v: one failed tick is a row an operator can SEE, not "+ "a hold they may write off — returning it whole would charge nothing for whatever the "+ "run actually spent", err, ErrSettlementNotStuck) } if after := balanceOf(t, s, ctx, "u1"); after != before { t.Errorf("the refused verdict moved the balance %s -> %s", before.USD(), after.USD()) } open, err := s.AttemptReservationOpen(ctx, "run_healthy", 1) if err != nil { t.Fatal(err) } if !open { t.Error("the refused verdict closed the reservation anyway") } } // The floor `run abandon` acts on and the threshold everything else measures against are ONE number. // // They are written in two packages — `runs.StalledAfter` cannot travel here, because `internal/runs` // depends on this package and not the other way — so the only thing keeping them equal is this // assertion. If they drift, the command either refuses rows the operator's own table calls stalled, // or writes off holds the table never showed as stalled at all. // // Mutation caught: changing either constant alone. func TestTheAbandonFloorIsTheStalledThreshold(t *testing.T) { // The literal is repeated on purpose: importing `runs` here would be an import cycle, and a test // that read the same constant twice would assert nothing. if abandonAfter != 5 { t.Errorf("abandonAfter is %d; runs.StalledAfter is 5, and the two are one number: an "+ "operator may only write off a hold the threshold has already called stalled", abandonAfter) } }