Files
undercooked/tests/unit/test_protocol_whitelist.py
T
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

95 lines
3.8 KiB
Python

"""Unit: `MESSAGE_WHITELIST` — totality and the pinned matrix (phase 06, task 01).
The whitelist is the single source of "what is legal when" (PLAN §7
"Accepted when"), imported by the endpoint and never duplicated. These tests
pin the whole matrix (a silent edit changes the wire contract) and assert
its *totality*: every room state is classified, every classification is a
known client type, and every client type is legal in at least one state —
so a new `ClientMsgType` that is never whitelisted (or a state that is
never classified) fails loudly.
"""
from __future__ import annotations
import pytest
from undercooked.server.protocol import CLIENT_MSG_TYPES, MESSAGE_WHITELIST
from undercooked.server.room import RoomState
pytestmark = pytest.mark.unit
#: The pinned matrix — PLAN §7 "Accepted when", one row per room state.
EXPECTED_MATRIX: dict[RoomState, frozenset[str]] = {
RoomState.LOBBY: frozenset({"hello", "ready", "set_level", "start", "emote", "ping"}),
RoomState.COUNTDOWN: frozenset({"hello", "ping"}),
RoomState.PLAYING: frozenset({"hello", "input", "emote", "ping"}),
RoomState.ROUND_END: frozenset({"hello", "to_lobby", "next_level", "ping"}),
RoomState.CLOSED: frozenset({"hello", "ping"}),
}
def test_the_matrix_is_pinned() -> None:
actual = {state: frozenset(types) for state, types in MESSAGE_WHITELIST.items()}
assert actual == EXPECTED_MATRIX
def test_totality_every_room_state_is_classified() -> None:
assert set(MESSAGE_WHITELIST) == set(RoomState)
def test_totality_every_whitelisted_type_is_known() -> None:
for state, allowed in MESSAGE_WHITELIST.items():
unknown = allowed - CLIENT_MSG_TYPES
assert not unknown, f"{state.value}: unknown types {sorted(unknown)}"
def test_totality_every_client_type_is_legal_somewhere() -> None:
union: set[str] = set()
for allowed in MESSAGE_WHITELIST.values():
union |= set(allowed)
never = CLIENT_MSG_TYPES - union
assert not never, f"client types legal in no state at all: {sorted(never)}"
def test_hello_and_ping_are_legal_in_every_state() -> None:
for state, allowed in MESSAGE_WHITELIST.items():
assert {"hello", "ping"} <= allowed, (
f"{state.value}: hello (the rejoin handshake) and ping (the "
f"keep-alive) must be legal in every state"
)
def test_input_is_playing_only() -> None:
assert "input" in MESSAGE_WHITELIST[RoomState.PLAYING]
for state in (RoomState.LOBBY, RoomState.COUNTDOWN, RoomState.ROUND_END, RoomState.CLOSED):
assert "input" not in MESSAGE_WHITELIST[state]
def test_the_lobby_verbs_are_lobby_only() -> None:
for verb in ("ready", "set_level", "start"):
assert verb in MESSAGE_WHITELIST[RoomState.LOBBY]
for state in (RoomState.COUNTDOWN, RoomState.PLAYING, RoomState.ROUND_END, RoomState.CLOSED):
for verb in ("ready", "set_level", "start"):
assert verb not in MESSAGE_WHITELIST[state]
def test_emote_is_lobby_and_playing_only() -> None:
assert "emote" in MESSAGE_WHITELIST[RoomState.LOBBY]
assert "emote" in MESSAGE_WHITELIST[RoomState.PLAYING]
for state in (RoomState.COUNTDOWN, RoomState.ROUND_END, RoomState.CLOSED):
assert "emote" not in MESSAGE_WHITELIST[state]
def test_the_round_end_verbs_are_round_end_only() -> None:
for verb in ("to_lobby", "next_level"):
assert verb in MESSAGE_WHITELIST[RoomState.ROUND_END]
for state in (RoomState.LOBBY, RoomState.COUNTDOWN, RoomState.PLAYING, RoomState.CLOSED):
for verb in ("to_lobby", "next_level"):
assert verb not in MESSAGE_WHITELIST[state]
def test_countdown_accepts_no_gameplay_verb() -> None:
# The 3 s countdown is a wall: only the handshake and the keep-alive get
# through (a late `ready` or `input` is `wrong_state`).
assert MESSAGE_WHITELIST[RoomState.COUNTDOWN] == {"hello", "ping"}