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`.
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
"""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"}
|
||||
Reference in New Issue
Block a user