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