From 153acb7f644d755c45533b7c5311e3ffa47e8973 Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Fri, 18 Sep 2026 05:31:09 -0400 Subject: fix: harden engine across casino, economy, concurrency, security, and content --- .../game/hardening_purge_session_reset_test.go | 50 ++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 internal/game/hardening_purge_session_reset_test.go (limited to 'internal/game/hardening_purge_session_reset_test.go') diff --git a/internal/game/hardening_purge_session_reset_test.go b/internal/game/hardening_purge_session_reset_test.go new file mode 100644 index 0000000..c3462a6 --- /dev/null +++ b/internal/game/hardening_purge_session_reset_test.go @@ -0,0 +1,50 @@ +package game + +// Regression (P0): a purged account must leave every one of its sessions in a +// safe protocol state. A queued-mode Close only drops the transport after the +// writer drains its outbox, so input already buffered in the connection can +// still be dispatched to the owner after the purge; a session left in the +// character menu with a nil account then dereferences sess.Account and kills +// the owner goroutine (and with it every session and all unsaved state). + +import ( + "testing" + + "thehouseoficarus/internal/net" +) + +func TestPurgeResetsEveryAccountSession(t *testing.T) { + h := newHarness(t) + h.seedAccount("alice") + h.seedCharacter("alice", "alice") + + other := h.connectMenu("alice") + purger := h.connectMenu("alice") + + h.g.RunOnOwner(func() { h.g.handlePurgeAccount(purger, "PURGE alice") }) + + for name, sess := range map[string]*net.Session{"other": other, "purger": purger} { + if got := sess.State(); got != net.StateAccountName { + t.Errorf("%s session state = %v, want StateAccountName", name, got) + } + if sess.Account != nil { + t.Errorf("%s session retained the purged account: %+v", name, sess.Account) + } + } + + // Input already buffered in either transport is still routed to the owner: + // it must land on the account-name prompt instead of faulting. + before := h.g.owner.panics.Load() + h.g.RunOnOwner(func() { + h.g.handleMenu(other, "c") + h.g.handleMenu(other, "l") + h.g.handlePurgeAccount(purger, "PURGE alice") + h.g.handleNewCharName(purger, "bob") + }) + if got := h.g.owner.panics.Load(); got != before { + t.Fatalf("post-purge input panicked the owner (panic count %d -> %d)", before, got) + } + if got := other.State(); got != net.StateAccountName { + t.Errorf("state = %v after post-purge input, want StateAccountName", got) + } +} -- cgit v1.2.3