From a8a67482bbff8297a96adff65d2fd7029ed998cc Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Fri, 18 Sep 2026 21:36:36 -0400 Subject: refactor: replace economy journal with single-file pending-wager wallet --- internal/game/readiness_regression_test.go | 216 +---------------------------- 1 file changed, 4 insertions(+), 212 deletions(-) (limited to 'internal/game/readiness_regression_test.go') diff --git a/internal/game/readiness_regression_test.go b/internal/game/readiness_regression_test.go index 41170b7..442bb1f 100644 --- a/internal/game/readiness_regression_test.go +++ b/internal/game/readiness_regression_test.go @@ -1,18 +1,15 @@ package game -// Readiness regressions for the account-rename, disconnect, and durable-money -// paths reviewed before world/casino expansion: a transport disconnect must -// be race-free, renames must not break ownership or strand owed payouts, -// reserved-loot pickups must never destroy items, and recovery failure paths -// must retry or surface instead of silently dropping money. +// Readiness regressions for the account-rename and disconnect paths reviewed +// before world/casino expansion: a transport disconnect must be race-free, +// renames must not break ownership, and reserved-loot pickups must never +// destroy items. import ( - "os" "path/filepath" "testing" "thehouseoficarus/internal/net" - "thehouseoficarus/internal/player" ) // TestDisconnectRaceIsFreeAndNeverLost drives DisconnectTick (owner) while a @@ -72,40 +69,6 @@ func TestReservedPickupDoesNotDestroyItems(t *testing.T) { }) } -// TestOwedPayoutSurvivesCharacterRename: economy records resolve their -// subject by the character's STABLE ID, so renaming the character between -// the crash and recovery still delivers the owed payout. -func TestOwedPayoutSurvivesCharacterRename(t *testing.T) { - h := newHarness(t) - h.seedAccount("acct") - p := h.seedCharacter("alice", "acct") - h.g.RunOnOwner(func() { - p.ID = "stable-char-id" // production characters carry a stable ID - p.Credits = 90 - if err := h.g.AccountStore.SaveCharacter(p); err != nil { - t.Error(err) - return - } - if _, err := h.g.Economy.start("payout", p, 0, 20, 90, 110, 0, 0); err != nil { - t.Error(err) - return - } - if err := h.g.renameCharacterTxn("acct", "alice", "alicia"); err != nil { - t.Error(err) - return - } - h.g.recoverEconomy() - loaded, err := h.g.AccountStore.LoadCharacter("alicia") - if err != nil { - t.Error(err) - return - } - if loaded.Credits != 110 { - t.Errorf("renamed character never receives owed payout: credits=%d, want 110", loaded.Credits) - } - }) -} - // TestAccountRenamePreservesAccessAndOwnership: renaming an account // rewrites the owned character files' owner field in the same transaction // and republishes stale sessions (still carrying the old cached name) so @@ -127,174 +90,3 @@ func TestAccountRenamePreservesAccessAndOwnership(t *testing.T) { } }) } - -// TestSettledSurrenderDoesNotRefundChipStake: a settlement payout — even a -// partial one like a surrendered hand paying half a chip stake — proves the -// round resolved, so recovery must not ALSO return the chip stake in kind. -func TestSettledSurrenderDoesNotRefundChipStake(t *testing.T) { - h := newHarness(t) - h.seedAccount("alice") - p := h.seedCharacter("alice", "alice") - h.g.RunOnOwner(func() { - p.Credits = 100 - p.SetInvSlot(0, &player.InventorySlot{ItemID: "chips", Quantity: 10}) - w := casinoWallet{g: h.g, p: p} - if _, err := w.Wager(10); err != nil { - t.Error(err) - return - } - // Surrender settles for half the stake; the crash lands before the - // resolution marker. - if err := w.PayCredits(5); err != nil { - t.Error(err) - return - } - h.g.recoverEconomy() - loaded, err := h.g.AccountStore.LoadCharacter("alice") - if err != nil { - t.Error(err) - return - } - if loaded.Credits != 105 || countChips(loaded) != 0 { - t.Errorf("settled surrender recovered %d credits / %d chips; want 105 / 0", loaded.Credits, countChips(loaded)) - } - }) -} - -// TestRecoverySaveFailureRetriesOnNextStartup: a TRANSIENT save failure -// during recovery must not re-anchor the owed record to unreadable -// checkpoints — the record keeps its original checkpoints and a later -// recovery (after the disk is fixed) re-applies the owed payout. -func TestRecoverySaveFailureRetriesOnNextStartup(t *testing.T) { - h := newHarness(t) - h.seedAccount("alice") - p := h.seedCharacter("alice", "alice") - charDir := filepath.Join(h.dir, "players", "characters") - t.Cleanup(func() { os.Chmod(charDir, 0755) }) - h.g.RunOnOwner(func() { - p.Credits = 90 - if err := h.g.AccountStore.SaveCharacter(p); err != nil { - t.Error(err) - return - } - if _, err := h.g.Economy.start("payout", p, 0, 20, 90, 110, 0, 0); err != nil { - t.Error(err) - return - } - if err := os.Chmod(charDir, 0500); err != nil { - t.Error(err) - return - } - h.g.recoverEconomy() // every save fails - if err := os.Chmod(charDir, 0755); err != nil { - t.Error(err) - return - } - h.g.recoverEconomy() // retry with a healthy disk - loaded, err := h.g.AccountStore.LoadCharacter("alice") - if err != nil { - t.Error(err) - return - } - if loaded.Credits != 110 { - t.Errorf("after fixing the disk and retrying: %d credits, want 110; open=%+v", loaded.Credits, h.g.Economy.mustLoadOpen(t)) - } - }) -} - -// TestResolvedStakeEvidenceSurvivesJournalFailure: a resolution marker -// that cannot be journaled (persistent append failure) leaves the stake -// OPEN — never silently forgotten — and records the resolution in the -// character file as durable evidence. Recovery then settles the stake -// WITHOUT refunding it: the round genuinely resolved, and the file is the -// proof. When the file save also fails, no evidence exists and recovery -// applies the documented refund-on-lost-state policy instead. -func TestResolvedStakeEvidenceSurvivesJournalFailure(t *testing.T) { - h := newHarness(t) - h.seedAccount("alice") - p := h.seedCharacter("alice", "alice") - h.g.RunOnOwner(func() { - p.Credits = 100 - w := casinoWallet{g: h.g, p: p} - if _, err := w.Wager(10); err != nil { - t.Error(err) - return - } - path := h.g.Economy.path - h.g.Economy.path = h.dir // persistent append failure - w.ResolveStakes() - h.g.Economy.path = path - if open := h.g.Economy.openStakes(economyKey(p)); len(open) != 1 { - t.Errorf("unresolvable stake was silently dropped: open=%d", len(open)) - } - // The resolution is recorded durably in the character file. - loaded, err := h.g.AccountStore.LoadCharacter("alice") - if err != nil { - t.Error(err) - return - } - if len(loaded.ResolvedStakes) != 1 { - t.Errorf("resolved-stake evidence not recorded: %+v", loaded.ResolvedStakes) - return - } - h.g.recoverEconomy() - loaded, err = h.g.AccountStore.LoadCharacter("alice") - if err != nil { - t.Error(err) - return - } - if loaded.Credits != 90 { - t.Errorf("after recovery: credits=%d, want 90 (round resolved; evidence prevents a refund)", loaded.Credits) - } - if open := h.g.Economy.mustLoadOpen(t); len(open) != 0 { - t.Errorf("records left open after recovery: %+v", open) - } - if len(loaded.ResolvedStakes) != 0 { - t.Errorf("stale resolved-stake evidence not pruned: %+v", loaded.ResolvedStakes) - } - }) -} - -// TestUnjournalablePayoutPaidThroughCharacterFile: a payout that cannot be -// journaled (persistent append failure) is still made durable through the -// character file — the save is the commit point, so the win is real and -// announced. The resolved stake is recorded as file evidence so recovery -// neither refunds the stake (double pay) nor drops the round. -func TestUnjournalablePayoutPaidThroughCharacterFile(t *testing.T) { - h := newHarness(t) - h.seedAccount("alice") - p := h.seedCharacter("alice", "alice") - h.g.RunOnOwner(func() { - p.Credits = 100 - w := casinoWallet{g: h.g, p: p} - if _, err := w.Wager(10); err != nil { - t.Error(err) - return - } - path := h.g.Economy.path - h.g.Economy.path = h.dir - err := w.PayCredits(20) - h.g.Economy.path = path - if err != nil { - t.Errorf("character-file fallback failed: %v", err) - return - } - if p.Credits != 110 { - t.Errorf("fallback payout did not land: credits=%d, want 110", p.Credits) - } - // As the games do after a successful settlement: resolve the stake. - w.ResolveStakes() - h.g.recoverEconomy() - loaded, err := h.g.AccountStore.LoadCharacter("alice") - if err != nil { - t.Error(err) - return - } - if loaded.Credits != 110 { - t.Errorf("after recovery: credits=%d, want 110 (win durable once; no stake refund on top)", loaded.Credits) - } - if open := h.g.Economy.mustLoadOpen(t); len(open) != 0 { - t.Errorf("open records: %+v", open) - } - }) -} -- cgit v1.2.3