aboutsummaryrefslogtreecommitdiff
path: root/internal/game/readiness_regression_test.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-09-16 19:57:45 -0400
committerhistoria <[not public]>2026-09-16 19:57:45 -0400
commit6ef40a5739f1723ec26cc32dfc48fe764f776dca (patch)
treee215a4dc96ea639bcea07bb589aae3e31c3609bb /internal/game/readiness_regression_test.go
parent1dad15698845d370e998f7cc5feb8aece3fab58f (diff)
downloadthehouseoficarus-6ef40a5739f1723ec26cc32dfc48fe764f776dca.tar.gz
fix: durable payouts survive journal failure, cap bank withdrawals, hash passwords off the owner loop.
Diffstat (limited to 'internal/game/readiness_regression_test.go')
-rw-r--r--internal/game/readiness_regression_test.go66
1 files changed, 41 insertions, 25 deletions
diff --git a/internal/game/readiness_regression_test.go b/internal/game/readiness_regression_test.go
index 7fa2b2e..3d21390 100644
--- a/internal/game/readiness_regression_test.go
+++ b/internal/game/readiness_regression_test.go
@@ -196,17 +196,19 @@ func TestRecoverySaveFailureRetriesOnNextStartup(t *testing.T) {
return
}
if loaded.Credits != 110 {
- t.Errorf("after fixing the disk and retrying: %d credits, want 110; open=%+v", loaded.Credits, h.g.Economy.loadOpen())
+ t.Errorf("after fixing the disk and retrying: %d credits, want 110; open=%+v", loaded.Credits, h.g.Economy.mustLoadOpen(t))
}
})
}
-// TestLostResolutionMarkerSurfacesAndRefunds: a resolution marker that
-// cannot be journaled (persistent append failure) leaves the stake OPEN —
-// never silently forgotten — and recovery then applies the documented
-// refund-on-lost-state policy (the player is never the loser of a lost
-// marker).
-func TestLostResolutionMarkerSurfacesAndRefunds(t *testing.T) {
+// TestResolvedStakeEvidenceSurvivesJournalFailure: a resolution marker
+// that cannot be journaled (persistent append failure) leaves the stake
+// OPEN — never silently forgotten — and records the resolution in the
+// character file as durable evidence. Recovery then settles the stake
+// WITHOUT refunding it: the round genuinely resolved, and the file is the
+// proof. When the file save also fails, no evidence exists and recovery
+// applies the documented refund-on-lost-state policy instead.
+func TestResolvedStakeEvidenceSurvivesJournalFailure(t *testing.T) {
h := newHarness(t)
h.seedAccount("alice")
p := h.seedCharacter("alice", "alice")
@@ -224,28 +226,40 @@ func TestLostResolutionMarkerSurfacesAndRefunds(t *testing.T) {
if open := h.g.Economy.openStakes(economyKey(p)); len(open) != 1 {
t.Errorf("unresolvable stake was silently dropped: open=%d", len(open))
}
- h.g.recoverEconomy()
+ // The resolution is recorded durably in the character file.
loaded, err := h.g.AccountStore.LoadCharacter("alice")
if err != nil {
t.Error(err)
return
}
- if loaded.Credits != 100 {
- t.Errorf("after recovery: credits=%d, want 100 (stake refunded; marker lost)", loaded.Credits)
+ if len(loaded.ResolvedStakes) != 1 {
+ t.Errorf("resolved-stake evidence not recorded: %+v", loaded.ResolvedStakes)
+ return
+ }
+ h.g.recoverEconomy()
+ loaded, err = h.g.AccountStore.LoadCharacter("alice")
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ if loaded.Credits != 90 {
+ t.Errorf("after recovery: credits=%d, want 90 (round resolved; evidence prevents a refund)", loaded.Credits)
}
- if open := h.g.Economy.loadOpen(); len(open) != 0 {
+ if open := h.g.Economy.mustLoadOpen(t); len(open) != 0 {
t.Errorf("records left open after recovery: %+v", open)
}
+ if len(loaded.ResolvedStakes) != 0 {
+ t.Errorf("stale resolved-stake evidence not pruned: %+v", loaded.ResolvedStakes)
+ }
})
}
-// TestUnjournalablePayoutRefundsStake: a payout that cannot even be
-// journaled (persistent append failure) is not announced as paid, the
-// balance is unchanged, and recovery applies the documented fallback: the
-// stake is refunded. The unrecorded win beyond the stake cannot be
-// protected, but the player never loses money and no win is announced that
-// is not durable.
-func TestUnjournalablePayoutRefundsStake(t *testing.T) {
+// TestUnjournalablePayoutPaidThroughCharacterFile: a payout that cannot be
+// journaled (persistent append failure) is still made durable through the
+// character file — the save is the commit point, so the win is real and
+// announced. The resolved stake is recorded as file evidence so recovery
+// neither refunds the stake (double pay) nor drops the round.
+func TestUnjournalablePayoutPaidThroughCharacterFile(t *testing.T) {
h := newHarness(t)
h.seedAccount("alice")
p := h.seedCharacter("alice", "alice")
@@ -260,23 +274,25 @@ func TestUnjournalablePayoutRefundsStake(t *testing.T) {
h.g.Economy.path = h.dir
err := w.PayCredits(20)
h.g.Economy.path = path
- if err == nil {
- t.Error("injected write failure did not fail")
+ if err != nil {
+ t.Errorf("character-file fallback failed: %v", err)
return
}
- if p.Credits != 90 {
- t.Errorf("failed payout mutated the balance: %d", p.Credits)
+ if p.Credits != 110 {
+ t.Errorf("fallback payout did not land: credits=%d, want 110", p.Credits)
}
+ // As the games do after a successful settlement: resolve the stake.
+ w.ResolveStakes()
h.g.recoverEconomy()
loaded, err := h.g.AccountStore.LoadCharacter("alice")
if err != nil {
t.Error(err)
return
}
- if loaded.Credits != 100 {
- t.Errorf("stake refund after unjournalable payout: credits=%d, want 100", loaded.Credits)
+ if loaded.Credits != 110 {
+ t.Errorf("after recovery: credits=%d, want 110 (win durable once; no stake refund on top)", loaded.Credits)
}
- if open := h.g.Economy.loadOpen(); len(open) != 0 {
+ if open := h.g.Economy.mustLoadOpen(t); len(open) != 0 {
t.Errorf("open records: %+v", open)
}
})