aboutsummaryrefslogtreecommitdiff
path: root/internal/game/owner_regression_test.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-09-15 19:55:21 -0400
committerhistoria <[not public]>2026-09-15 19:55:21 -0400
commit6f3c43aca62a7d33a19f14f6ad7c16891952673a (patch)
tree0b0665ef624ef442df87ec7131129aaecc3420bd /internal/game/owner_regression_test.go
parentcd95de60364aba8f5bdf8ec86f7234a9903eacc8 (diff)
downloadthehouseoficarus-6f3c43aca62a7d33a19f14f6ad7c16891952673a.tar.gz
fix: outstanding-stake ledger, owner backpressure, editor state preservation
Diffstat (limited to 'internal/game/owner_regression_test.go')
-rw-r--r--internal/game/owner_regression_test.go68
1 files changed, 68 insertions, 0 deletions
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")
+ }
+}