Files
undercooked/tests/unit/test_protocol_doc_sync.py
ducoterra f26849c1f3 phase: 06_realtime_websocket_protocol
All green. Final verification complete — writing the report.

**Final pass report — phase 06 `realtime_websocket_protocol`**

- Verified: all 6 task files present in `complete/`; this was the final verification pass (no implementation work outstanding)
- `uv run ruff check .` → all checks passed; `uv run pyright` → 0 errors
- `uv run undercooked-levels validate` → content OK; `uv run pytest` (full suite) → **1751 passed, 0 failed, coverage 97.30% (>90% floor)**; slow sim stage (`-m slow tests/sim --no-cov`) → 2 passed; repo hygiene clean
- App launches: `/healthz` 200, `/` 200, `/api/levels`, `/api/config`, `POST /api/rooms` (201), `/api/stats` all verified live on an ephemeral port (server cleaned up)
- Criteria: scripted client room→hello→ready→start→countdown→≥20 snaps→round — PASS (`test_ws_round_two_clients.py`); identical `round` payloads both clients — PASS (`round_a == round_b`); version mismatch → `err protocol_mismatch` + close `4400` — PASS; 64 KB refused / 4 KB boundary, 500-burst → `rate_limited` → `4404` with bounded queue, 2000 malformed + 200 handshakes within memory bound — PASS; snap ≤ 1.5 KB (4p/4o/3h) and 64×64 ≤ 4 KB, downlink ≤ 60 KB/s over 100 ticks — PASS; `docs/PROTOCOL.md` model-sync test-enforced in both directions + full-round sequence diagram present — PASS
- Docs: README.md had 3 stale spots — status section (claimed 01–05 complete, 06 pending → now 01–06 complete, 07 pending), "until phases 06/07 wire the transport" wording, and the `PROTOCOL.md` layout note ("phase 06 completes it"). All fixed. AGENTS.md verified consistent — no changes needed
- No defects found in completed phases; no live infrastructure touched

Next pending phase: `07_canvas_client_and_hud`.
2026-10-09 11:13:25 -04:00

248 lines
8.1 KiB
Python

"""Unit: `docs/PROTOCOL.md` and `protocol.py` cannot drift (phase 06, task 01).
The phase's completion criteria demand that "every field in code appears in
the doc" — and, in the other direction, that the doc never names a field
that does not exist. This test is the guard: for **every** message model
in both directions (the nine client→server, the eight server→client, and
the six wire objects they nest) it extracts the field names from the doc's
``### ModelName`` table and compares them with the model's ``model_fields``
— set-equal, in both directions. It also keeps the doc's error-code and
close-code tables equal to the `ErrorCode` / `CloseCode` constants.
Doc convention (what this test relies on): every ``###`` heading in
``docs/PROTOCOL.md`` is exactly one model section (no other ``###``
headings — use ``##`` for prose sections), and each section holds one
markdown table whose first column is the field name. So: an added field
that is not documented fails here, a documented field that is not in the
model fails here, a renamed model fails here, and a new error code without
a doc row fails here.
"""
from __future__ import annotations
import re
from pathlib import Path
import pytest
from pydantic import BaseModel
from undercooked.server.protocol import (
CloseCode,
CountdownMsg,
EmoteMsg,
ErrMsg,
ErrorCode,
EventWire,
EvMsg,
HelloMsg,
InputMsg,
LobbyMsg,
NextLevelMsg,
PingMsg,
PlayerScoreWire,
PongMsg,
ReadyMsg,
RoundMsg,
SeatWire,
SetLevelMsg,
SnapMsg,
SpectatorWire,
StartMsg,
ToLobbyMsg,
Vec2Msg,
WelcomeMsg,
YouWire,
)
pytestmark = pytest.mark.unit
DOCS = Path(__file__).resolve().parents[2] / "docs" / "PROTOCOL.md"
#: Every message model in both directions, plus the wire objects they nest.
C_TO_S_MODELS: dict[str, type[BaseModel]] = {
"HelloMsg": HelloMsg,
"ReadyMsg": ReadyMsg,
"SetLevelMsg": SetLevelMsg,
"StartMsg": StartMsg,
"InputMsg": InputMsg,
"EmoteMsg": EmoteMsg,
"ToLobbyMsg": ToLobbyMsg,
"NextLevelMsg": NextLevelMsg,
"PingMsg": PingMsg,
}
S_TO_C_MODELS: dict[str, type[BaseModel]] = {
"WelcomeMsg": WelcomeMsg,
"LobbyMsg": LobbyMsg,
"CountdownMsg": CountdownMsg,
"SnapMsg": SnapMsg,
"RoundMsg": RoundMsg,
"EvMsg": EvMsg,
"ErrMsg": ErrMsg,
"PongMsg": PongMsg,
}
WIRE_MODELS: dict[str, type[BaseModel]] = {
"Vec2Msg": Vec2Msg,
"YouWire": YouWire,
"SeatWire": SeatWire,
"SpectatorWire": SpectatorWire,
"EventWire": EventWire,
"PlayerScoreWire": PlayerScoreWire,
}
ALL_MODELS: dict[str, type[BaseModel]] = {
**C_TO_S_MODELS,
**S_TO_C_MODELS,
**WIRE_MODELS,
}
SECTION_HEADING = re.compile(r"^###\s+(\S+)\s*$")
def _clean(cell: str) -> str:
return cell.strip().strip("`").strip()
def extract_model_sections(text: str) -> dict[str, list[str]]:
"""Field names (first table column) per ``###`` section.
A section runs from its ``###`` heading to the next heading of any
level; inside it, only table rows count, and the header row (first cell
``Field``) and the separator row (dashes/colons) are skipped.
"""
sections: dict[str, list[str]] = {}
current: str | None = None
for line in text.splitlines():
heading = SECTION_HEADING.match(line)
if heading is not None:
name = heading.group(1)
sections.setdefault(name, [])
current = name
continue
if line.lstrip().startswith("#"):
current = None # any other heading ends the section
continue
if current is None:
continue
stripped = line.strip()
if not stripped.startswith("|"):
continue
cells = [_clean(cell) for cell in stripped.strip("|").split("|")]
if not cells or cells[0] in {"Field", ""}:
continue
if set(cells[0]) <= {"-", ":"}:
continue
sections[current].append(cells[0])
return sections
def extract_section_first_cells(text: str, heading: str) -> list[str]:
"""First table column of one ``##`` section (the code tables)."""
lines = text.splitlines()
try:
start = next(i for i, line in enumerate(lines) if line.strip() == heading)
except StopIteration:
pytest.fail(f"{heading!r} section not found in docs/PROTOCOL.md")
cells: list[str] = []
for line in lines[start + 1 :]:
if line.lstrip().startswith("#"):
break
stripped = line.strip()
if not stripped.startswith("|"):
continue
first = _clean(stripped.strip("|").split("|")[0])
if first in {"Code", ""} or set(first) <= {"-", ":"}:
continue
cells.append(first)
return cells
def test_the_doc_exists() -> None:
assert DOCS.is_file(), "docs/PROTOCOL.md is the phase deliverable — it must exist"
def test_documented_sections_are_exactly_the_model_registry() -> None:
sections = extract_model_sections(DOCS.read_text(encoding="utf-8"))
missing = set(ALL_MODELS) - set(sections)
extra = set(sections) - set(ALL_MODELS)
assert not missing, f"models with no ### section in the doc: {sorted(missing)}"
assert not extra, (
f"### sections that are not model sections (doc convention): {sorted(extra)}"
)
@pytest.mark.parametrize("name", sorted(ALL_MODELS))
def test_the_doc_fields_match_the_model_fields_in_both_directions(name: str) -> None:
model = ALL_MODELS[name]
sections = extract_model_sections(DOCS.read_text(encoding="utf-8"))
assert name in sections, f"no ### {name} section in docs/PROTOCOL.md"
documented = set(sections[name])
implemented = set(model.model_fields)
undocumented = implemented - documented
ghost = documented - implemented
assert not undocumented, (
f"{name}: fields in the model but missing from the doc: {sorted(undocumented)}"
)
assert not ghost, (
f"{name}: fields documented but not in the model: {sorted(ghost)}"
)
def test_both_directions_are_covered_by_the_registry() -> None:
assert len(C_TO_S_MODELS) == 9
assert len(S_TO_C_MODELS) == 8
assert len(WIRE_MODELS) == 6
assert set(ALL_MODELS) == set(C_TO_S_MODELS) | set(S_TO_C_MODELS) | set(WIRE_MODELS)
def test_the_error_code_table_matches_the_constants() -> None:
cells = extract_section_first_cells(DOCS.read_text(encoding="utf-8"), "## Error codes")
assert len(cells) == len(set(cells)), "duplicate rows in the error-code table"
documented = set(cells)
implemented = {
ErrorCode.UNKNOWN_TYPE,
ErrorCode.BAD_JSON,
ErrorCode.BAD_FIELD,
ErrorCode.TOO_BIG,
ErrorCode.RATE_LIMITED,
ErrorCode.NOT_SEATED,
ErrorCode.NOT_HOST,
ErrorCode.LEVEL_LOCKED,
ErrorCode.WRONG_STATE,
ErrorCode.ROOM_FULL,
ErrorCode.ROOM_GONE,
ErrorCode.PROTOCOL_MISMATCH,
ErrorCode.EVICTED,
ErrorCode.INTERNAL,
}
assert documented == implemented, (
f"error-code table drift: missing {sorted(implemented - documented)}, "
f"ghosts {sorted(documented - implemented)}"
)
def test_the_close_code_table_matches_the_constants() -> None:
cells = extract_section_first_cells(DOCS.read_text(encoding="utf-8"), "## Close codes")
documented = {int(cell) for cell in cells}
implemented = {
CloseCode.NORMAL,
CloseCode.PROTOCOL_MISMATCH,
CloseCode.ROOM_GONE,
CloseCode.ROOM_FULL,
CloseCode.EVICTED,
CloseCode.ABUSE,
}
assert documented == implemented, (
f"close-code table drift: missing {sorted(implemented - documented)}, "
f"ghosts {sorted(documented - implemented)}"
)
def test_the_whole_contract_round_trips_through_the_models() -> None:
"""Every model here is a real Pydantic model with a `type` discriminator
(C→S) or a defaulted one (S→C) — a smoke over the registry itself."""
for name, model in ALL_MODELS.items():
assert isinstance(model, type)
assert issubclass(model, BaseModel)
fields = set(model.model_fields)
if name in C_TO_S_MODELS:
assert "type" in fields, f"{name} has no discriminator"