From 6f3c43aca62a7d33a19f14f6ad7c16891952673a Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Tue, 15 Sep 2026 19:55:21 -0400 Subject: fix: outstanding-stake ledger, owner backpressure, editor state preservation --- internal/game/owner_regression_test.go | 68 ++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 internal/game/owner_regression_test.go (limited to 'internal/game/owner_regression_test.go') diff --git a/internal/game/owner_regression_test.go b/internal/game/owner_regression_test.go new file mode 100644 index 0000000..1ccd25f --- /dev/null +++ b/internal/game/owner_regression_test.go @@ -0,0 +1,68 @@ +package game + +// Single-owner serialization regressions: a full task queue applies +// backpressure instead of bypassing the owner, and the owner-stop path +// keeps its documented inline fallback for shutdown stragglers only. + +import ( + "sync" + "testing" + "time" +) + +// TestFullOwnerQueueAppliesBackpressure: when the owner's task queue is +// full and the owner is blocked inside another task, run() must NOT execute +// the new task inline on the caller's goroutine — that would mutate live +// state concurrently with the owner. It blocks until the owner drains. +func TestFullOwnerQueueAppliesBackpressure(t *testing.T) { + g := &Game{owner: newOwner()} + release := make(chan struct{}) + started := make(chan struct{}) + g.owner.submit(func() { + close(started) + <-release + }) + <-started + var once sync.Once + t.Cleanup(func() { + once.Do(func() { close(release) }) + g.stopOwner() + }) + + for i := 0; i < ownerTaskQueue; i++ { + if !g.owner.submit(func() {}) { + t.Fatal("setup: could not fill the owner queue") + } + } + + ran := make(chan struct{}) + go g.run(func() { close(ran) }) + select { + case <-ran: + t.Fatal("task executed inline while the owner was still running other tasks") + case <-time.After(100 * time.Millisecond): + } + + // Release the owner: the queued work drains and the blocked task runs. + once.Do(func() { close(release) }) + select { + case <-ran: + case <-time.After(5 * time.Second): + t.Fatal("task never ran after the owner drained its queue") + } +} + +// TestStoppedOwnerRunsStragglersInline: after the owner stops (graceful +// shutdown), straggler submissions execute inline rather than blocking +// forever — the documented admissions-closed behavior. +func TestStoppedOwnerRunsStragglersInline(t *testing.T) { + g := &Game{owner: newOwner()} + g.stopOwner() + ran := make(chan struct{}) + go g.run(func() { close(ran) }) + select { + case <-ran: + case <-time.After(5 * time.Second): + t.Fatal("straggler task blocked forever after the owner stopped") + } +} -- cgit v1.2.3