phase: 07_canvas_client_and_hud

**Final report — phase 07 final verification pass**

- **Verified:** all 5 tasks present in `complete/`; app boots (`/healthz` 200, `/` 200, static + atlas + `/api/levels` served); `docs/CLIENT.md` module map matches the on-disk `app/static/` tree; no code defects found.
- **Tests/lint/coverage:** `bash .agents/validate.sh` → ruff clean, pyright 0 errors, `undercooked-levels validate` pass, **full pytest 1838 passed, 97.21% coverage (floor 90%)**, slow sim stage 2 passed, repo hygiene clean. `uv run pytest tests/e2e -q` → **53 passed** (all 8 suites added this phase).
- **Criteria:** (1) two tabs create→join→ready→start→serve one dish, keyboard only — green via `test_us01/02/03/04` ✔; (2) e2e green ✔; (3) canvas non-blank, pixels change after a move — `test_us04_render_smoke` ✔; (4) HUD score within 200 ms of the `served` event — asserted, green ✔; (5) zero console errors + zero off-origin requests — `test_atlas_loads` / `test_us01_landing_surface` ✔; (6) `docs/CLIENT.md` matches layout ✔; (7) 100% suite + 97.21% ✔; (8) docs pass below ✔; (9) no regression in completed phases (all prior suites green) ✔.
- **README.md fixed:** status still said "next pending phase is 07" → now 07 complete, 08 pending; the phase-07 client text buried in the phase-05 bullet → its own "canvas client, art, HUD (phase 07)" bullet; stale "What does not exist yet" (claimed movement/controls missing) → rewritten to phases 08–14; "once the room flow ships" → "live in the browser today"; Layout `app/static/` "renderer arrives with the rest of phase 07" → full shipped-client map; Layout `docs/` "CLIENT lands with phase 07" → `CLIENT.md` listed; added `gen_art_index.py` Layout entry.
- **AGENTS.md fixed:** "the eight screens" (7 listed) → "the seven screens" (+ `dom.js` is the shared builder, not a screen); e2e naming line → notes the non-story guard `test_atlas_loads.py`.
- **Decisions:** no code changes were needed; only the two doc fixes above. Docs re-verified by `test_docs_commands`/`test_docs_index` (44 passed) plus the final full gate.
- **Next pending phase:** `08_client_prediction_and_touch`.
This commit is contained in:
2026-10-10 18:03:52 -04:00
parent f26849c1f3
commit 95ca3fd1fa
78 changed files with 13650 additions and 259 deletions
+32 -13
View File
@@ -111,25 +111,30 @@ def test_root_serves_the_shell_html(client: TestClient) -> None:
assert response.headers["content-type"].startswith("text/html")
assert int(response.headers["content-length"]) > 500 # streamed from the file
html = response.text
assert 'src="/static/js/app.js"' in html
assert 'src="/static/js/main.js"' in html # the composition root (task 03)
assert 'href="/static/css/base.css"' in html
assert 'id="screen-lobby"' in html
assert 'aria-live="polite"' in html
assert 'href="/static/css/screens.css"' in html
assert 'id="screen-root"' in html
assert (
'name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover"' in html
)
def test_shell_offers_the_lobby_controls(client: TestClient) -> None:
"""The controls are mounted by the landing screen module (phase 07
task 02); the shell offers the screen root they mount into."""
html = client.get("/").text
assert 'id="create-lobby"' in html
assert 'id="join-form"' in html
assert 'id="join-code"' in html
assert 'inputmode="text"' in html
assert 'autocapitalize="characters"' in html
assert 'size="5"' in html # five characters wide: no 20-char intrinsic box
assert 'maxlength="5"' in html
assert 'for="join-code"' in html # the code input has a real label
assert 'id="screen-root"' in html
landing = client.get("/static/js/screens/landing.js")
assert landing.status_code == 200
text = landing.text
assert 'id: "create-lobby"' in text
assert 'id: "join-form"' in text
assert 'id: "join-code"' in text
assert 'inputmode: "text"' in text
assert 'autocapitalize: "characters"' in text
assert "maxlength: String(CODE_LENGTH)" in text # five characters: no 20-char intrinsic box
assert 'for: "join-code"' in text # the code input has a real label
def test_static_css_and_js_are_served_with_real_mime_types(client: TestClient) -> None:
@@ -138,10 +143,24 @@ def test_static_css_and_js_are_served_with_real_mime_types(client: TestClient) -
assert css.headers["content-type"].startswith("text/css")
assert "--uc-hit: 44px" in css.text # the 44px hit-target contract lives in CSS
script = client.get("/static/js/app.js")
screens_css = client.get("/static/css/screens.css")
assert screens_css.status_code == 200
assert screens_css.headers["content-type"].startswith("text/css")
script = client.get("/static/js/main.js")
assert script.status_code == 200
assert script.headers["content-type"].startswith("text/javascript")
assert "uc.token" in script.text
assert "createApp" in script.text # the composition root imports the machine
app = client.get("/static/js/app.js")
assert app.status_code == 200
assert app.headers["content-type"].startswith("text/javascript")
assert "createApp" in app.text
identity = client.get("/static/js/identity.js")
assert identity.status_code == 200
assert identity.headers["content-type"].startswith("text/javascript")
assert "uc.token" in identity.text # the per-tab token lives in identity.js (A16)
def test_unknown_path_is_404(client: TestClient) -> None: