aboutsummaryrefslogtreecommitdiff
path: root/internal/game/economy_shutdown_test.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-09-18 21:36:36 -0400
committerhistoria <[not public]>2026-09-18 21:36:36 -0400
commita8a67482bbff8297a96adff65d2fd7029ed998cc (patch)
treee84f8ba636c8f2edb4d2ff8b45d7cff7298fa7cc /internal/game/economy_shutdown_test.go
parentff04d26ce90d6b411a04a2fb59f7c24ceefb161f (diff)
downloadthehouseoficarus-a8a67482bbff8297a96adff65d2fd7029ed998cc.tar.gz
refactor: replace economy journal with single-file pending-wager wallet
Diffstat (limited to 'internal/game/economy_shutdown_test.go')
-rw-r--r--internal/game/economy_shutdown_test.go273
1 files changed, 0 insertions, 273 deletions
diff --git a/internal/game/economy_shutdown_test.go b/internal/game/economy_shutdown_test.go
deleted file mode 100644
index 27190fb..0000000
--- a/internal/game/economy_shutdown_test.go
+++ /dev/null
@@ -1,273 +0,0 @@
-package game
-
-// Durable-money and graceful-shutdown regressions: journal-first wagers
-// with revert on save failure, startup recovery that refunds lost wagers
-// and re-pays lost payouts, checked currency arithmetic, and a shutdown
-// that persists online characters.
-
-import (
- "encoding/json"
- "os"
- "path/filepath"
- "testing"
- "time"
-
- "thehouseoficarus/internal/casino"
- "thehouseoficarus/internal/config"
- "thehouseoficarus/internal/net"
- "thehouseoficarus/internal/player"
-
- "gopkg.in/yaml.v3"
-)
-
-// harnessFromDir builds a Game over an existing directory (used when tests
-// must pre-seed character files and journal records before startup).
-func harnessFromDir(t *testing.T, dir string) *harness {
- t.Helper()
- colors := &config.ColorsConfig{}
- g := New(dir, colors, colors, config.ValidationConfig{}, 1)
- hub := net.NewHub()
- g.SetHub(hub)
- stopTick := make(chan struct{})
- done := make(chan struct{})
- go func() {
- defer close(done)
- tick := time.NewTicker(2 * time.Millisecond)
- defer tick.Stop()
- for {
- select {
- case <-tick.C:
- g.Ticks.Tick()
- case <-stopTick:
- return
- }
- }
- }()
- h := &harness{
- t: t, dir: dir, g: g, hub: hub,
- sessions: make(map[string]*net.Session),
- conns: make(map[string]*chanConn),
- }
- h.stop = func() { close(stopTick); <-done }
- t.Cleanup(h.stop)
- return h
-}
-
-func writeSeedFile(t *testing.T, path string, data []byte) {
- t.Helper()
- if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
- t.Fatal(err)
- }
- if err := os.WriteFile(path, data, 0600); err != nil {
- t.Fatal(err)
- }
-}
-
-func writeEconomyJournal(t *testing.T, dir string, records []economyRecord) {
- t.Helper()
- if err := os.MkdirAll(filepath.Join(dir, "economy"), 0700); err != nil {
- t.Fatal(err)
- }
- var data []byte
- for _, rec := range records {
- line, err := json.Marshal(rec)
- if err != nil {
- t.Fatal(err)
- }
- data = append(data, line...)
- }
- if err := os.WriteFile(filepath.Join(dir, "economy", "journal.jsonl"), data, 0600); err != nil {
- t.Fatal(err)
- }
-}
-
-func TestEconomyWagerDebitsDurallyAndSettles(t *testing.T) {
- h := newHarness(t)
- h.seedAccount("alice")
- p := h.seedCharacter("alice", "alice")
- h.g.RunOnOwner(func() {
- p.Credits = 100
- })
- w := casinoWallet{g: h.g, p: p}
-
- var wagerErr error
- h.g.RunOnOwner(func() {
- _, wagerErr = w.Wager(10)
- })
- h.barrier()
- if wagerErr != nil {
- t.Fatalf("wager failed: %v", wagerErr)
- }
- if p.Credits != 90 {
- t.Fatalf("credits after wager = %d, want 90", p.Credits)
- }
- // The debit is durable, and the record stays OPEN: the stake is
- // outstanding until the round resolves (recovery refunds open stakes
- // after a crash).
- if open := h.g.Economy.openStakes("alice"); len(open) != 1 || open[0].Credit != 10 {
- t.Fatalf("outstanding stake after a successful wager: %+v", open)
- }
- // Resolution (round resolved or refunded) closes it.
- h.g.RunOnOwner(func() {
- w.ResolveStakes()
- })
- h.barrier()
- if open := h.g.Economy.openStakes("alice"); len(open) != 0 {
- t.Fatalf("open stakes remain after resolution: %v", open)
- }
-}
-
-func TestEconomyFailedSaveRevertsAndVoids(t *testing.T) {
- h := newHarness(t)
- h.seedAccount("alice")
- p := h.seedCharacter("alice", "alice")
- h.g.RunOnOwner(func() {
- p.Credits = 100
- })
-
- // Make character saves fail: read-only characters directory.
- charDir := filepath.Join(h.dir, "players", "characters")
- if err := os.Chmod(charDir, 0500); err != nil {
- t.Fatal(err)
- }
- t.Cleanup(func() { os.Chmod(charDir, 0700) })
-
- var wagerErr error
- h.g.RunOnOwner(func() {
- _, wagerErr = casinoWallet{g: h.g, p: p}.Wager(10)
- })
- h.barrier()
- if wagerErr != casino.ErrWalletPersistence {
- t.Fatalf("wager error = %v, want ErrWalletPersistence", wagerErr)
- }
- // The in-memory mutation must have been reverted: nothing was charged.
- if p.Credits != 100 {
- t.Fatalf("credits after failed wager = %d, want 100 (reverted)", p.Credits)
- }
- // The record is void, not open: recovery must not compensate.
- os.Chmod(charDir, 0700)
- if open := h.g.Economy.mustLoadOpen(t); len(open) != 0 {
- t.Fatalf("void record remained open: %v", open)
- }
-}
-
-func TestEconomyRecoveryRefundsDurableLostWager(t *testing.T) {
- dir := t.TempDir()
- seedWorld(t, dir)
-
- // Character with the debit already durable (crash after save).
- hash, _ := player.HashPassword("pw")
- accData, _ := yaml.Marshal(&player.Account{Name: "alice", PasswordHash: hash})
- writeSeedFile(t, filepath.Join(dir, "players", "accounts", "alice.yaml"), accData)
- p := player.New("alice")
- p.Credits = 90 // post-debit balance
- p.Account = "alice"
- pData, _ := yaml.Marshal(p)
- writeSeedFile(t, filepath.Join(dir, "players", "characters", "alice.yaml"), pData)
-
- // Open wager record: 10 credits debited (before=100, after=90).
- writeEconomyJournal(t, dir, []economyRecord{{
- ID: "lost-wager", Player: "alice", Kind: "wager",
- Chips: 0, Credit: 10, Status: "open",
- CreditsBefore: 100, CreditsAfter: 90,
- ChipsBefore: 0, ChipsAfter: 0,
- }})
-
- h := harnessFromDir(t, dir)
- // Recovery must refund the stake: credits back to 100.
- loaded, err := h.g.AccountStore.LoadCharacter("alice")
- if err != nil {
- t.Fatal(err)
- }
- if loaded.Credits != 100 {
- t.Fatalf("credits after recovery = %d, want 100 (refund)", loaded.Credits)
- }
- if open := h.g.Economy.mustLoadOpen(t); len(open) != 0 {
- t.Fatalf("recovered record left open: %v", open)
- }
-}
-
-func TestEconomyRecoveryAppliesLostPayout(t *testing.T) {
- dir := t.TempDir()
- seedWorld(t, dir)
-
- // Character at pre-payout balance; the payout was journaled but the
- // save never happened (crash before save). The player is owed it.
- hash, _ := player.HashPassword("pw")
- accData, _ := yaml.Marshal(&player.Account{Name: "bob", PasswordHash: hash})
- writeSeedFile(t, filepath.Join(dir, "players", "accounts", "bob.yaml"), accData)
- p := player.New("bob")
- p.Credits = 50
- p.Account = "bob"
- pData, _ := yaml.Marshal(p)
- writeSeedFile(t, filepath.Join(dir, "players", "characters", "bob.yaml"), pData)
-
- writeEconomyJournal(t, dir, []economyRecord{{
- ID: "lost-payout", Player: "bob", Kind: "payout",
- Chips: 0, Credit: 25, Status: "open",
- CreditsBefore: 50, CreditsAfter: 75,
- ChipsBefore: 0, ChipsAfter: 0,
- }})
-
- h := harnessFromDir(t, dir)
- loaded, err := h.g.AccountStore.LoadCharacter("bob")
- if err != nil {
- t.Fatal(err)
- }
- if loaded.Credits != 75 {
- t.Fatalf("credits after recovery = %d, want 75 (payout re-applied)", loaded.Credits)
- }
- if open := h.g.Economy.mustLoadOpen(t); len(open) != 0 {
- t.Fatalf("recovered record left open: %v", open)
- }
-}
-
-func TestShutdownSavesOnlineCharactersAndRefundsWagers(t *testing.T) {
- h := newHarness(t)
- h.seedAccount("alice")
- h.seedAccount("bob")
- h.seedCharacter("alice", "alice")
- h.seedCharacter("bob", "bob")
- sA, _ := h.connect("alice")
- pA := sA.Player
- sB, _ := h.connect("bob")
- pB := sB.Player
-
- // bob sits at a machine with a committed stake; shutdown must refund.
- h.g.RunOnOwner(func() {
- pB.Credits = 100
- h.g.Casino.JoinMachine(1, casino.MachineConfig{ID: "slots", Game: casino.GameSlots, MinBet: 1}, "bob")
- h.g.Casino.SetMachineBet(1, "slots", "bob", 20, casinoWallet{g: h.g, p: pB})
- })
- h.barrier()
- if pB.Credits != 80 {
- t.Fatalf("bob's credits after wager = %d, want 80", pB.Credits)
- }
-
- transportClosed := make(chan struct{})
- h.g.SetTransportStop(func() { close(transportClosed) })
- h.g.RequestShutdown("test")
- // The shutdown goroutine closes the transports at the end of the
- // ordered sequence; block for it.
- select {
- case <-transportClosed:
- case <-time.After(5 * time.Second):
- t.Fatal("transport stop was never invoked during shutdown")
- }
-
- // Both characters must be saved with the wager refunded.
- loadedA, err := h.g.AccountStore.LoadCharacter("alice")
- if err != nil {
- t.Fatal(err)
- }
- if loadedA.Credits != pA.Credits {
- t.Fatalf("alice save mismatch: file %d, memory %d", loadedA.Credits, pA.Credits)
- }
- loadedB, err := h.g.AccountStore.LoadCharacter("bob")
- if err != nil {
- t.Fatal(err)
- }
- if loadedB.Credits != 100 {
- t.Fatalf("bob's file = %d, want 100 (stake refunded at shutdown)", loadedB.Credits)
- }
-}