From f8efa21158f906685325a9fa5c178e2df5fa6be0 Mon Sep 17 00:00:00 2001 From: Ilia Denisov Date: Tue, 28 Jul 2026 15:47:19 +0200 Subject: [PATCH] test(inttest): de-flake the no-repeat pin guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bag seed comes from crypto/rand, so an opening rack occasionally has no playable word — a legitimate engine answer that says nothing about the pin the test is guarding. Reproduced at roughly one run in three hundred locally, and it turned CI red once. Retry with fresh games instead: an unlucky draw no longer decides the result, while a genuinely broken path still fails every attempt. --- backend/internal/inttest/norepeat_test.go | 48 +++++++++++++++-------- 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/backend/internal/inttest/norepeat_test.go b/backend/internal/inttest/norepeat_test.go index 52d6f6a..c6903a1 100644 --- a/backend/internal/inttest/norepeat_test.go +++ b/backend/internal/inttest/norepeat_test.go @@ -4,6 +4,7 @@ package inttest import ( "context" + "errors" "testing" "github.com/google/uuid" @@ -64,23 +65,38 @@ func TestNoRepeatWordsPinnedFromTheVariant(t *testing.T) { func TestNoRepeatWordsHonoursThePinNotTheVariant(t *testing.T) { ctx := context.Background() svc := newGameService() - g := createTwoSeatGame(t, svc, engine.VariantErudit) - if _, err := testDB.ExecContext(ctx, - `UPDATE backend.games SET no_repeat_words = false WHERE game_id = $1`, g.ID); err != nil { - t.Fatalf("clear the pin: %v", err) - } + // Each game draws from a bag seeded with crypto/rand, so an opening rack occasionally has no + // playable word at all. That is a legitimate engine answer which says nothing about the pin + // under test, and taking it for a failure made this test flake (~1 run in 300 on the committed + // dictionary, and more often on another one). Retry with fresh games instead: one unlucky draw + // no longer decides the result, while a genuinely broken path still fails every attempt. + const draws = 5 + for attempt := 1; ; attempt++ { + g := createTwoSeatGame(t, svc, engine.VariantErudit) + if _, err := testDB.ExecContext(ctx, + `UPDATE backend.games SET no_repeat_words = false WHERE game_id = $1`, g.ID); err != nil { + t.Fatalf("clear the pin: %v", err) + } - reloaded, err := svc.GameByID(ctx, g.ID) - if err != nil { - t.Fatalf("get game: %v", err) - } - if reloaded.NoRepeatWords { - t.Fatal("an Erudit game whose row has the rule off must report it off") - } - // The rule reaches the engine from the row, so the reconstructed game plays unrestricted: - // its generated moves are the solver's full list, none filtered away. - if _, err := svc.Hint(ctx, reloaded.ID, reloaded.Seats[reloaded.ToMove].AccountID); err != nil { - t.Fatalf("hint on an unrestricted Erudit game: %v", err) + reloaded, err := svc.GameByID(ctx, g.ID) + if err != nil { + t.Fatalf("get game: %v", err) + } + if reloaded.NoRepeatWords { + t.Fatal("an Erudit game whose row has the rule off must report it off") + } + // The rule reaches the engine from the row, so the reconstructed game plays unrestricted: + // its generated moves are the solver's full list, none filtered away. + _, err = svc.Hint(ctx, reloaded.ID, reloaded.Seats[reloaded.ToMove].AccountID) + if err == nil { + return + } + if !errors.Is(err, game.ErrNoHintAvailable) { + t.Fatalf("hint on an unrestricted Erudit game: %v", err) + } + if attempt == draws { + t.Fatalf("no opening rack in %d games had a playable move", draws) + } } }