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") } }