Phase `02_level_content_and_validation` — final verification **green**. - **Gate:** `bash .agents/validate.sh` → ruff ✅ · pyright 0 errors ✅ · `undercooked-levels validate` OK (2 levels) ✅ · **668 passed** (incl. 10 Playwright e2e) ✅ · coverage **100%** ✅ · repo hygiene ✅ - **Criteria:** `validate` exit 0, one line per level; `list` = `01_first_kitchen`, `02_double_shift` in campaign order; live `:8000` `/healthz` 200, `/api/levels` (+detail) carries both levels with numeric `target_score`/`time_limit` and no grid/station leakage, `/api/config` correct, unknown id → 404, startup log `content loaded: 2 level(s), 4 recipe(s)`; `validate --dir tests/fixtures/levels/_bad/` → exit 1 with named `RECIPE_STATION_MISSING` / `SERVE_REACHABLE` / `TARGET_IMPLAUSIBLE`; A8 lock test proves a YAML-only level reaches loader+API+CLI. - **Defect fixed (pre-existing path, phase-02-triggered):** `undercooked` with unusable content printed the report then a raw traceback and exited `1`. `src/undercooked/__main__.py` now catches `ContentError` → one closing line, exit `2` (like a bad setting); 2 new tests in `tests/unit/test_main.py` (incl. non-content failures still propagate). - **Docs pass (full, project-wide):** README (content gate + exit `2`, config section), AGENTS.md §3, `docs/LEVELS.md` (startup refusal + exact CLI exit codes), `docs/DEPLOYMENT.md` (crash-loop diagnosis bullet); DESIGN/TESTING re-read, consistent. - **Container re-verified (no phase-01 regression):** `podman build -f Containerfile` succeeds; the image ships `levels/` and serves `/healthz` 200, `/api/levels` (2 levels), `/` 200 as non-root. - **Deviation to note:** the phase overview names `tests/fixtures/levels/bad/`; the actual broken-fixture dir is `tests/fixtures/levels/_bad/` (the `_` prefix is what keeps it out of normal discovery). Overview files are read-only to me — README/LEVELS.md document the real path. - No git index/history commands run; all changes left unstaged for the harness. ntfy notification sent.
82 lines
2.5 KiB
Python
82 lines
2.5 KiB
Python
"""Unit tests for the A5 purity guard (phase 02 task 01).
|
|
|
|
Proves the shipped `sim/` package imports nothing forbidden, and — just as
|
|
important — that the guard actually catches a violation when one exists.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from undercooked.sim.purity import (
|
|
FORBIDDEN_ROOTS,
|
|
PurityError,
|
|
assert_pure_imports,
|
|
find_violations,
|
|
)
|
|
|
|
pytestmark = pytest.mark.unit
|
|
|
|
|
|
def test_forbidden_roots_are_the_plan_a5_list() -> None:
|
|
assert frozenset({"asyncio", "fastapi", "socket", "time", "uvicorn"}) == FORBIDDEN_ROOTS
|
|
|
|
|
|
def test_shipped_sim_package_is_pure() -> None:
|
|
assert find_violations() == []
|
|
assert_pure_imports() # must not raise
|
|
|
|
|
|
def test_clean_module_with_stdlib_sibling_and_relative_imports(tmp_path: Path) -> None:
|
|
(tmp_path / "ok.py").write_text(
|
|
"import math\n"
|
|
"from hashlib import sha256\n"
|
|
"from undercooked.sim.model import Vec2\n"
|
|
"from . import model\n",
|
|
encoding="utf-8",
|
|
)
|
|
assert find_violations(tmp_path) == []
|
|
|
|
|
|
def test_comments_and_strings_are_not_imports(tmp_path: Path) -> None:
|
|
(tmp_path / "prose.py").write_text(
|
|
'# we must never "import asyncio" here\nDOC = "import time is forbidden"\n',
|
|
encoding="utf-8",
|
|
)
|
|
assert find_violations(tmp_path) == []
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("statement", "root"),
|
|
[
|
|
("import asyncio", "asyncio"),
|
|
("import time", "time"),
|
|
("from time import monotonic", "time"),
|
|
("import socket", "socket"),
|
|
("from socket import socket", "socket"),
|
|
("from fastapi import FastAPI", "fastapi"),
|
|
("import uvicorn", "uvicorn"),
|
|
("import uvicorn.protocols", "uvicorn"),
|
|
],
|
|
)
|
|
def test_find_violations_detects_each_forbidden_import(
|
|
tmp_path: Path, statement: str, root: str
|
|
) -> None:
|
|
(tmp_path / "bad.py").write_text(f"{statement}\n", encoding="utf-8")
|
|
assert find_violations(tmp_path) == [f"bad.py: imports {root}"]
|
|
|
|
|
|
def test_find_violations_scans_subpackages(tmp_path: Path) -> None:
|
|
sub = tmp_path / "deep"
|
|
sub.mkdir()
|
|
(sub / "leak.py").write_text("import asyncio\n", encoding="utf-8")
|
|
assert find_violations(tmp_path) == ["deep/leak.py: imports asyncio"]
|
|
|
|
|
|
def test_assert_pure_imports_raises_with_the_violation_list(tmp_path: Path) -> None:
|
|
(tmp_path / "bad.py").write_text("import time\nimport socket\n", encoding="utf-8")
|
|
with pytest.raises(PurityError, match=r"bad\.py: imports socket"):
|
|
assert_pure_imports(tmp_path)
|