From 72434cecc7e87ee50275757e0a3c040ea14577b5 Mon Sep 17 00:00:00 2001 From: heaven Date: Sat, 29 Aug 2026 22:03:09 +0300 Subject: [PATCH] Land the pin for the money read whose two figures the SQL gate cannot tell apart: swapping them stays green on the whole battery and inverts the numbers an operator reads during drift --- platform/internal/pgstore/readaccount_test.go | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 platform/internal/pgstore/readaccount_test.go diff --git a/platform/internal/pgstore/readaccount_test.go b/platform/internal/pgstore/readaccount_test.go new file mode 100644 index 00000000..6ef0c9c6 --- /dev/null +++ b/platform/internal/pgstore/readaccount_test.go @@ -0,0 +1,63 @@ +package pgstore + +import ( + "testing" + "time" + + "textmachine/platform/internal/money" +) + +// readaccount_test.go: that the two money figures ReadAccount returns are not INTERCHANGEABLE. +// +// ⚠ Why this needs its own test, when the lifecycle test next door already reads both: that test — +// and every other money test in this zone — asserts `Balance == LedgerSum`, because in the healthy +// state they ARE equal. So the one thing none of them can see is the two being SWAPPED. Measured +// rather than supposed: exchanging the two Scan targets in ReadAccount leaves the ENTIRE battery +// green, all eighteen packages, and the mutant is not equivalent — under drift the clean code +// answers Balance=4.00/LedgerSum=1.00 and the mutant answers 1.00/4.00 (register row PD-430). +// +// What it would cost in production is conditional but pointed. Drift is the one state ReadAccount +// exists to reveal, and in exactly that state `tmplatformctl balance --user` would print the LEDGER +// sum under the word "balance", while every decision taken on `Account.Balance` would be taken on +// the ledger instead of the cache. The drift WARNING would still fire — the comparison survives a +// swap — so the operator gets a correct alarm attached to two wrong numbers. +// +// The class is PD-394's and PD-376's: a money path that is right, and right only by nobody having +// mistyped it, because no test distinguishes the values. +// +// Mutation caught: exchanging any two of the three Scan targets in ReadAccount. +func TestTheAccountsThreeFiguresAreNotInterchangeable(t *testing.T) { + s, ctx := testDB(t) + now := time.Now().UTC() + seedUser(t, s, ctx, "u1") + seedBook(t, s, ctx, "bk1", "u1", 10) + if _, err := s.Grant(ctx, "u1", 1_000_000, "test", "seed", "", now); err != nil { + t.Fatal(err) + } + // A hold makes the THIRD figure distinct too, so a swap with `Reserved` cannot hide either. + if err := s.Hold(ctx, "u1", "bk1", "r_read#1", 300_000, now); err != nil { + t.Fatal(err) + } + // ⚠ DRIFT, manufactured on purpose: the cache is moved away from the ledger, which is the only + // state in which these two figures differ — and therefore the only state that can tell them + // apart. The zone's own fixture for PD-97 builds drift the same way, by hand, because no code + // path produces it. + exec(t, s, ctx, `update account_balances set balance_micro_usd = 4_000_000 where user_id = 'u1'`) + + a, err := s.ReadAccount(ctx, "u1") + if err != nil { + t.Fatal(err) + } + if want := money.MicroUSD(4_000_000); a.Balance != want { + t.Errorf("Balance is %s, want the CACHED balance %s — a reader that hands back the ledger sum "+ + "under this name reports the wrong money in the one state this function exists to reveal", + a.Balance.USD(), want.USD()) + } + if want := money.MicroUSD(700_000); a.LedgerSum != want { + t.Errorf("LedgerSum is %s, want the sum of the LEDGER %s (a grant of 1.00 less a hold of 0.30)", + a.LedgerSum.USD(), want.USD()) + } + if want := money.MicroUSD(300_000); a.Reserved != want { + t.Errorf("Reserved is %s, want the open hold %s", a.Reserved.USD(), want.USD()) + } +}