Specify Poker Game Stall Triage
Quick Checks (Fast Signal)
- •Fetch state from Gateway:
- •
curl -sS -H "Authorization: Bearer $TOKEN" "http://localhost:4000/api/tables/$TABLE_ID/state"
- •
- •Validate invariants:
- •
state.handexists once 2+ players are seated. - •
state.hand.turnpoints at a seat withuser_idandstatus: "ACTIVE". - •A single
user_idappears in at most one seat (no duplicate seating).
- •
- •If the UI shows “Watching” behavior (no actions / no hole cards), confirm you’re seated:
- •Your
user_idshould appear instate.seats[*].user_id.
- •Your
Generate a Local Dev Token (Compose / E2E)
The docker-compose stack accepts HS256 tokens signed with default-secret (matching Playwright helpers).
Generate a token:
bash
TOKEN="$(
USER_ID="debug-user" NICKNAME="Debug" JWT_SECRET="default-secret" node - <<'NODE'
const crypto = require("crypto");
function base64Url(input) {
return Buffer.from(input)
.toString("base64")
.replace(/=/g, "")
.replace(/\+/g, "-")
.replace(/\//g, "_");
}
function sign(data, secret) {
return base64Url(crypto.createHmac("sha256", secret).update(data).digest());
}
const userId = process.env.USER_ID ?? "debug-user";
const nickname = process.env.NICKNAME ?? "Debug";
const secret = process.env.JWT_SECRET ?? "default-secret";
const header = base64Url(JSON.stringify({ alg: "HS256", typ: "JWT" }));
const payload = base64Url(JSON.stringify({
sub: userId,
nickname,
iss: "poker-gateway",
aud: "poker-ui",
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 3600,
}));
console.log(`${header}.${payload}.${sign(`${header}.${payload}`, secret)}`);
NODE
)"
Symptom → Likely Cause Map
- •Stuck on
PREFLOP/ “Current Turn” is an empty seat:- •
hand.turndrifted to anEMPTY/inactive seat (or the timer never fired). - •Check Game Service turn repair + timer scheduling in
apps/game/src/services/tableService.ts.
- •
- •Same player appears in multiple seats:
- •Duplicate seating wasn’t blocked; action routing can hit the “wrong” seat.
- •Confirm Game Service rejects second seat join with
ALREADY_SEATED.
- •Player can’t see their own hole cards:
- •UI thinks they’re spectating (no
seatId) even though the server shows them seated. - •Confirm UI infers your seat from the JWT
subinapps/ui/src/state/tableStore.tsand then fetches hole cards.
- •UI thinks they’re spectating (no
Logs (When You Need Root Cause)
- •Game Service:
docker compose logs --since 10m game | rg 'turn\\.repair|NOT_YOUR_TURN|NO_HAND_IN_PROGRESS' - •Gateway boundary:
docker compose logs --since 10m gateway | rg '/api/tables/|SubmitAction|JoinSeat' - •For traceId correlation across services, use
.codex/skills/specify-poker-observability-triage/SKILL.md.
Regression Tests
- •Run just the “duplicate seating” rule:
- •
PLAYWRIGHT_EXTERNAL=1 npm --prefix apps/ui run test:e2e -- tests/e2e/gameplay-rules.spec.ts -g "rejects joining multiple seats"
- •