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)
}
}
|