test(e2e): unblock the 3 fixme tests in modifier-profiles spec

P6 (source chain in pinned panel):
  Required two server-side changes to make the badge actually meaningful:
  - Add `profile` field to GameStatePayload schema (server emits it,
    client receives it) so multiplayer clients see the room's active
    profile metadata, not just the modifier facts.
  - Make `ChessEngine.activeProfile` mutable via `setActiveProfile()`
    so PredictionManager can sync it from `game.state` snapshots.
  Also wire `modifier-profile.updated` through GameClient + Prediction-
  Manager so hot-swap broadcasts update the engine's profile field
  reactively.
  Fix Lobby.handlePlaySolo's resetToFreshGame to forward the selected
  profile to the new ChessEngine — otherwise the local engine had
  modifier facts (via server reconcile) but no profile metadata,
  breaking source-chain attribution and any other profile-aware UI.

P7 (multiplayer propose → approve → both observe updated):
P8 (multiplayer propose → reject → no updated broadcast):
  Implemented at the WS-protocol level using two parallel raw sockets
  per test (mirrors multiplayer.spec.ts pattern). Critical sequencing:
  - Both sockets opened concurrently via Promise.all so opponent is
    listening BEFORE host's propose arrives at the server (otherwise
    proposal-pending broadcasts to nobody and the test deadlocks).
  - Token must travel at the envelope level, not in payload, for the
    server's reconnect-by-token path to fire (otherwise hits ROOM_FULL
    on the second connection from each player).
  - game.move payload uses algebraic notation strings ('a2', 'a3'), not
    square indices — the protocol schema only accepts strings.
  - Host re-uses original room.create token, opponent re-uses their
    join token. Server's reconnectManager treats both as grace-window
    reconnects since the original WS closed cleanly.

Verification:
  - 1231 unit tests pass (96 files)
  - 58/58 Playwright tests pass in 1.9 min (was 55 + 3 fixme)
  - Total Playwright surface coverage: solo-smoke (7) + multiplayer (2) +
    full-flow (1) + layouts (24) + modifier-profiles (24 — including all
    8 T2-polish tests, 0 fixme).
This commit is contained in:
Joey Yakimowich-Payne 2026-04-19 13:15:15 -06:00
commit 567480a788
No known key found for this signature in database
8 changed files with 647 additions and 42 deletions

View file

@ -487,6 +487,7 @@ function handleRoomJoin(
.error("room.join: missing session for existing room");
return;
}
const activeProfile = session.getProfile();
broadcastToRoom(
payload.code,
envelope("game.state", {
@ -496,6 +497,7 @@ function handleRoomJoin(
moveHistory: [],
activeRules: [...result.activeRules],
activations: session.getPresetActivations(),
...(activeProfile !== undefined ? { profile: activeProfile } : {}),
// fen is a UI convenience for v1; we haven't wired FEN generation
// on the server yet, so we send an empty string. Clients that need
// FEN can derive it from `facts`.
@ -573,6 +575,7 @@ function handleReconnect(
// Snapshot: authoritative state for the returning client. The client
// discards its local fact store and rebuilds from this frame.
const reconnectActiveProfile = session.getProfile();
sendTo(
ws,
envelope("game.state", {
@ -586,6 +589,7 @@ function handleReconnect(
moveHistory: [],
activeRules: [...room.rulesetIds],
activations: session.getPresetActivations(),
...(reconnectActiveProfile !== undefined ? { profile: reconnectActiveProfile } : {}),
fen: "",
}),
);

View file

@ -545,6 +545,11 @@ export const GameStatePayloadSchema = z.object({
/** Full preset activation set authoritative on the server. Optional
* on the wire so older servers don't break the schema check. */
activations: z.array(PresetActivationSchema).optional(),
/** Active modifier profile for the room (T2). Optional — absent for
* vanilla rooms or when the server hasn't been told about a profile.
* Clients use this to populate `engine.activeProfile` so source-chain
* attribution and other profile-aware UI works on snapshot replay. */
profile: ModifierProfileSchema.optional(),
fen: z.string(),
});
export type GameStatePayload = z.infer<typeof GameStatePayloadSchema>;