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
|
package game
// Rollback-failure regressions for the durable money journal. When a journal
// append fails, the invariant was historically "append failed means no record
// bytes remain". A failed rollback (the truncate, or the sync that makes it
// durable) breaks that invariant: the record can survive while the caller
// believes the append failed. The wallet's journal-unavailable payout fallback
// must then still record the attempted record's ID as open-payout evidence, or
// recovery re-applies the surviving payout and pays the player twice.
import (
"encoding/json"
"errors"
"os"
"path/filepath"
"testing"
)
// TestAppendRollbackFailureIsUncertain: /dev/full accepts the open but fails
// the write with ENOSPC and the rollback truncate with EINVAL, so the append
// must report that the record MAY have survived.
func TestAppendRollbackFailureIsUncertain(t *testing.T) {
if _, err := os.Stat("/dev/full"); err != nil {
t.Skipf("/dev/full is not available: %v", err)
}
j := newEconomyJournal(t.TempDir())
j.path = "/dev/full"
err := j.append(economyRecord{ID: "uncertain", Kind: "payout", Status: "open"})
if !errors.Is(err, errJournalAppendUncertain) {
t.Fatalf("append rollback failure = %v, want errJournalAppendUncertain", err)
}
}
// TestFallbackPayoutRecordsEvidenceForSurvivingRecord: a payout whose journal
// append fails and whose rollback cannot be proven must leave open-payout
// evidence in the character file. A surviving record is then settled from that
// evidence at the next startup instead of being re-applied.
func TestFallbackPayoutRecordsEvidenceForSurvivingRecord(t *testing.T) {
g, p := newEvidenceHarness(t)
// Break the journal append: point it at its own directory, which cannot
// be opened for append or truncation. The wallet takes the file-only
// fallback.
journalPath := g.Economy.path
g.Economy.path = filepath.Dir(journalPath)
w := casinoWallet{g: g, p: p}
if err := w.PayCredits(20); err != nil {
t.Fatalf("fallback payout failed: %v", err)
}
if p.Credits != 110 {
t.Fatalf("fallback payout not applied: credits=%d, want 110", p.Credits)
}
if len(p.OpenPayouts) != 1 {
t.Fatalf("fallback payout recorded %d open-payout evidence IDs, want 1: %v",
len(p.OpenPayouts), p.OpenPayouts)
}
id := p.OpenPayouts[0]
// Simulate the failed append having survived in the journal (the exact
// state a failed rollback produces). Recovery must use the file evidence
// and settle it, not re-apply it.
g.Economy.path = journalPath
rec := economyRecord{
ID: id, Player: p.Name, PlayerID: p.ID, Kind: "payout", Tracked: true,
Credit: 20, Status: "open", CreditsBefore: 90, CreditsAfter: 110,
}
line, err := json.Marshal(rec)
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(journalPath, append(line, '\n'), 0o600); 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 (surviving payout settled from evidence, not re-applied)",
loaded.Credits)
}
if open := g.Economy.mustLoadOpen(t); len(open) != 0 {
t.Fatalf("records left open after recovery: %+v", open)
}
}
|