textmachine/platform/internal/pgstore/readaccount_test.go

63 lines
3 KiB
Go

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())
}
}