f8efa21158
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 12s
CI / integration (pull_request) Successful in 21s
CI / ui (pull_request) Successful in 1m16s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m51s
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.
103 lines
3.6 KiB
Go
103 lines
3.6 KiB
Go
//go:build integration
|
|
|
|
package inttest
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"scrabble/backend/internal/engine"
|
|
"scrabble/backend/internal/game"
|
|
)
|
|
|
|
// createTwoSeatGame starts a plain two-human game of the given variant.
|
|
func createTwoSeatGame(t *testing.T, svc *game.Service, variant engine.Variant) game.Game {
|
|
t.Helper()
|
|
g, err := svc.Create(context.Background(), game.CreateParams{
|
|
Variant: variant,
|
|
Seats: []uuid.UUID{provisionAccount(t), provisionAccount(t)},
|
|
HintsAllowed: true,
|
|
HintsPerPlayer: 1,
|
|
MultipleWordsPerTurn: true,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create %s game: %v", variant, err)
|
|
}
|
|
return g
|
|
}
|
|
|
|
// TestNoRepeatWordsPinnedFromTheVariant checks the rule is written into games.no_repeat_words at
|
|
// creation from the variant — on for Erudit, off for both Scrabble variants — and read back with
|
|
// the game rather than re-derived.
|
|
func TestNoRepeatWordsPinnedFromTheVariant(t *testing.T) {
|
|
svc := newGameService()
|
|
for _, tc := range []struct {
|
|
variant engine.Variant
|
|
want bool
|
|
}{
|
|
{engine.VariantErudit, true},
|
|
{engine.VariantRussianScrabble, false},
|
|
{engine.VariantEnglish, false},
|
|
} {
|
|
t.Run(tc.variant.String(), func(t *testing.T) {
|
|
created := createTwoSeatGame(t, svc, tc.variant)
|
|
if created.NoRepeatWords != tc.want {
|
|
t.Errorf("created game NoRepeatWords = %v, want %v", created.NoRepeatWords, tc.want)
|
|
}
|
|
// Re-read it: the value must come from the row, not from the create call.
|
|
reloaded, err := svc.GameByID(context.Background(), created.ID)
|
|
if err != nil {
|
|
t.Fatalf("get game: %v", err)
|
|
}
|
|
if reloaded.NoRepeatWords != tc.want {
|
|
t.Errorf("reloaded game NoRepeatWords = %v, want %v", reloaded.NoRepeatWords, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestNoRepeatWordsHonoursThePinNotTheVariant is the compatibility guard for every Erudit game
|
|
// that existed before the rule: those rows carry no_repeat_words false (the migration's default),
|
|
// and such a game must keep playing unrestricted. Flipping the stored flag stands in for one.
|
|
func TestNoRepeatWordsHonoursThePinNotTheVariant(t *testing.T) {
|
|
ctx := context.Background()
|
|
svc := newGameService()
|
|
|
|
// 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.
|
|
_, 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)
|
|
}
|
|
}
|
|
}
|