aboutsummaryrefslogtreecommitdiff
path: root/internal/game/economy_open_payout_list_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/game/economy_open_payout_list_test.go')
-rw-r--r--internal/game/economy_open_payout_list_test.go120
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)
+ }
+}