diff options
Diffstat (limited to 'internal/game/economy_open_payout_list_test.go')
| -rw-r--r-- | internal/game/economy_open_payout_list_test.go | 120 |
1 files changed, 120 insertions, 0 deletions
diff --git a/internal/game/economy_open_payout_list_test.go b/internal/game/economy_open_payout_list_test.go new file mode 100644 index 0000000..893022f --- /dev/null +++ b/internal/game/economy_open_payout_list_test.go @@ -0,0 +1,120 @@ +package game + +// Regressions for the character-file payout-evidence scheme: an owed payout +// must never be erased because an unrelated credit change coincidentally +// lands on the record's post-payout balance, and a payout whose save became +// durable must never be re-applied (double-paid) just because its settled +// journal marker was lost. + +import ( + "os" + "testing" + + "thehouseoficarus/internal/casino" + "thehouseoficarus/internal/player" +) + +// newEvidenceHarness builds a minimal Game with just the account store and +// economy journal — enough for casinoWallet and recoverEconomy. +func newEvidenceHarness(t *testing.T) (*Game, *player.Player) { + t.Helper() + dir := t.TempDir() + if err := os.MkdirAll(dir+"/players/characters", 0o700); err != nil { + t.Fatal(err) + } + g := &Game{ + Deps: Deps{AccountStore: player.NewAccountStore(dir)}, + Economy: newEconomyJournal(dir), + } + p := player.New("alice") + p.ID = "stable-identity" + p.Credits = 90 + if err := g.AccountStore.SaveCharacter(p); err != nil { + t.Fatal(err) + } + return g, p +} + +// TestRecoveryReappliesPayoutWhenUnrelatedCreditsMaskTheBalance: a payout +// whose save failed leaves an OPEN record at balance 90 (want 110). A later, +// unrelated 20-credit gain brings the durable balance to 110 — the record's +// CreditsAfter. Recovery must read the character-file evidence (absent: the +// save never landed) and re-apply the payout, not settle it as "already +// durable" and silently erase the 20 owed credits. +func TestRecoveryReappliesPayoutWhenUnrelatedCreditsMaskTheBalance(t *testing.T) { + g, p := newEvidenceHarness(t) + + // Force the payout save to fail without touching its journal. + path := g.AccountStore.CharPath(p.Name) + backup := path + ".backup" + if err := os.Rename(path, backup); err != nil { + t.Fatal(err) + } + if err := os.Mkdir(path, 0o700); err != nil { + t.Fatal(err) + } + w := casinoWallet{g: g, p: p} + if err := w.PayCredits(20); err != casino.ErrWalletPersistence { + t.Fatalf("PayCredits error = %v, want ErrWalletPersistence", err) + } + if err := os.Remove(path); err != nil { + t.Fatal(err) + } + if err := os.Rename(backup, path); err != nil { + t.Fatal(err) + } + if p.Credits != 90 { + t.Fatalf("failed payout was paid: credits=%d, want 90", p.Credits) + } + + // The unrelated credit change: a shop sale, a bank withdrawal, anything. + p.Credits += 20 + if err := g.AccountStore.SaveCharacter(p); err != nil { + t.Fatal(err) + } + + g.recoverEconomy() + + loaded, err := g.AccountStore.LoadCharacter(p.Name) + if err != nil { + t.Fatal(err) + } + if loaded.Credits != 130 { + t.Fatalf("balance after recovery = %d, want 130 (owed payout re-applied once)", loaded.Credits) + } + if open := g.Economy.loadOpen(); len(open) != 0 { + t.Fatalf("records left open after recovery: %+v", open) + } +} + +// TestRecoverySettlesDurablePayoutWithoutReapplying: the payout save landed +// (the character file records the record ID in open_payouts) but the settled +// journal marker was lost in the crash. Recovery must settle from the file +// evidence — re-applying would double-pay the win. +func TestRecoverySettlesDurablePayoutWithoutReapplying(t *testing.T) { + g, p := newEvidenceHarness(t) + + rec, err := g.Economy.start("payout", p, 0, 20, 90, 110, 0, 0) + if err != nil { + t.Fatal(err) + } + // The save landed: balance and evidence both durable. + p.Credits = 110 + p.OpenPayouts = append(p.OpenPayouts, rec.ID) + if err := g.AccountStore.SaveCharacter(p); err != nil { + t.Fatal(err) + } + + g.recoverEconomy() + + loaded, err := g.AccountStore.LoadCharacter(p.Name) + if err != nil { + t.Fatal(err) + } + if loaded.Credits != 110 { + t.Fatalf("balance after recovery = %d, want 110 (durable payout not re-applied)", loaded.Credits) + } + if open := g.Economy.loadOpen(); len(open) != 0 { + t.Fatalf("records left open after recovery: %+v", open) + } +} |
