AGENTS.md@apps/lemon_sim_ui · diff
git:20260927.5588f91 to git:20260927.6027bc7
70 added, 194 removed. Audit B to B.
- # LemonSimUi Agent Guide
-
- ## Quick Orientation
-
- `lemon_sim_ui` is a Phoenix LiveView dashboard for the `lemon_sim` simulation harness. It does not contain any simulation logic — all game rules, runners, and domain examples live in `lemon_sim`. This app is responsible for:
-
- - launching, checkpointing, retrying/terminalizing recovery, and capacity-limiting simulations via `SimManager`
- - driving the runner loop (calling `LemonSim.Kernel.Runner.step/3` in a supervised task)
- - keeping the always-on model arenas running via `Arena` (one per domain: werewolf, poker — randomized lineups, crash resume, restart reconciliation, and durably marked league recording through `LemonSim.Bench.League`)
- - hosting durable human/AI Werewolf rooms via `HostedGame` and one serialized `RoomServer` per room
- - rendering live state in the browser via the public `LobbyLive`, public `LeaderboardLive`, public `ArenaLive`/`ArenaLeaderboardLive` (per-domain arena + league standings), admin `SimDashboardLive`, and public read-only `SpectatorLive` watcher
- - exposing a token-protected admin API for remote sim start/stop
- - accepting human-player moves for interactive domains and server-authorized, match-epoch Werewolf commands
-
- The primary entry points for model simulations are `SimManager`, `Arena`, `SimDashboardLive`, `SpectatorLive`, `LeaderboardLive`, and the board component for the relevant domain. Hosted Werewolf changes start in `HostedGame`, `HostedGame.RoomServer`, `HostedGame.Replay`, `HostedGameSessionController`, and `hosted_werewolf_live.ex`.
-
- ## File Structure
-
- ```
- lib/
- lemon_sim_ui.ex Web context macros (router/live_view/html helpers)
- lemon_sim_ui/
- application.ex OTP application: supervisor tree
- endpoint.ex Bandit HTTP endpoint + LiveView socket
- router.ex Public, no-store hosted, private admin, and protected API routes
- artifact_reader.ex Suite/usage JSON readers and formatting helpers
- sim_manager.ex GenServer: owns all running sim tasks
- demo.ex Keyless demo start helper (`start_werewolf/1`, watch URL)
- arena.ex GenServer per domain: always-on league scheduler + recorder
- arena_domains.ex Presentation config for arena domains
- hosted_game.ex Room creation, recovery, capacity, retention, kill switch
- hosted_game/
- supervisor.ex One-for-all hosted subsystem
- room_server.ex Durable room lifecycle, auth, timers, commands, replay
- replay.ex Canonical event replay/hash verifier
- sim_helpers.ex Pure helpers: domain inference, labels, colors
- werewolf_playback.ex Buffered live-playback helper for readable Werewolf spectator pacing
- telemetry.ex Bounded hosted lifecycle/latency metrics
- gettext.ex Gettext backend
- components/
- core_components.ex Shared form/button/flash components
- layouts.ex App/root layout modules
- layouts/app.html.heex
- layouts/root.html.heex
- controllers/
- admin_session_controller.ex CSRF-protected admin login/logout + session renewal
- admin_sim_controller.ex Token-protected JSON API for start/stop
- error_html.ex 404/500 HTML error views
- error_json.ex JSON error views
- health_controller.ex Public liveness `/healthz` and dependency readiness `/readyz`
- hosted_game_session_controller.ex Hosted form/session exchange and replay download
- metrics_controller.ex Protected runtime metrics
- live/
- lobby_live.ex Public list of currently running sims
- leaderboard_live.ex Public benchmark suite leaderboard page
- sim_dashboard_live.ex Admin LiveView (launch + detail)
- spectator_live.ex Public read-only spectator view (Werewolf, Poker, VendingBench)
- hosted_werewolf_live.ex Hosted lobby/join/host/player/public-safe views
- plugs/
- require_access_token.ex Expiring browser-session and bearer-only API gate
- components/
- event_log.ex Renders recent_events list
- plan_history.ex Renders plan_history steps
- memory_viewer.ex Renders LemonSim.Memory.Tools files
- tic_tac_toe_board.ex Board for :tic_tac_toe
- werewolf_board.ex Player cards for :werewolf
- poker_board.ex Table/stack display for :poker
- vending_bench_board.ex Retro VendingBench machine view, generated product sprites, Arena standings, operations, supplier/fault signals, and scorecard display
- mix/tasks/
- lemon.sim.demo.ex `mix lemon.sim.demo`: boot UI + offline Werewolf game, print watch URL
- test/
- lemon_sim_ui/
- live/
- sim_dashboard_live_test.exs
- components/board_components_test.exs
- support/
- conn_case.ex
- test_helper.exs
- ```
-
- Browser sources and locked npm dependencies live under `assets/`. Run
- `mix sim_ui.assets.build` from the repository root after changing HEEx, CSS, or
- JS. Release/container builds use `MIX_ENV=prod mix sim_ui.assets.deploy` to also
- digest and precompress static files. Production pages must remain
- self-contained and must not add runtime CDN dependencies.
-
- ## Key Modules
-
- | File | Module | Purpose |
- |---|---|---|
- | `lib/lemon_sim_ui/sim_manager.ex` | `LemonSimUi.SimManager` | Central GenServer; durable start/stop/abandon/resume, lifecycle and final-usage checkpoints, bounded recovery queue with transient backoff/terminalization, retention, and auto-loop controls |
- | `lib/lemon_sim_ui/demo.ex` | `LemonSimUi.Demo` | `start_werewolf/1` starts a keyless offline Werewolf game (`offline_strategy: "heuristic"`) through `SimManager`; used by `mix lemon.sim.demo` |
- | `lib/lemon_sim_ui/hosted_game.ex` | `LemonSimUi.HostedGame` | Hosted room coordinator for codes, recovery readiness, active capacity, retention, and feature kill switch |
- | `lib/lemon_sim_ui/hosted_game/room_server.ex` | `LemonSimUi.HostedGame.RoomServer` | One durable serialized room: token auth, safe projections, timers, AI/human commands, pause/replacement, export, and rematch |
- | `lib/lemon_sim_ui/hosted_game/replay.ex` | `LemonSimUi.HostedGame.Replay` | Re-folds redacted canonical events from the room seed via `LemonSim.Bench.Artifacts.Replay.fold/4` and checks command/final hashes |
- | `lib/lemon_sim_ui/live/hosted_werewolf_live.ex` | `LemonSimUi.Hosted*Live` | Hosted room creation, joining, role-blind host, private player, and public-safe story surfaces |
- | `lib/lemon_sim_ui/live/lobby_live.ex` | `LemonSimUi.LobbyLive` | Public lobby for `/`; lists running sims, links to spectator pages, and can expose configured VendingBench launcher presets |
- | `lib/lemon_sim_ui/live/leaderboard_live.ex` | `LemonSimUi.LeaderboardLive` | Public leaderboard for `/leaderboards`; scans configured suite roots and renders rankings, failures, token totals, and null-safe costs |
- | `lib/lemon_sim_ui/controllers/vending_bench_launch_controller.ex` | `LemonSimUi.VendingBenchLaunchController` | Public non-JS route for the fixed VendingBench launcher |
- | `lib/lemon_sim_ui/live/sim_dashboard_live.ex` | `LemonSimUi.SimDashboardLive` | Dashboard LiveView for `/admin` and `/admin/sims/:sim_id`; handles sim launch and admin/detail flows |
- | `lib/lemon_sim_ui/live/spectator_live.ex` | `LemonSimUi.SpectatorLive` | Public shareable watcher for `/watch/:sim_id`; supports Werewolf, Poker, and VendingBench, subscribes to sim/lobby updates, refreshes CLI VendingBench runs from checkpoint artifacts, and shows live usage collector snapshots or artifact `usage.json` |
- | `lib/lemon_sim_ui/artifact_reader.ex` | `LemonSimUi.ArtifactReader` | Reads `suite.json` and `usage.json`; keeps token/cost formatting null-safe |
- | `lib/lemon_sim_ui/werewolf_playback.ex` | `LemonSimUi.WerewolfPlayback` | Buffers exact Werewolf state snapshots and enforces minimum dwell times so live dialogue/night beats stay readable |
- | `lib/lemon_sim_ui/controllers/admin_sim_controller.ex` | `LemonSimUi.AdminSimController` | Protected JSON API for remote sim start/stop |
- | `lib/lemon_sim_ui/controllers/admin_session_controller.ex` | `LemonSimUi.AdminSessionController` | CSRF-protected browser login/logout flow for the private control room |
- | `lib/lemon_sim_ui/controllers/health_controller.ex` | `LemonSimUi.HealthController` | Public liveness and dependency readiness checks |
- | `lib/lemon_sim_ui/plugs/require_access_token.ex` | `LemonSimUi.Plugs.RequireAccessToken` | Expiring signed-session gate for browsers and bearer-only gate for the admin API |
- | `lib/lemon_sim_ui/sim_helpers.ex` | `LemonSimUi.SimHelpers` | `infer_domain_type/1`, `sim_summary/1`, `domain_label/1`, `domain_badge_color/1` |
- | `lib/lemon_sim_ui/live/components/event_log.ex` | `LemonSimUi.Live.Components.EventLog` | Stateless component; renders `recent_events` with color-coded event kinds |
- | `lib/lemon_sim_ui/live/components/plan_history.ex` | `LemonSimUi.Live.Components.PlanHistory` | Stateless component; renders `plan_history` as collapsible steps |
- | `lib/lemon_sim_ui/live/components/run_log.ex` | `LemonSimUi.Live.Components.RunLog` | Stateless component; renders current status, recent events, and model-visible VendingBench tool/decision traces |
- | `lib/lemon_sim_ui/live/components/memory_viewer.ex` | `LemonSimUi.Live.Components.MemoryViewer` | Reads scoped memory files from `LemonSim.Memory.Tools.memory_root/1` |
- | `lib/lemon_sim_ui/live/components/vending_bench_board.ex` | `LemonSimUi.Live.Components.VendingBenchBoard` | Retro vending-machine broadcast view with generated product sprites, Arena standings, supplier, refund, fault, trade, and scorecard display |
-
- ## Common Modification Patterns
-
- ### Adding a New Simulation Domain
-
- 1. Implement the domain in `lemon_sim` (state, modules, updater, projector, action space, runner opts), declare its `terminal_statuses/0`, and list it in `LemonSim.Examples`.
- 2. Add a `build_initial_state/3` clause in `SimManager` for the new domain atom.
- 3. Add `generate_id/1` clause in `SimManager`.
- 4. Add domain detection in `SimHelpers.infer_domain_type/1` (key a unique world map field).
- 5. Add `sim_summary` world summary clause in `SimHelpers`.
- 6. Add `domain_label/1` and `domain_badge_color/1` clauses in `SimHelpers`.
- 7. Create a board component in `lib/lemon_sim_ui/live/components/<domain>_board.ex`.
- 8. Alias the board component and add a `<% :domain -> %>` clause in the `render/1` case in `SimDashboardLive`.
- 9. Add the domain to the form `options` list in `SimDashboardLive.render/1`.
- 10. Add `min_players/1`, `max_players/1`, `default_player_count/1`, and `player_count_label/1` clauses if the domain uses player counts.
-
- ### Adding Interactive Human Play to a Domain
-
- 1. Detect whose turn it is in `SimManager.human_turn?/2` (pattern on a world key unique to the domain).
- 2. Add phx-event handlers in `SimDashboardLive` (e.g., `handle_event("human_action", ...)`) that call `SimManager.submit_human_move/2` with a `LemonSim.Kernel.Event`.
- 3. Render interactive controls in the board component, gated on the `interactive` attribute (set by the LiveView when `human_player != nil && sim_id in running`).
-
- ### Updating an Existing Board Component
-
- Board components are pure stateless `Phoenix.Component` functions. They receive `:world` (the `LemonSim.Kernel.State.world` map) and optionally `:interactive`. They do not hold any state — all data is derived from `world` in the function body before the `~H"""` template.
-
- When adding display fields, read them with `LemonCore.MapHelpers.get_key/2` (or the local `get_val/3` helper already defined in most board components) to tolerate both atom and string keys in the world map.
- VendingBench board data may come from JSON checkpoint artifacts, so do not use `String.to_atom/1` for slot, product, supplier, or agent ids while rendering.
-
- ### Changing Runner Behavior (step interval, retry count)
-
- `SimManager` contains the runner loop in `do_ai_loop/7`. Constants to adjust:
+ # LemonSimUi contributor guide
- - `@max_step_retries` — number of retries on step error before giving up
- - `Process.sleep(500)` between successful steps
- - `Process.sleep(2000 * (retries + 1))` backoff on failure
- - The human move timeout is `300_000` ms (5 minutes) in `do_interactive_loop/7`
+ This Phoenix LiveView app launches/observes simulations, owns arena/hosted-room
+ lifecycle and renders boards. Game rules and world transitions stay in
+ `lemon_sim`. See [README](README.md).
- ### Adding Per-Seat Model Assignment to a Domain
+ ## Runtime and rendering
- Domains that support per-seat models use `build_multi_model_opts/5` in `SimManager`. To add this to a new domain:
+ - `SimManager` owns model-simulation runner PIDs, checkpoint/recovery, turn budgets
+ and capacity. Hosted AI work belongs to `HostedGame.RoomServer` under the
+ dedicated bounded AI task supervisor, not unbounded shared provider fanout.
+ - Use `LemonSim.Examples.terminal?/1` for completion, recovery, retention, league
+ recording and UI replay state. Different domains have different terminal values.
+ - Offline strategies reuse the domain's keyless decider; never resolve models or
+ credentials on that path. Resume reconstructs the same persisted start options.
+ - Boards are stateless function components. Read mixed-key checkpoint maps safely;
+ do not convert external slot/product/supplier/agent IDs into atoms while rendering.
+ - New domains need registered kernel contracts, initialization, summary/detection,
+ board/form wiring and appropriate human/per-seat controls. Match against the
+ registered model provider list rather than input-driven atom creation.
+ - Werewolf metadata uses stable villager actor IDs, not generic player seat IDs.
+ - Broadcast pacing is UI-side dwell over exact snapshots, never changes to game
+ rules. Arena attempt rotation includes failures for role balance and reliability;
+ failed games do not contribute game outcome ratings.
+ - Show evidence reliability/interpretation, not clue prose alone. Vending traces
+ expose visible tool/results and domain summaries, not provider-hidden thoughts.
- 1. Accept `player_count` and `model_specs` in `build_initial_state/3`.
- 2. Call `build_multi_model_opts/5` with a `:default_opts_fn` pointing to the domain's `default_opts/1`.
- 3. Add model picker rows to the launch form in `SimDashboardLive.render/1` (already templated for the `~w(werewolf vending_bench)` guard — extend that list).
+ ## Hosted Werewolf privacy and durability
- For Werewolf specifically, the internal actor IDs are villager names (`"Alice"`, `"Bram"`, etc.), not `player_n` seat IDs. Any per-player metadata such as `model_assignments` or `character_profiles` must be keyed by those canonical names.
+ - Omniscient `WerewolfBoard` is for model admin/broadcast, never hosted humans.
+ Player/public/host views come from role-safe engine projections. Hiding HTML
+ after sending raw world state is not a privacy boundary.
+ - Hosts are role-blind. Secret actors, roles, actions, votes, meetings, pack chat,
+ investigations, items, journals and thoughts never enter host/public projections
+ or telemetry. Private rooms stay private after stop/completion.
+ - Credentials stay in signed cookies and `socket.private`, never assigns,
+ persisted rooms, logs/crash formatting or exported replay. Preserve parameter
+ filtering and disabled lifecycle logging on sensitive routes.
+ - Commands carry match/state epochs and server-built legal actions. Persist before
+ acknowledgement; client idempotency IDs cannot collide with system timeout/AI IDs.
+ - Persisted deadlines are authoritative. Late human/AI decisions enter the timeout
+ path rather than winning a mailbox race. World RNG is recorded/threaded, never
+ process-global provider/decider randomness.
+ - Waiting/pre-start cancellation is sealed: no phase/day/actor/role leakage.
+ Distinguish runtime failure from host stop; finished players see final roles.
+ - Restart child specs contain room IDs only and reload latest stored rows, not
+ captured snapshots. Disabled hosted feature fails closed and recovers no timers
+ or AI. It is a runtime kill switch, not only a hidden creation button.
+ - SQLite/Registry/PubSub ownership is single-node. Do not advertise horizontal
+ replicas without distributed ownership, storage and PubSub.
- ### Adding a New Model to the Launch Form
+ ## Access and operations
- Edit `provider_options/0`, `model_options_for_provider/1`, and the default provider/model constants in `SimDashboardLive`. The spec format is `"provider:model_id"` — parsed by `SimManager.parse_model_spec/1` against the registered `LemonAi.Models` provider list, so canonical names like `openai-codex` and supported aliases like `openai_codex` both resolve.
+ - Admin pages require `RequireAccessToken`; production requires a 32-byte token.
+ Browser login is the CSRF-protected form with an expiring signed marker, never
+ query credentials. Admin API is bearer-only.
+ - Lobby, leaderboards, watchers and health endpoints are deliberately public.
+ Optional public Vending launch uses validated configured presets, not arbitrary
+ model/job input. Hosted creation also requires its production creation token
+ and HTTPS.
+ - Keep runner concurrency, stored-sim retention, league record limits, hosted
+ TTL/AI limits and deployment configuration coherent. See deployment instructions
+ in [README](README.md) rather than duplicating environment catalogs here.
+ - Suite discovery accepts configured suite roots, logs/skips malformed JSON and
+ remains bounded. MemoryViewer render-time reads are bounded by file count/size;
+ do not turn it into unrestricted filesystem browsing.
+ - Build assets with `mix sim_ui.assets.build` or
+ `MIX_ENV=prod mix sim_ui.assets.deploy` for release assets.
- ## Design Boundaries
+ ## Verification
- - Do not add simulation logic here. Game rules, event shapes, and world state mutations belong in `lemon_sim`.
- - A run is finished only when `LemonSim.Examples.terminal?/1` says so. SimManager lifecycle (`completed` marking, recovery eligibility, resume refusal as `{:error, :finished}`, retention pruning), Arena recording, auto-loop restart, spectator redirects, and lobby REPLAY badges all use it; never compare world status with `"game_over"` outside a single-scenario board.
- - Offline strategies are keyless SimManager runs: `offline_strategy` selects a decider from `@offline_deciders` (today only Werewolf `"heuristic"`, i.e. `LemonSim.Examples.Werewolf.OfflineRunner`; never add a second copy of a lemon_sim heuristic here), `run_offline/3` owns persistence, traces, turn budget, and lifecycle, and resume rebuilds the same decider from persisted `start_opts`. Never resolve models or provider keys on that path.
- - Board components must remain stateless function components. Do not convert them to LiveComponents unless there is a strong rendering-isolation reason.
- - `SimManager` owns model-simulation runner PIDs. Hosted AI work is owned by `HostedGame.RoomServer` under the dedicated bounded `HostedGame.AiTaskSupervisor`; do not use the shared task supervisor for provider fanout.
- - The omniscient `WerewolfBoard` is only for model admin/broadcast surfaces. Never reuse it for hosted humans. Hosted player/public/host data must come from role-safe projections; hiding markup after sending raw state is not sufficient.
- - A hosted host is role-blind. Secret-phase actor IDs, roles, actions, votes, meetings, pack chat, investigations, items, journals, and model thoughts must not enter host/public projections or telemetry. Private rooms remain private after stop/completion.
- - Hosted credentials stay in signed cookies and LiveView `socket.private`, never assigns, lifecycle logs, crash formatting, storage, telemetry, or exported replays. Keep Phoenix parameter filtering and shared LiveView lifecycle logging disabled when adding routes.
- - Hosted commands must carry match and state epochs, accept only server-built legal actions, persist before acknowledgement, and namespace client idempotency IDs away from system timeout/AI IDs.
- - Persisted turn deadlines are authoritative: late human or AI decisions must enqueue the timeout path, never win a mailbox race. Authoritative game randomness lives in `world.rng_state`, seeded from the room config seed; updaters never read the process `:rand` state, so provider/decider randomness cannot change exported replay hashes.
- - Lobby and pre-start cancellation projections expose a sealed waiting state with no phase, day, actor, or role. Runtime terminal failures use a safe reason distinct from host stop/cancel and completed player views reveal the final role roster.
- - Hosted child specs pass only room IDs and reload the latest Store row. Never capture a room snapshot in a restart MFA.
- - Hosted feature disablement is a runtime kill switch: disabled boot must not recover room timers/AI, and API/LiveView operations must fail closed.
- - Local SQLite/Registry/PubSub make hosted rooms single-node. Do not advertise horizontal replicas without distributed ownership, shared storage, and distributed PubSub.
- - Buffered Werewolf watch pacing belongs in `lemon_sim_ui`, not `lemon_sim`. Use exact broadcast snapshots plus UI-side dwell heuristics for readability, but keep simulation rules and state transitions in `lemon_sim`.
- - Always-on Werewolf rotates configured model specs one seat per attempted game and starts with fixed roles by sorted seat. Failed attempts must remain visible as reliability data without contributing game outcomes to ratings. Preserve this full-cycle role balance when changing arena planning, resumption, or league pruning.
- - Werewolf evidence rendering must show the engine-provided reliability and interpretation, not only the clue prose, so spectators can distinguish a noisy lead from proof.
- - VendingBench live-log model traces are compact `plan_history` entries from `SimManager`. Keep them to visible tool calls/results and domain summaries; do not try to expose provider-hidden chain-of-thought.
- - `SimHelpers.infer_domain_type/1` uses world map key heuristics. If two domains share the same distinguishing key, ensure the more specific one is listed first in the `cond`.
- - Keep `/admin` and `/admin/sims/:sim_id` on `SimDashboardLive` behind `RequireAccessToken`. Dedicated production releases require at least a 32-byte access token. Browsers authenticate through the CSRF-protected `/admin/login` form and receive an expiring signed-session marker; query-string credentials must remain rejected. `/api/admin/*` accepts bearer credentials only. `/`, `/leaderboards`, `/watch/:sim_id`, `/healthz`, and `/readyz` are intentionally public. The optional public VendingBench launcher is controlled by `LEMON_SIM_UI_PUBLIC_VENDING_LAUNCHER` and should stay limited to validated configured presets unless the route is moved behind auth.
- - Keep `LEMON_SIM_UI_MAX_CONCURRENT_RUNNERS`, `LEMON_SIM_UI_MAX_STORED_SIMS`, and per-arena `MAX_GAME_RECORDS` wired through runtime config, readiness, deployment examples, and tests when lifecycle/storage behavior changes.
- - Keep every `LEMON_WEREWOLF_HOSTED_*` knob, HTTPS requirement, room TTL/retention, AI limit/model, readiness, `.env.example`, and deployment manifest coherent. Production room creation also requires a 32-byte `LEMON_WEREWOLF_HOST_CREATE_TOKEN`.
- - Configure public benchmark discovery with `config :lemon_sim_ui, :suite_roots, ["/tmp/vending-suite"]`. The default reader accepts either a suite directory containing `suite.json` or a parent directory with child suite directories, logs malformed JSON, and skips bad files.
- - Treat auto-loop and deployment wiring as an ops slice. Runtime env flags such as `LEMON_SIM_AUTO_LOOP` and deployment manifests such as `fly.toml` should stay coherent with `SimManager` auto-loop behavior, but separate from the general public/admin UI route changes.
- - `MemoryViewer` reads files synchronously at render time (no caching). Keep it bounded to small memory namespaces; it already limits to 20 files and 4096 bytes per file.
+ From the umbrella root: `mix test apps/lemon_sim_ui/test`.
+ ConnCase starts the endpoint with the real kernel store; clean created states.
+ Connected LiveView tests are necessary for PubSub behavior. Board tests use minimal
+ worlds; dashboard navigation uses `render_patch/2` when testing an existing mount.
- ## Testing
+ Hosted browser QA:
```bash
- mix test apps/lemon_sim_ui
+ HOSTED_WEREWOLF_SMOKE_URL=http://127.0.0.1:4090 npm --prefix apps/lemon_sim_ui/assets run smoke:hosted-werewolf
```
- Tests use `ConnCase` which starts the full endpoint. `LemonSim.Kernel.Store` is live (not mocked) — tests that create state must clean up with `Store.delete_state/1`.
-
- Run hosted browser QA with `HOSTED_WEREWOLF_SMOKE_URL=http://127.0.0.1:4090 npm --prefix apps/lemon_sim_ui/assets run smoke:hosted-werewolf`. It uses five isolated sessions and covers secret non-leakage, timeout, pause/resume, reconnect, completion, replay, rematch, reduced motion, keyboard focus, and phone/tablet/desktop layouts.
-
- When writing new board component tests, use `render_component/2` from `Phoenix.LiveViewTest` with a `%{world: ...}` assign. Pass a minimal world map that exercises the branch under test rather than a full sim state.
-
- When writing new `SimDashboardLive` tests, use `render_patch/2` to navigate between routes without remounting.
-
- When writing `SpectatorLive` tests, assert against `render(view)` after pubsub-driven updates so the test exercises the connected LiveView path rather than only the initial disconnected HTML.
+ Check secret non-leakage, timeout/reconnect, replay/rematch, keyboard/reduced-motion
+ behavior and phone/tablet/desktop layouts with isolated player sessions.