aboutsummaryrefslogtreecommitdiff
path: root/internal/game/economy_open_payout_list_test.go
blob: 893022fb31e2e2bab66d7be6703eec6bedf1b5ca (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
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)
	}
}