--- name: verify description: Drive the Lost Cities web client in a real browser to observe a change working — model loading, a full match, the result card, records being written. --- # Verifying the web client The client is where a silent bug hides best. `npm test` and `tsc` were both green while the app was announcing the wrong winner and dropping every game record on the floor. Neither throws. Run it. ## Build and serve ```bash cd web && npm run build cd .. && tmux new-session -d -s websrv \ "uv run python scripts/serve_web_with_logs.py --host 127.0.0.1 --port 5199 \ --dist web/dist --output /tmp/verify-records.jsonl" ``` Serve the **built dist**, not the vite dev server — the deploy builds from dist, and the model is a static asset whose path only resolves there. Point `--output` at a scratch file so verification runs never touch `data/human-play/`. ## Drive it Playwright is deliberately **not** a dependency: the `playwright` package downloads ~114MB of Chromium on install, and `npm ci` runs in the deploy workflow. Install it for the run and uninstall after. ```bash cd web npm i -D playwright --no-fund --no-audit && npx playwright install chromium # ... drive ... npm uninstall playwright ``` ## Selectors that actually work Found by dumping the DOM; guessing at them wasted two runs. | What | Selector | |---|---| | a hand card | `button.card:not([disabled])` | | play onto an expedition | `button.lane__zone--mine.is-target` | | discard | `button.lane__discard.is-target` | | draw from deck | `button.deck-stack:not([disabled])` | | which model loaded | `.score-plaque--rival small` | | match progress | `.round-strip` | | final scores | `.result-card` | A turn is three clicks: pick a card, choose where it goes, then draw. The place and draw targets only appear **after** the card is selected, and only the legal ones are enabled — so click the card first, then query. The rival answers on a 620ms timer plus inference; ~200ms of slack between plies is enough. A full three-round match runs ~140 plies, so budget a few minutes. ## Worth driving - **A whole match, not one round.** The round roll-over is where carry banks, and it is where the result card got it wrong. - **Check the record actually saved.** A schema bump on the client silently 400s against `scripts/serve_web_with_logs.py` until its allowlist is updated too. - **A stale save in localStorage.** Bump the key on a schema change; a half-migrated save is worse than a fresh deal. - **The same seed twice.** In match mode the seed fixes all three deals and the coin flips, so a match is a pure function of it.