"""Phase 07 task 03 — the JS wire enums must stay equal to the Python ones. `app/static/js/shared/enums.js` is the client's vocabulary for the sim's StrEnums and the protocol's `ErrorCode`. The wire carries the *string values*; a drift — a value added to the Python side and forgotten in the JS set, or removed from one side only — would show up in the client as `undefined` sprites and dead FX. So the parity is a gate, not a convention: this test reads the JS file (the same source `app/static/index.html` serves — no copy that could drift), extracts the frozen sets, and asserts set equality in **both directions** (mirroring the doc-sync discipline of `tests/unit/test_protocol_doc_sync.py`). It checks the seven sets named by the task: `TILE_KIND`, `STATION_KIND`, `ITEM_KIND`, `ITEM_FORM`, `FACING`, `EVENT_KIND`, `ERROR_CODE`. Removing a value from either side of one of them makes this test fail, naming the missing value. """ from __future__ import annotations import json import re from enum import Enum from pathlib import Path import pytest from undercooked.server.protocol import ErrorCode from undercooked.sim.model import ( EventKind, Facing, ItemForm, ItemKind, StationKind, TileKind, ) pytestmark = pytest.mark.unit JS_ENUMS_PATH = ( Path(__file__).resolve().parent.parent.parent / "app" / "static" / "js" / "shared" / "enums.js" ) def _js_set(source: str, name: str) -> set[str]: """One frozen JS array, as a set of string literals (a tiny, stable parser: `export const NAME = Object.freeze([ "a", "b" ]);`).""" match = re.search( rf"export const {re.escape(name)} = Object\.freeze\(\[(.*?)\]\)", source, re.DOTALL, ) assert match is not None, f"{name} is not a `Object.freeze([...])` export" body = match.group(1) assert "'" not in body, ( f"{name} must use double-quoted literals (the test parses them as JSON)" ) # A JS array may end in a trailing comma; JSON may not. body = re.sub(r",\s*$", "", body.rstrip()) values = json.loads(f"[{body}]") assert all(isinstance(v, str) for v in values), f"{name} has a non-string entry" return set(values) def _js_ordered(source: str, name: str) -> list[str]: match = re.search( rf"export const {re.escape(name)} = Object\.freeze\(\[(.*?)\]\)", source, re.DOTALL, ) assert match is not None, f"{name} is not a `Object.freeze([...])` export" body = re.sub(r",\s*$", "", match.group(1).rstrip()) return json.loads(f"[{body}]") def _python_set(enum_cls) -> set[str]: """A StrEnum's values — or, for a plain constant class (the `ErrorCode`'s `Final` strings), the uppercase string attributes.""" if isinstance(enum_cls, type) and issubclass(enum_cls, Enum): return {member.value for member in enum_cls} return { value for name, value in vars(enum_cls).items() if name.isupper() and isinstance(value, str) } @pytest.fixture(scope="module") def js_source() -> str: assert JS_ENUMS_PATH.is_file(), f"the shared enum module is missing: {JS_ENUMS_PATH}" return JS_ENUMS_PATH.read_text(encoding="utf-8") @pytest.mark.parametrize( ("js_name", "py_name", "enum_cls"), [ ("TILE_KIND", "TileKind", TileKind), ("STATION_KIND", "StationKind", StationKind), ("ITEM_KIND", "ItemKind", ItemKind), ("ITEM_FORM", "ItemForm", ItemForm), ("FACING", "Facing", Facing), ("EVENT_KIND", "EventKind", EventKind), ("ERROR_CODE", "ErrorCode", ErrorCode), ], ) def test_enum_parity(js_source, js_name, py_name, enum_cls) -> None: """Set equality in both directions, naming the missing value.""" js = _js_set(js_source, js_name) py = _python_set(enum_cls) only_py = py - js only_js = js - py assert only_py == set() and only_js == set(), ( f"{js_name} (JS) drifted from {py_name} (Python): " f"missing in the JS set: {sorted(only_py) or '—'}, " f"missing in Python: {sorted(only_js) or '—'}" ) def test_facing_order_is_the_wire_index(js_source) -> None: """The wire's `f` is an *index* into the facing list (PLAN §4: 0-3): the JS array's order must be the enum's declaration order, not just its value set — `decode.js`'s `facingName` maps against that order.""" js_ordered = _js_ordered(js_source, "FACING") py_ordered = [member.value for member in Facing] assert js_ordered == py_ordered, ( f"FACING order drifted (the wire index maps against it): " f"JS {js_ordered} vs Python {py_ordered}" ) assert len(js_ordered) == 4