diff options
| author | historia <[not public]> | 2026-09-18 05:31:09 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-09-18 05:31:09 -0400 |
| commit | 153acb7f644d755c45533b7c5311e3ffa47e8973 (patch) | |
| tree | 7645e6f7b65bc3830b078e3b9336d2e6b199d584 /internal/game/correct_owner_panic_subscription_test.go | |
| parent | 87d08f0151e643951769e323995f1c5ed89ee200 (diff) | |
| download | thehouseoficarus-153acb7f644d755c45533b7c5311e3ffa47e8973.tar.gz | |
fix: harden engine across casino, economy, concurrency, security, and content
Diffstat (limited to 'internal/game/correct_owner_panic_subscription_test.go')
| -rw-r--r-- | internal/game/correct_owner_panic_subscription_test.go | 85 |
1 files changed, 85 insertions, 0 deletions
diff --git a/internal/game/correct_owner_panic_subscription_test.go b/internal/game/correct_owner_panic_subscription_test.go new file mode 100644 index 0000000..f535dcc --- /dev/null +++ b/internal/game/correct_owner_panic_subscription_test.go @@ -0,0 +1,85 @@ +package game + +// Regression: the owner loop contains a panic in a tick callback, so the tick +// engine no longer sees it. SubscribeTicks must therefore treat "the callback +// did not run to completion" as KEEP - a false return unsubscribes the system, +// and the master subscriber main.go registers (RunTickSystems) is what drains +// queued commands, settles casino rounds, and cleans up disconnects. Likewise +// RunAdminOnOwner must not report a contained panic as "game is shutting down". + +import ( + "testing" + + "thehouseoficarus/internal/engine" +) + +func TestSubscribeTicksKeepsSubscriptionWhenCallbackPanics(t *testing.T) { + g := &Game{owner: newOwner()} + g.Ticks = engine.New() + t.Cleanup(g.stopOwner) + + calls := 0 + g.SubscribeTicks(1, func() bool { + calls++ + if calls == 1 { + panic("tick system failure") + } + return false + }) + + g.Ticks.Tick() + // The panicking task's runTask (recover, log, counter) has not finished + // when the blocked caller is released, so drain the queue before reading + // the counter: the owner processes tasks FIFO. + g.run(func() {}) + if got := g.owner.panics.Load(); got != 1 { + t.Fatalf("contained panics = %d, want 1", got) + } + if calls != 1 { + t.Fatalf("callback calls = %d, want 1", calls) + } + + // The subscription must still be live: a deleted one never fires again, + // which would leave the world frozen while the process keeps running. + g.Ticks.Tick() + if calls != 2 { + t.Fatalf("a contained panic unsubscribed the system: calls = %d, want 2", calls) + } + + // An explicit false must still unsubscribe. + g.Ticks.Tick() + if calls != 2 { + t.Fatalf("explicit false did not unsubscribe: calls = %d, want 2", calls) + } +} + +func TestRunAdminOnOwnerReportsContainedPanicToCaller(t *testing.T) { + g := &Game{owner: newOwner()} + t.Cleanup(g.stopOwner) + + func() { + defer func() { + if r := recover(); r != adminTaskPanic { + t.Errorf("recovered %v, want the contained-task-panic value", r) + } + }() + if g.RunAdminOnOwner(func() { panic("editor task failure") }) { + t.Error("a panicking editor task was reported as accepted") + } + t.Fatal("a contained panic was reported as a normal result") + }() + + g.run(func() {}) + if got := g.owner.panics.Load(); got != 1 { + t.Errorf("contained panics = %d, want 1", got) + } + // The owner loop must still be draining, and the game must not have been + // flagged as shutting down by the panic. + ran := false + if !g.RunAdminOnOwner(func() { ran = true }) { + t.Fatal("the owner stopped accepting editor tasks after a contained panic") + } + if !ran { + t.Fatal("the owner loop did not execute later editor tasks") + } +} |
