From 97ce177dd3eaf6a38eb3ede23f96a5c0603364fb Mon Sep 17 00:00:00 2001 From: RissRIce Date: Wed, 12 Aug 2026 20:59:47 -0600 Subject: [PATCH] test(games): reproduce duplicate queue deadlock --- internal/games/matchmaker_test.go | 40 +++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/internal/games/matchmaker_test.go b/internal/games/matchmaker_test.go index d0db8a2..665fd06 100644 --- a/internal/games/matchmaker_test.go +++ b/internal/games/matchmaker_test.go @@ -2,6 +2,7 @@ package games import ( "context" + "errors" "math" "sync" "testing" @@ -133,3 +134,42 @@ func TestMatchmakerUnknownGame(t *testing.T) { t.Fatalf("want ErrUnknownGame, got %v", err) } } + +func TestMatchmakerRejectsDuplicateWaiter(t *testing.T) { + mm := NewMatchmaker(Catalog(), nil, time.Second, time.Minute) + firstCtx, cancelFirst := context.WithCancel(context.Background()) + firstDone := make(chan error, 1) + go func() { + firstDone <- mm.Play(firstCtx, "ttt", &firstLegalPlayer{name: "same-agent"}) + }() + + deadline := time.Now().Add(time.Second) + for { + mm.mu.Lock() + queued := mm.queue["ttt"] != nil + mm.mu.Unlock() + if queued { + break + } + if time.Now().After(deadline) { + t.Fatal("first player never entered the queue") + } + time.Sleep(time.Millisecond) + } + + duplicateCtx, cancelDuplicate := context.WithTimeout(context.Background(), 50*time.Millisecond) + defer cancelDuplicate() + if err := mm.Play(duplicateCtx, "ttt", &firstLegalPlayer{name: "same-agent"}); err == nil || errors.Is(err, context.DeadlineExceeded) { + t.Fatalf("duplicate Play error = %v, want immediate already-queued rejection", err) + } + + cancelFirst() + select { + case err := <-firstDone: + if !errors.Is(err, context.Canceled) { + t.Fatalf("first Play error = %v, want context.Canceled", err) + } + case <-time.After(100 * time.Millisecond): + t.Fatal("first player remained blocked after its context was canceled") + } +}