aboutsummaryrefslogtreecommitdiff
path: root/internal/game/casino_refund_regression_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/game/casino_refund_regression_test.go')
-rw-r--r--internal/game/casino_refund_regression_test.go141
1 files changed, 61 insertions, 80 deletions
diff --git a/internal/game/casino_refund_regression_test.go b/internal/game/casino_refund_regression_test.go
index 90864e2..6842c32 100644
--- a/internal/game/casino_refund_regression_test.go
+++ b/internal/game/casino_refund_regression_test.go
@@ -1,10 +1,9 @@
package game
-// Settlement/refund regressions for the durable-money audit: a settlement
-// whose save failed is journaled as owed money, so a shutdown refund must
-// NOT also return the round's stake — recovery re-applies the owed payout,
-// which already contains the stake's return (table hands) or the round's
-// winnings (slots, where the stake was consumed by playing the round).
+// Settlement/refund regressions for single-file money: a settlement credits
+// the payout and clears the stakes it returns in ONE character save, so a
+// failed settlement leaves the stake pending (and refundable) rather than
+// paying it twice; an outstanding stake with no settlement is refunded.
import (
"path/filepath"
@@ -23,12 +22,11 @@ func failingSaveDuring(g *Game, dir string, fn func()) {
fn()
}
-// TestFailedTableSettlementDoesNotDoubleReturnStake: a won hand whose payout
-// save fails leaves the settlement journaled and owed. A graceful-shutdown
-// refund must not ALSO return the stake; recovery re-applies the payout
-// (which returns the stake inside it) exactly once. 100 - 10 + 20 = 110,
-// never 120.
-func TestFailedTableSettlementDoesNotDoubleReturnStake(t *testing.T) {
+// TestFailedSettlementLeavesStakeRefundable: a settlement whose save fails
+// changes nothing — the credit is reverted and the stake stays pending. A
+// restart refunds the stake; the win was never durable, and it is never paid
+// on top of a refund.
+func TestFailedSettlementLeavesStakeRefundable(t *testing.T) {
h := newHarness(t)
h.seedAccount("alice")
p := h.seedCharacter("alice", "alice")
@@ -43,72 +41,61 @@ func TestFailedTableSettlementDoesNotDoubleReturnStake(t *testing.T) {
failingSaveDuring(h.g, h.dir, func() {
if err := w.PayStakeCover(20, []string{wager.ID}); err == nil {
t.Error("expected the settlement save to fail")
- return
- }
- // The shutdown refund path must not return the covered stake.
- if chips, credits, err := w.RefundOutstanding(); err != nil || chips != 0 || credits != 0 {
- t.Errorf("shutdown refund returned a covered stake: chips=%d credits=%d err=%v", chips, credits, err)
- return
}
})
- h.g.recoverEconomy()
- loaded, err := h.g.AccountStore.LoadCharacter("alice")
- if err != nil {
- t.Error(err)
- return
- }
- if loaded.Credits != 110 {
- t.Errorf("balance after recovery = %d, want 110 (stake returned exactly once, inside the re-applied payout)", loaded.Credits)
- }
- if open := h.g.Economy.mustLoadOpen(t); len(open) != 0 {
- t.Errorf("records left open after recovery: %+v", open)
+ if p.Credits != 90 || len(p.PendingWagers) != 1 {
+ t.Errorf("after failed settlement: credits=%d pending=%+v; want 90 and one stake",
+ p.Credits, p.PendingWagers)
}
})
+
+ // A restart refunds the still-pending stake; nothing double-pays.
+ restarted := harnessFromDir(t, h.dir)
+ loaded, err := restarted.g.AccountStore.LoadCharacter("alice")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if loaded.Credits != 100 {
+ t.Fatalf("balance after restart = %d, want 100 (failed payout not applied; stake refunded)", loaded.Credits)
+ }
+ if len(loaded.PendingWagers) != 0 {
+ t.Fatalf("pending after recovery = %+v, want none", loaded.PendingWagers)
+ }
}
-// TestFailedSlotsPayoutDoesNotDoubleReturnStake: a slot spin's payout is
-// winnings only — the stake is consumed by playing the round. When the
-// payout save fails, the journaled winnings prove the round resolved: the
-// shutdown refund must not return the stake on top of the owed payout.
-func TestFailedSlotsPayoutDoesNotDoubleReturnStake(t *testing.T) {
+// TestSuccessfulSettlementClearsPendingDurably: the payout and the removal of
+// the stake it returns are one save, so the durable file never holds both.
+func TestSuccessfulSettlementClearsPendingDurably(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
- }
- failingSaveDuring(h.g, h.dir, func() {
- // Machine payouts carry no stake linkage (nil covers): the
- // journaled settlement still resolves the round.
- if err := w.PayCredits(20); err == nil {
- t.Error("expected the payout save to fail")
- return
- }
- if chips, credits, err := w.RefundOutstanding(); err != nil || chips != 0 || credits != 0 {
- t.Errorf("shutdown refund paid a resolved round's stake: chips=%d credits=%d err=%v", chips, credits, err)
- return
- }
- })
- h.g.recoverEconomy()
- loaded, err := h.g.AccountStore.LoadCharacter("alice")
+ wager, err := w.Wager(10)
if err != nil {
t.Error(err)
return
}
- if loaded.Credits != 110 {
- t.Errorf("balance after recovery = %d, want 110 (stake consumed by the round, winnings paid once)", loaded.Credits)
+ if err := w.PayStakeCover(20, []string{wager.ID}); err != nil {
+ t.Error(err)
}
})
+ if p.Credits != 110 || len(p.PendingWagers) != 0 {
+ t.Fatalf("after settlement: credits=%d pending=%+v; want 110 and none", p.Credits, p.PendingWagers)
+ }
+ loaded, err := h.g.AccountStore.LoadCharacter("alice")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if loaded.Credits != 110 || len(loaded.PendingWagers) != 0 {
+ t.Fatalf("durable after settlement: credits=%d pending=%+v; want 110 and none",
+ loaded.Credits, loaded.PendingWagers)
+ }
}
-// TestUncoveredStakeStillRefunds: without a journaled settlement, the
-// shutdown refund path keeps returning outstanding stakes (the documented
-// refund-on-shutdown policy) — a regression guard that the coverage skip
-// never over-triggers.
+// TestUncoveredStakeStillRefunds: without a settlement, the refund path
+// returns an outstanding stake (the refund-on-shutdown policy).
func TestUncoveredStakeStillRefunds(t *testing.T) {
h := newHarness(t)
h.seedAccount("alice")
@@ -125,12 +112,15 @@ func TestUncoveredStakeStillRefunds(t *testing.T) {
t.Errorf("uncovered stake refund: chips=%d credits=%d err=%v", chips, credits, err)
return
}
+ if p.Credits != 100 || len(p.PendingWagers) != 0 {
+ t.Errorf("after refund: credits=%d pending=%+v; want 100 and none", p.Credits, p.PendingWagers)
+ }
})
}
-// TestCasinoManagerRefundPathUsesCoverage: the same rule through the live
-// manager path (baccarat settle → RefundAll), exercised with a probe wallet
-// so the settlement save fails mid-round.
+// TestCasinoManagerFailedSettlementRefundIsExact drives a full baccarat round
+// through the manager with a wallet whose settlement save fails: the stake
+// stays pending and is refunded exactly once (never paid on top of a payout).
func TestCasinoManagerFailedSettlementRefundIsExact(t *testing.T) {
h := newHarness(t)
h.seedAccount("alice")
@@ -138,27 +128,18 @@ func TestCasinoManagerFailedSettlementRefundIsExact(t *testing.T) {
h.g.RunOnOwner(func() {
p.Credits = 1000
h.g.Casino.Join(1, casino.TableConfig{ID: "b", Game: casino.GameBaccarat}, "alice")
- var totalPaid int
w := casinoWallet{g: h.g, p: p}
- wallet := &fallbackProbeWallet{harness: h, inner: w, onPay: func(credits int, failedCall bool) {
- totalPaid += credits
- }}
- // One bet: a lone player's bet deals and settles immediately. The
- // wallet fails the first settlement's journal-free save via a
- // broken SAVE (not journal) so the record stays open and owed.
- h.g.Casino.Bet(1, "b", "alice", "player", 10, wallet)
+ // A lone player's bet deals and settles immediately; break the save
+ // for that settlement so the stake stays pending.
+ failingSaveDuring(h.g, h.dir, func() {
+ h.g.Casino.Bet(1, "b", "alice", "player", 10, w)
+ })
h.g.emitCasinoEvents(h.g.Casino.RefundAll())
- h.g.recoverEconomy()
- loaded, err := h.g.AccountStore.LoadCharacter("alice")
- if err != nil {
- t.Error(err)
- return
- }
- // The stake was committed durably (990); RefundAll must not return
- // it while the settlement payout is owed; recovery re-applies the
- // payout: 1000 - 10 + totalPaid = final.
- if want := 1000 - 10 + totalPaid; loaded.Credits != want {
- t.Errorf("balance after refund+recovery = %d, want %d (wager 10, paid %d)", loaded.Credits, want, totalPaid)
- }
})
+ if p.Credits != 1000 {
+ t.Fatalf("balance after failed settlement and refund = %d, want 1000", p.Credits)
+ }
+ if len(p.PendingWagers) != 0 {
+ t.Fatalf("pending after refund = %+v, want none", p.PendingWagers)
+ }
}