phase: 02_level_content_and_validation
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.
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user