phase: 01_runnable_service_scaffold
**Phase 01 final verification pass — all criteria green**
- Verified (nothing left to implement; all 4 task files already in `complete/`): toolchain lock, typed `Settings`+`.env.example`, debugpy bootstrap, app factory, CLI (`--host/--port/--reload/--levels-dir`, `python -m undercooked`), static shell + `/static`, security headers, no-CDN/doc guards, `validate.sh`.
- `uv sync --extra dev` → exit 0; `uv.lock` present (untracked → harness commits it with the phase).
- `uv run undercooked --host 127.0.0.1 --port 8000` → `curl /healthz` = 200 `{"status":"ok","version":"0.1.0","rooms":0}` (superset of the criterion's body — now documented in README); `curl /` = 200 shell HTML; static css/js 200; CSP/nosniff/no-referrer/DENY on 200, 404 and 405 too.
- `DEBUGPY=1`: port 5678 is held on this machine by the user's own podman dev-container debugpy adapter — **no live change made/killed**; verified via `DEBUGPY=1 DEBUGPY_PORT=5679` → listener opens, `/healthz` still 200; the occupied-port case degraded gracefully as designed (warning, server serves). Bad config → one line + exit 2 ✓.
- `bash .agents/validate.sh` (root **and** `.agents/`) → exit 0: `ruff check .` 0 errors, `pyright` 0 errors, levels-CLI notice-skip (matches README prose), **284 passed** full suite incl. 10 Playwright e2e, coverage **100.00%** (floor 90), repo hygiene clean. `uv run pytest -m e2e` → 10 passed.
- Docs fixed: README Status → phase 01 complete + next pending 02 (was pointing at `todo/01…`), documented real `/healthz` body, precedence now "CLI flags > env > .env > default", added the PLAN §13-promised **How to play** section (honest about shell-only today), Layout rows marked what exists vs phases 02/07. AGENTS.md: recorded the `data:`-URL exemption in the no-CDN rule (test allows it) and matched the session-token rule to the browser's WebCrypto/base64url implementation. Also corrected a stale "session-wide chromium" docstring in `tests/e2e/conftest.py` (fixtures are function-scoped).
- Deviations: no container build run (needs an image pull; not a phase criterion — `Containerfile`/`compose.yaml` reviewed and consistent with `docs/DEPLOYMENT.md`); `HEAD /` is 405 (routes are GET-only by design; health checks use GET). No git index/history commands run.
- Next pending phase: `02_level_content_and_validation`.
This commit is contained in:
@@ -15,7 +15,10 @@
|
||||
(`00_phase.md` + `NN_task.md`). Finished work: `.agents/phases/complete/`.
|
||||
- Execution is owned by the `phased-execution` skill (`run-task.sh`, `run-phase.sh`,
|
||||
`auto-phase.sh`). Each task runs in a fresh subprocess and must pass
|
||||
`.agents/validate.sh` before it moves to `complete/`.
|
||||
`.agents/validate.sh` before it moves to `complete/`. That gate fails fast in this
|
||||
order: `ruff check .` → `pyright` → `undercooked-levels validate` (skipped with a
|
||||
notice until phase 02 creates the loader) → the **full** `pytest` suite with the
|
||||
coverage floor → repo-hygiene checks. It runs from the repo root or from `.agents/`.
|
||||
- New work is authored by the `phase-authoring` skill as a phase directory — not
|
||||
implemented straight from chat.
|
||||
- **Never** modify `.agents/PLAN.md` or anything under `.agents/phases/complete/`.
|
||||
@@ -60,16 +63,52 @@
|
||||
|
||||
## 5. Hard rules
|
||||
|
||||
- **Configuration is read once, in `config.py`.** `src/undercooked/config.py` is
|
||||
the only module that touches `os.environ`. To add a knob: add a `Settings`
|
||||
field + `ENV_KEYS` entry + a line in `.env.example` + a row in the README table
|
||||
(a test fails when those disagree), and read it from `get_settings()`. Values
|
||||
are validated at startup: an impossible one raises `ConfigError` rather than
|
||||
half-configuring the server (`undercooked` prints one line and exits `2`).
|
||||
Precedence is **CLI flag > environment > built-in default**: `__main__.py`
|
||||
resolves it purely, then hands the flags to `config.apply_cli_overrides()`,
|
||||
which is the only writer of `os.environ` (so a uvicorn `--reload` child
|
||||
re-reads the same values). `DEBUGPY=1` binds a debugger listener on `0.0.0.0`
|
||||
— dev-only, never on a public host.
|
||||
- **No external CDNs or remote assets.** `tests/unit/test_no_external_assets.py`
|
||||
fails the build on `http://`/`https://`/`//cdn` references in `app/static/` and
|
||||
templates. CSP is `default-src 'self'` (plus `ws:`/`wss:` same-origin).
|
||||
fails the build on `http://`/`https://`/`//host` references, `@import url(`,
|
||||
`srcset`, an absolute-URL `fetch()`/`import`, an absolute-URL `WebSocket`, or
|
||||
`EventSource` anywhere in `app/static/**` (html/css/js/svg), and requires every
|
||||
`<script src>`/`<link href>`/`<img src>` in `index.html` to be a same-origin
|
||||
`/static/...` path. It scans the same `STATIC_DIR` `server/app.py` mounts, so it
|
||||
cannot pass by scanning the wrong folder; the exemptions are XML `xmlns` URIs, which
|
||||
are identifiers the browser never fetches, and a `data:` URL in an `index.html`
|
||||
element attribute where the CSP already allows it (`img-src 'self' data:`) — the
|
||||
app's own bytes either way. The US-01 e2e suite additionally
|
||||
fails if loading the page ever issues an off-origin request. CSP is
|
||||
`default-src 'self'` (plus `ws:`/`wss:` same-origin) and is applied by
|
||||
`server/headers.py` to **every** response, including errors. FastAPI's `/docs`,
|
||||
`/redoc` and `/openapi.json` stay disabled because their bundled HTML pulls Swagger
|
||||
assets from `cdn.jsdelivr.net`; the wire contract lives in `docs/PROTOCOL.md`
|
||||
(written by phase 06) instead.
|
||||
- **Docs are guarded like code.** `tests/unit/test_docs_commands.py` requires every
|
||||
command line in a `bash` fence of `README.md`/`docs/*.md` to start with a known-safe
|
||||
prefix (`uv `, `podman `, `curl `, `cd `, `bash .agents/validate.sh`, `#`, or a
|
||||
documented `VAR=value` lead) and to launch only a program declared in
|
||||
`pyproject.toml`, and keeps the `UNDERCOOKED_*`/`DEBUGPY*` names in the docs and the
|
||||
ones in `config.py`/`.env.example` equal in both directions. So: never document a
|
||||
command that does not run yet — describe it in prose until the phase that ships it.
|
||||
`tests/unit/test_docs_index.py` keeps PLAN §13's documentation index and the `docs/`
|
||||
tree identical (no missing doc for a shipped phase, no doc nothing promises), and
|
||||
`docs/TESTING.md` is the reference for test layout, markers and artifact policy.
|
||||
- **No new technology** (dependency, framework, service) without explicit written
|
||||
user permission recorded in the phase's `00_phase.md` and signed off into PLAN.md.
|
||||
- **The simulation is the single source of truth.** Client-side values are guesses.
|
||||
Never trust inbound coordinates, scores, timers, or level ids — validate every
|
||||
inbound field against the room's state (`server/protocol.py`).
|
||||
- **Anonymous by design.** No accounts, no emails, no cookies with personal data.
|
||||
Identity = a random per-browser token (`secrets.token_urlsafe`) held in
|
||||
Identity = 24 random bytes per browser tab (`secrets.token_urlsafe(24)` when the
|
||||
server mints one, `crypto.getRandomValues` + base64url in `app/static/js/app.js`,
|
||||
which is the same entropy and the only option in a browser) held in
|
||||
`sessionStorage`. Names are sanitized, length-capped, non-authoritative.
|
||||
- **Frame and rate limits are enforced, not suggested**: `max_message_bytes`,
|
||||
`max_inputs_per_s`, room caps, and message-type whitelists per room state.
|
||||
@@ -92,14 +131,28 @@
|
||||
|
||||
| Change | Location |
|
||||
|---|---|
|
||||
| Configuration (env vars → typed `Settings`), debugpy bootstrap | `src/undercooked/config.py`, `.env.example`, `src/undercooked/debug.py` |
|
||||
| Pure game rules (movement, stations, orders, scoring, hazards) | `src/undercooked/sim/` |
|
||||
| Level/recipe content | `levels/*.yaml` |
|
||||
| Rooms, lobby lifecycle, session records | `src/undercooked/server/rooms.py`, `room.py` |
|
||||
| Wire messages + validation | `src/undercooked/server/protocol.py`, `docs/PROTOCOL.md` |
|
||||
| HTTP app, static serving, headers | `src/undercooked/server/app.py` |
|
||||
| HTTP app (`/`, `/healthz`, `/static`), lifespan | `src/undercooked/server/app.py` |
|
||||
| Security headers (CSP `default-src 'self'`, no-sniff, frame-ancestors) | `src/undercooked/server/headers.py` |
|
||||
| CLI entry point (`undercooked`): flags → `--help` → serve | `src/undercooked/__main__.py` |
|
||||
| Renderer, HUD, input, interpolation | `app/static/js/`, `app/static/assets/` |
|
||||
| Documentation (design / testing / deployment; per-phase docs) | `docs/*.md`, indexed by PLAN §13 and guarded by `tests/unit/test_docs_index.py` |
|
||||
| The pipeline gate itself | `.agents/validate.sh` |
|
||||
| User-visible flow contract | `.agents/user_stories/US-XX_*.md` |
|
||||
|
||||
Tests mirror that: `tests/unit/`, `tests/server/` (HTTP routes and stores),
|
||||
`tests/integration/` (live uvicorn + WebSocket clients), `tests/sim/` (headless
|
||||
determinism and replays), `tests/e2e/` (Playwright, named `test_usNN_*.py`, marked `e2e`).
|
||||
Tests mirror that: `tests/unit/` (pure, no-I/O logic incl. HTTP-route unit tests),
|
||||
`tests/integration/` (in-process app boot, live uvicorn + WebSocket clients), `tests/sim/`
|
||||
(headless determinism and replays), `tests/e2e/` (Playwright, named `test_usNN_*.py`,
|
||||
marked `e2e`). `tests/` is not a package; shared fixtures live in `tests/conftest.py`.
|
||||
|
||||
- The browser suites use the **`playwright` sync API directly with fixtures written in
|
||||
`tests/e2e/conftest.py`** — `pytest-playwright` is deliberately not a dependency, so
|
||||
adding it needs the usual permission. Those `browser`/`context`/`page` fixtures are
|
||||
**function-scoped on purpose**: a long-lived sync driver keeps an asyncio loop flagged
|
||||
as running in the main thread and breaks every later `async def` test.
|
||||
- `live_server` (session-scoped, `tests/conftest.py`) is the only fixture that binds a
|
||||
socket: an ephemeral `127.0.0.1` port, never a routable interface.
|
||||
|
||||
Reference in New Issue
Block a user