"""A8, enforced: the campaign is YAML on disk and never a list in Python. Phase 02 task 05's regression lock. The loader (`sim/content.py`), the content API (`server/routes_content.py`) and the author CLI (`tools/levels.py`) all discover levels by *reading a directory*, so a new `.yaml` file is the only thing an author writes to ship a level. This test proves that twice over: * **addition without code** — copy `levels/` to `tmp_path`, drop in `90_zerocool.yaml` with `order: 99`, and the loader, `/api/levels` and `undercooked-levels list`/`show`/`validate` all carry it, while the id appears in no Python file at all (and deleting the file removes it again); * **absence of literals** — no file under `src/undercooked/` (and no browser asset) contains a shipped level id or recipe id as a literal, so a hard-coded list cannot be quietly re-added later. The session fixture points `UNDERCOOKED_LEVELS_DIR` at the empty `tests/fixtures/levels/` directory, so every path here is set explicitly. """ from __future__ import annotations import shutil from collections.abc import Mapping from pathlib import Path import pytest from fastapi.testclient import TestClient from undercooked.config import load_settings from undercooked.server.app import create_app from undercooked.server.routes_content import RECIPES_FILENAME from undercooked.sim.content import load_content from undercooked.sim.recipe import load_recipe_book from undercooked.tools.levels import EXIT_OK, EXIT_UNUSABLE, main pytestmark = pytest.mark.unit REPO_ROOT = Path(__file__).resolve().parents[2] SHIPPED_LEVELS = REPO_ROOT / "levels" SHIPPED_RECIPES = SHIPPED_LEVELS / RECIPES_FILENAME SOURCE_ROOT = REPO_ROOT / "src" / "undercooked" STATIC_ROOT = REPO_ROOT / "app" / "static" def shipped_level_ids() -> set[str]: """Every id the shipped `levels/` directory carries (the book is not a level).""" return { path.stem for path in SHIPPED_LEVELS.glob("*.yaml") if path.name not in {RECIPES_FILENAME, "recipes.yml"} } #: The level this test adds. `order: 99` puts it last in the campaign whatever #: the shipped levels do, and three dishes with three orders live at once move #: the escalation axes the campaign rule (`DIFFICULTY_ORDER`) asks about. NEW_LEVEL_ID = "90_zerocool" NEW_LEVEL_YAML = """\ id: 90_zerocool name: Zero Cool order: 99 difficulty: 3 time_limit: 120 target_score: 110 blurb: >- A manifest probe, not a campaign level: three dishes, three orders live at once, and the same pinched floor as the second kitchen. grid: - "#############" - "#T....C....O#" - "#.....S.....#" - "####=...=####" - "#....P.X....#" - "#...........#" - "####=...=####" - "#R.K.....L..#" - "#############" tiles: "#": {kind: wall} ".": {kind: floor} "=": {kind: counter} "T": {kind: floor, station: spawn_shelf, item: tomato} "O": {kind: floor, station: spawn_shelf, item: onion} "L": {kind: floor, station: spawn_shelf, item: lettuce} "R": {kind: floor, station: spawn_shelf, item: rice} "C": {kind: floor, station: chop_board} "S": {kind: floor, station: stove} "P": {kind: floor, station: prep} "X": {kind: floor, station: serve} "K": {kind: floor, station: sink} spawns: [[2, 2], [10, 5]] orders: pool: [soup, salad, fried_rice] concurrent: 3 spawn_interval: [10, 14] deadline: 45 hazards: [] escalation_notes: >- Moves recipe_complexity, order_pressure and time_budget up versus the last shipped level, which is what the campaign rule asks for. """ def copy_shipped_levels(destination: Path) -> Path: """`levels/` copied verbatim — making a level here edits no Python.""" shutil.copytree(SHIPPED_LEVELS, destination) return destination def text_files(root: Path, suffixes: tuple[str, ...]) -> Mapping[str, str]: """Every file under `root` with one of `suffixes`, keyed by path.""" return { str(path): path.read_text(encoding="utf-8") for path in sorted(root.rglob("*")) if path.is_file() and path.suffix in suffixes and "__pycache__" not in path.parts } def offenders(literals: set[str], texts: Mapping[str, str]) -> dict[str, list[str]]: """Which literals appear in which file — empty means the scan passed.""" return { path: sorted(literal for literal in literals if literal in text) for path, text in texts.items() if any(literal in text for literal in literals) } @pytest.fixture() def extended_campaign(tmp_path: Path) -> Path: """A copy of the shipped campaign plus one level nothing ever registered.""" campaign = copy_shipped_levels(tmp_path / "campaign") (campaign / f"{NEW_LEVEL_ID}.yaml").write_text(NEW_LEVEL_YAML, encoding="utf-8") return campaign # --- a file is the only thing an author writes ---------------------------------- def test_a_new_yaml_file_reaches_the_loader_the_api_and_the_cli_without_code( extended_campaign: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], ) -> None: shipped_ids = [level.id for level in load_content(SHIPPED_LEVELS, SHIPPED_RECIPES).levels] # 1. the pure loader bundle = load_content(extended_campaign, extended_campaign / RECIPES_FILENAME) assert [level.id for level in bundle.levels] == [*shipped_ids, NEW_LEVEL_ID] assert list(bundle.by_id) == [*shipped_ids, NEW_LEVEL_ID] assert bundle.by_id[NEW_LEVEL_ID].order == 99 # 2. the HTTP content API, addressed only through the environment monkeypatch.setenv("UNDERCOOKED_LEVELS_DIR", str(extended_campaign)) monkeypatch.delenv("UNDERCOOKED_RECIPES_PATH", raising=False) with TestClient(create_app(load_settings())) as client: listing = client.get("/api/levels") assert listing.status_code == 200 assert [level["id"] for level in listing.json()] == [*shipped_ids, NEW_LEVEL_ID] detail = client.get(f"/api/levels/{NEW_LEVEL_ID}") assert detail.status_code == 200 assert detail.json()["order"] == 99 assert detail.json()["difficulty"] == 3 # 3. the author CLI, on the same environment and no `--dir` assert main(["list"]) == EXIT_OK assert capsys.readouterr().out.splitlines()[-1].split()[1] == NEW_LEVEL_ID assert main(["show", NEW_LEVEL_ID, "--grid"]) == EXIT_OK shown = capsys.readouterr().out assert shown.startswith(f"{NEW_LEVEL_ID} - Zero Cool") assert "concurrent=3" in shown # 4. and the gate accepts it: a row in a table that breaks PLAN §8 is not a level assert main(["validate"]) == EXIT_OK assert NEW_LEVEL_ID in capsys.readouterr().out # The proof of the whole point: the new level is everywhere except the code. assert offenders({NEW_LEVEL_ID}, text_files(SOURCE_ROOT, (".py", ".pyi"))) == {} def test_the_copied_campaign_on_its_own_is_still_the_shipped_two(tmp_path: Path) -> None: """The copy is faithful, so the test above measures exactly one added file.""" pristine = copy_shipped_levels(tmp_path / "pristine") copied = load_content(pristine, pristine / RECIPES_FILENAME) shipped = load_content(SHIPPED_LEVELS, SHIPPED_RECIPES) assert [level.id for level in copied.levels] == [level.id for level in shipped.levels] def test_a_deletion_is_as_invisible_as_an_addition( extended_campaign: Path, capsys: pytest.CaptureFixture[str] ) -> None: """Nothing caches the campaign in Python either: remove the file, it is gone.""" (extended_campaign / f"{NEW_LEVEL_ID}.yaml").unlink() assert main(["list", "--dir", str(extended_campaign)]) == EXIT_OK assert NEW_LEVEL_ID not in capsys.readouterr().out assert main(["show", NEW_LEVEL_ID, "--dir", str(extended_campaign)]) == EXIT_UNUSABLE assert NEW_LEVEL_ID not in load_content( extended_campaign, extended_campaign / RECIPES_FILENAME ).by_id # --- no literals in the source -------------------------------------------------- def test_no_python_source_names_a_shipped_level_id() -> None: """`levels/*.yaml` owns level ids: nothing in `src/undercooked/` repeats one.""" shipped = load_content(SHIPPED_LEVELS, SHIPPED_RECIPES) literals = shipped_level_ids() | {level.id for level in shipped.levels} assert literals # a scan of an empty set would pass and prove nothing assert offenders(literals, text_files(SOURCE_ROOT, (".py", ".pyi"))) == {} def test_no_python_source_names_a_recipe_from_the_book() -> None: """A8 covers recipes too: `recipes.yaml` is the only list of dishes.""" literals = set(load_recipe_book(SHIPPED_RECIPES).ids()) assert literals assert offenders(literals, text_files(SOURCE_ROOT, (".py", ".pyi"))) == {} def test_no_browser_asset_names_a_shipped_level_id() -> None: """The client asks the API for the campaign; it does not carry one.""" literals = shipped_level_ids() assets = text_files(STATIC_ROOT, (".js", ".css", ".html", ".svg")) assert assets assert offenders(literals, assets) == {} def test_the_loader_discovers_levels_by_walking_the_directory() -> None: """The discovery mechanism behind every test above: a directory walk.""" loader = (SOURCE_ROOT / "sim" / "content.py").read_text(encoding="utf-8") assert "rglob" in loader assert "iterdir" not in loader # one walk, at any depth, not a flat listing