//go:build integration package inttest import ( "context" "testing" "github.com/google/uuid" "scrabble/backend/internal/account" "scrabble/backend/internal/engine" "scrabble/backend/internal/game" ) // The seat display-name snapshot freezes the name an opponent sees for the life of a // game (a later rename no longer rewrites past games) and lets a disguised robot carry a // fresh per-game name (docs/ARCHITECTURE.md ยง7). // seatByAccount returns the seat held by id and whether it was found. func seatByAccount(g game.Game, id uuid.UUID) (game.Seat, bool) { for _, s := range g.Seats { if s.AccountID == id { return s, true } } return game.Seat{}, false } // TestSeatNameFrozenAfterRename checks a human seat captures the player's display name // when the seat is taken and keeps it after the account is later renamed. func TestSeatNameFrozenAfterRename(t *testing.T) { ctx := context.Background() clearOpenGames(t) svc := newGameService() accounts := account.NewStore(testDB) starter := provisionAccount(t) if _, err := accounts.UpdateProfile(ctx, starter, account.ProfileUpdate{ DisplayName: "Original Name", PreferredLanguage: "en", TimeZone: "UTC", }); err != nil { t.Fatalf("set name: %v", err) } g := openGame(t, svc, starter, evenOpeningSeed(t)) seat, ok := seatByAccount(g, starter) if !ok || seat.DisplayName != "Original Name" { t.Fatalf("opened seat snapshot = %q (found %v), want %q", seat.DisplayName, ok, "Original Name") } // Rename the account; the snapshot on the already-taken seat must not follow. if _, err := accounts.UpdateProfile(ctx, starter, account.ProfileUpdate{ DisplayName: "Renamed Player99", PreferredLanguage: "en", TimeZone: "UTC", }); err != nil { t.Fatalf("rename: %v", err) } reloaded, err := svc.GameByID(ctx, g.ID) if err != nil { t.Fatalf("reload game: %v", err) } got, _ := seatByAccount(reloaded, starter) if got.DisplayName != "Original Name" { t.Errorf("seat snapshot after rename = %q, want it frozen at %q", got.DisplayName, "Original Name") } } // TestRobotSeatGetsComposedName checks the disguised auto-match robot's seat stores the // fresh per-game name composed at attach time (PickNamed -> AttachRobot -> seat), and // that the name is a valid human display name so the robot stays indistinguishable. func TestRobotSeatGetsComposedName(t *testing.T) { ctx := context.Background() clearOpenGames(t) svc := newGameService() robots := newRobotService(t, svc) if err := robots.EnsurePool(ctx); err != nil { t.Fatalf("ensure pool: %v", err) } g := openGame(t, svc, provisionAccount(t), evenOpeningSeed(t)) robotID, name, err := robots.PickNamed(engine.VariantEnglish) if err != nil { t.Fatalf("pick named: %v", err) } if _, err := account.ValidateDisplayName(name); err != nil { t.Fatalf("composed robot name %q is not a valid display name: %v", name, err) } if _, attached, err := svc.AttachRobot(ctx, g.ID, robotID, name); err != nil || !attached { t.Fatalf("attach robot = (attached %v, err %v), want attached", attached, err) } reloaded, err := svc.GameByID(ctx, g.ID) if err != nil { t.Fatalf("reload game: %v", err) } seat, ok := seatByAccount(reloaded, robotID) if !ok { t.Fatal("robot seat not found after attach") } if seat.DisplayName != name { t.Errorf("robot seat snapshot = %q, want the composed per-game name %q", seat.DisplayName, name) } }