aboutsummaryrefslogtreecommitdiff
path: root/internal/game/journal_failure_regression_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/game/journal_failure_regression_test.go')
-rw-r--r--internal/game/journal_failure_regression_test.go243
1 files changed, 0 insertions, 243 deletions
diff --git a/internal/game/journal_failure_regression_test.go b/internal/game/journal_failure_regression_test.go
deleted file mode 100644
index 043afeb..0000000
--- a/internal/game/journal_failure_regression_test.go
+++ /dev/null
@@ -1,243 +0,0 @@
-package game
-
-// Journal-failure regressions: a broken economy journal (persistent append
-// failure) must never lose money silently. Settlements pay through the
-// character file, stake resolutions are recorded as file evidence, and an
-// unreadable journal aborts recovery/compaction instead of erasing records.
-
-import (
- "os"
- "strings"
- "testing"
- "time"
-
- "thehouseoficarus/internal/casino"
- "thehouseoficarus/internal/player"
-)
-
-// failJournalDuring redirects the economy journal path for the duration of
-// fn, simulating a persistent append failure, and restores it afterwards.
-func failJournalDuring(h *harness, fn func()) {
- path := h.g.Economy.path
- h.g.Economy.path = h.dir
- defer func() { h.g.Economy.path = path }()
- fn()
-}
-
-// TestSettlementFallbackKeepsWholeBalance: a full table round whose payout
-// journal append fails still ends with the player whole — the settlement is
-// paid through the character file, the stake resolution is recorded as
-// evidence, and recovery leaves the balance exact.
-func TestSettlementFallbackKeepsWholeBalance(t *testing.T) {
- h := newHarness(t)
- h.seedAccount("alice")
- p := h.seedCharacter("alice", "alice")
- h.g.RunOnOwner(func() {
- p.Credits = 1000
- h.g.Casino.Join(1, casino.TableConfig{ID: "b", Game: casino.GameBaccarat}, "alice")
- var totalPaid int
- failed := false
- w := casinoWallet{g: h.g, p: p}
- // Wrap the wallet so exactly one settlement hits a broken journal.
- wallet := &fallbackProbeWallet{harness: h, inner: w, onPay: func(credits int, failedCall bool) {
- totalPaid += credits
- if failedCall {
- failed = true
- }
- }}
- rounds := 0
- for rounds < 100 && !failed {
- rounds++
- h.g.Casino.Bet(1, "b", "alice", "player", 10, wallet)
- }
- if !failed {
- t.Fatal("probe never observed a settlement payout")
- return
- }
- // One more full round after the failure: the table keeps working.
- rounds++
- h.g.Casino.Bet(1, "b", "alice", "player", 10, wallet)
- h.g.recoverEconomy()
- loaded, err := h.g.AccountStore.LoadCharacter("alice")
- if err != nil {
- t.Error(err)
- return
- }
- want := 1000 - 10*rounds + totalPaid
- if loaded.Credits != want {
- t.Errorf("balance after journal-failure round: credits=%d, want %d (wagers %d, paid %d)",
- loaded.Credits, want, 10*rounds, totalPaid)
- }
- if open := h.g.Economy.mustLoadOpen(t); len(open) != 0 {
- t.Errorf("records left open: %+v", open)
- }
- })
-}
-
-// fallbackProbeWallet routes PayCredits through the real wallet once with a
-// broken journal (the first settlement), then normally.
-type fallbackProbeWallet struct {
- harness *harness
- inner casinoWallet
- onPay func(credits int, failedCall bool)
- broke bool
-}
-
-func (w *fallbackProbeWallet) Wager(amount int) (casino.Wager, error) { return w.inner.Wager(amount) }
-
-func (w *fallbackProbeWallet) PayCredits(amount int) error {
- if w.broke {
- w.onPay(amount, false)
- return w.inner.PayCredits(amount)
- }
- w.broke = true
- w.onPay(amount, true)
- failJournalDuring(w.harness, func() {
- _ = w.inner.PayCredits(amount)
- })
- return nil
-}
-
-func (w *fallbackProbeWallet) ResolveStakes() { w.inner.ResolveStakes() }
-
-func (w *fallbackProbeWallet) PayStakeCover(amount int, stakeIDs []string) error {
- if w.broke {
- w.onPay(amount, false)
- return w.inner.PayStakeCover(amount, stakeIDs)
- }
- w.broke = true
- w.onPay(amount, true)
- failJournalDuring(w.harness, func() {
- _ = w.inner.PayStakeCover(amount, stakeIDs)
- })
- return nil
-}
-
-func (w *fallbackProbeWallet) ResolveStake(stakeID string) { w.inner.ResolveStake(stakeID) }
-func (w *fallbackProbeWallet) RefundOutstanding() (int, int, error) {
- return w.inner.RefundOutstanding()
-}
-
-// TestChipStakeSettlementFallbackNoDoublePay: a chip-funded stake whose win
-// is paid through the character-file fallback must not ALSO get its chips
-// returned in kind by recovery — the settlement returned the stake as
-// credits inside the payout.
-func TestChipStakeSettlementFallbackNoDoublePay(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
- }
- // The journal is broken for BOTH the settlement and the resolution:
- // the win lands in the character file, the resolution in evidence.
- failJournalDuring(h, func() {
- if err := w.PayCredits(20); err != nil {
- t.Errorf("fallback settlement failed: %v", err)
- return
- }
- w.ResolveStakes()
- })
- h.g.recoverEconomy()
- loaded, err := h.g.AccountStore.LoadCharacter("alice")
- if err != nil {
- t.Error(err)
- return
- }
- if loaded.Credits != 120 || countChips(loaded) != 0 {
- t.Errorf("after recovery: %d credits / %d chips, want 120 / 0 (chip stake debited, win of 20 paid once in credits; no in-kind chip return on top)",
- loaded.Credits, countChips(loaded))
- }
- if open := h.g.Economy.mustLoadOpen(t); len(open) != 0 {
- t.Errorf("open records: %+v", open)
- }
- })
-}
-
-// TestCorruptJournalAbortsRecoveryWithoutErasure: an unreadable journal
-// region must abort compensation and compaction — the owed payout record
-// beyond the corruption stays in the file for operator inspection instead
-// of being compacted away.
-func TestCorruptJournalAbortsRecoveryWithoutErasure(t *testing.T) {
- h := newHarness(t)
- h.seedAccount("alice")
- p := h.seedCharacter("alice", "alice")
- h.g.RunOnOwner(func() {
- p.Credits = 100
- if err := h.g.AccountStore.SaveCharacter(p); err != nil {
- t.Error(err)
- return
- }
- // Corrupt the journal with an oversized line, then journal an owed
- // payout BEYOND it (as appends would after the corruption).
- f, err := os.OpenFile(h.g.Economy.path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
- if err != nil {
- t.Error(err)
- return
- }
- _, werr := f.WriteString(strings.Repeat("x", 1024*1024+1) + "\n")
- f.Close()
- if werr != nil {
- t.Error(werr)
- return
- }
- if _, err := h.g.Economy.start("payout", p, 0, 20, 100, 120, 0, 0); err != nil {
- t.Errorf("payout could not be journaled: %v", err)
- return
- }
- h.g.recoverEconomy()
- // The owed payout record is preserved verbatim: no compensation ran
- // (credits unchanged) and no compaction erased the record.
- loaded, err := h.g.AccountStore.LoadCharacter("alice")
- if err != nil {
- t.Error(err)
- return
- }
- data, err := os.ReadFile(h.g.Economy.path)
- if err != nil {
- t.Fatal(err)
- }
- if loaded.Credits != 100 {
- t.Errorf("aborted recovery paid the payout: credits=%d, want 100 (preserved for inspection)", loaded.Credits)
- }
- if !strings.Contains(string(data), `"kind":"payout"`) {
- t.Error("corrupt journal recovery erased the owed payout record instead of preserving it")
- }
- })
-}
-
-// TestBankWithdrawalHonorsCreditCap: a withdrawal that would push the
-// on-hand balance past the shared economy cap is rejected; neither balance
-// changes.
-func TestBankWithdrawalHonorsCreditCap(t *testing.T) {
- h := newHarness(t)
- h.seedAccount("alice")
- p := h.seedCharacter("alice", "alice")
- p.Credits = casino.MaxCredits
- p.BankedCredits = 10
- if err := h.g.AccountStore.SaveCharacter(p); err != nil {
- t.Fatal(err)
- }
- sess, conn := h.connect("alice")
- h.g.RunOnOwner(func() {
- h.g.doBankWithdrawCredits(sess, []string{"withdraw", "credits", "10"})
- })
- if sess.Player.Credits != casino.MaxCredits || sess.Player.BankedCredits != 10 {
- t.Errorf("rejected withdrawal changed balances: credits=%d banked=%d",
- sess.Player.Credits, sess.Player.BankedCredits)
- }
- // The session writer goroutine flushes asynchronously; wait for the
- // rejection to arrive instead of racing it.
- deadline := time.Now().Add(2 * time.Second)
- for !conn.Contains("cannot hold") && time.Now().Before(deadline) {
- time.Sleep(time.Millisecond)
- }
- if !conn.Contains("cannot hold") {
- t.Errorf("withdrawal rejection not reported; output=%s", conn.Bytes())
- }
-}