aboutsummaryrefslogtreecommitdiff
path: root/internal/game/economy_recovery_regression_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/game/economy_recovery_regression_test.go')
-rw-r--r--internal/game/economy_recovery_regression_test.go118
1 files changed, 118 insertions, 0 deletions
diff --git a/internal/game/economy_recovery_regression_test.go b/internal/game/economy_recovery_regression_test.go
index 12df39b..0e764ca 100644
--- a/internal/game/economy_recovery_regression_test.go
+++ b/internal/game/economy_recovery_regression_test.go
@@ -359,3 +359,121 @@ func TestRecoveryRestoresChipsFromMixedRefundCrash(t *testing.T) {
t.Fatalf("nothing may stay owed after recovery: %+v", open)
}
}
+
+// TestRecoveryDoesNotReturnPaidChipStakeInKind: a chip wager whose round
+// resolved with a durable payout (a win returns the stake inside the
+// payout) must not ALSO get the chips back in kind on recovery — the
+// resolving payout is the refund. Only a payout that does not cover the
+// stake's full value (e.g. the credit side of a crashed mixed refund)
+// leaves the chip side outstanding.
+func TestRecoveryDoesNotReturnPaidChipStakeInKind(t *testing.T) {
+ h := newHarness(t)
+ h.seedAccount("alice", "pw", false)
+ 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
+ }
+ // The round resolved: a 2x payout (20) is durable, then the crash
+ // lands before the stake's resolution marker.
+ if err := w.PayCredits(20); err != nil {
+ t.Error(err)
+ return
+ }
+ h.g.recoverEconomy()
+ })
+ loaded, err := h.g.AccountStore.LoadCharacter("alice")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if loaded.Credits != 120 || countChips(loaded) != 0 {
+ t.Fatalf("won chip bet after recovery = (%d credits, %d chips), want (120, 0) — no double refund",
+ loaded.Credits, countChips(loaded))
+ }
+}
+
+// TestRecoveryUnpaidLiabilitySurvivesLaterRecoveries: an unpaid payout
+// liability (the credit cap blocked re-application) must survive repeated
+// recoveries. Without checkpoint re-anchoring, a later recovery mistakes
+// the balance a paid sibling moved for the unpaid record's own durability
+// and silently erases the owed amount.
+func TestRecoveryUnpaidLiabilitySurvivesLaterRecoveries(t *testing.T) {
+ h := newHarness(t)
+ h.seedAccount("alice", "pw", false)
+ p := h.seedCharacter("alice", "alice")
+ h.g.RunOnOwner(func() {
+ p.Credits = casino.MaxCredits - 30
+ if err := h.g.AccountStore.SaveCharacter(p); err != nil {
+ t.Error(err)
+ return
+ }
+ for i := 0; i < 2; i++ {
+ if _, err := h.g.Economy.start("payout", p, 0, 20, p.Credits, p.Credits+20, 0, 0); err != nil {
+ t.Error(err)
+ return
+ }
+ }
+ h.g.recoverEconomy()
+ if len(h.g.Economy.loadOpen()) != 1 {
+ t.Errorf("setup: expected one unpaid record after capped recovery, got %d", len(h.g.Economy.loadOpen()))
+ return
+ }
+ // A later startup over the same files: the liability stays owed.
+ h.g.recoverEconomy()
+ if got := len(h.g.Economy.loadOpen()); got != 1 {
+ t.Errorf("second recovery erased the unpaid 20-credit liability: open=%d, want 1", got)
+ }
+ })
+ loaded, err := h.g.AccountStore.LoadCharacter("alice")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if loaded.Credits != casino.MaxCredits-10 {
+ t.Fatalf("balance after repeated recovery = %d, want %d (one payout applied)", loaded.Credits, casino.MaxCredits-10)
+ }
+}
+
+// TestCapBlockedPayoutIsJournaled: a won payout at the economy cap must be
+// recorded as an owed (open) payout record even though it cannot be
+// applied — a reported win is never silently dropped.
+func TestCapBlockedPayoutIsJournaled(t *testing.T) {
+ h := newHarness(t)
+ h.seedAccount("alice", "pw", false)
+ p := h.seedCharacter("alice", "alice")
+ h.g.RunOnOwner(func() {
+ p.Credits = casino.MaxCredits
+ w := casinoWallet{g: h.g, p: p}
+ if _, err := w.Wager(10); err != nil {
+ t.Error(err)
+ return
+ }
+ if err := w.PayCredits(20); err == nil {
+ t.Error("payout at the credit cap must fail, not silently clamp")
+ return
+ }
+ if p.Credits != casino.MaxCredits-10 {
+ t.Errorf("cap-blocked payout mutated the balance: %d", p.Credits)
+ }
+ })
+ if err := h.g.AccountStore.SaveCharacter(p); err != nil {
+ t.Fatal(err)
+ }
+ restarted := harnessFromDir(t, h.dir)
+ loaded, err := restarted.g.AccountStore.LoadCharacter("alice")
+ if err != nil {
+ t.Fatal(err)
+ }
+ // Recovery: the owed win cannot be applied (cap), so the liability
+ // stays open for operator inspection, never erased; the consumed
+ // stake is settled against the payout evidence.
+ if got := len(restarted.g.Economy.loadOpen()); got != 1 {
+ t.Fatalf("owed cap-blocked payout must stay open after recovery, open records: %+v", restarted.g.Economy.loadOpen())
+ }
+ if loaded.Credits != casino.MaxCredits-10 {
+ t.Fatalf("recovery mutated a capped balance: %d", loaded.Credits)
+ }
+}