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.
96 lines
3.3 KiB
Python
96 lines
3.3 KiB
Python
"""Phase-01 task-01 harness smoke tests: package metadata and pytest wiring."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
import undercooked
|
|
|
|
pytestmark = pytest.mark.unit
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
HARNESS_MARKERS = ("unit", "integration", "e2e", "slow")
|
|
TEST_SUBDIRS = ("unit", "integration", "sim", "e2e")
|
|
|
|
|
|
def test_package_metadata() -> None:
|
|
assert undercooked.__version__ == "0.1.0"
|
|
assert undercooked.__all__ == []
|
|
|
|
|
|
def test_harness_markers_registered(pytestconfig: pytest.Config) -> None:
|
|
registered = {line.split(":", 1)[0].strip() for line in pytestconfig.getini("markers")}
|
|
assert set(HARNESS_MARKERS) <= registered
|
|
|
|
|
|
def test_strict_markers_enabled(pytestconfig: pytest.Config) -> None:
|
|
assert "--strict-markers" in pytestconfig.getini("addopts")
|
|
|
|
|
|
@pytest.mark.parametrize("name", TEST_SUBDIRS)
|
|
def test_test_tree_directories_exist_with_readme(name: str) -> None:
|
|
directory = REPO_ROOT / "tests" / name
|
|
assert directory.is_dir()
|
|
readme = directory / "README.md"
|
|
assert readme.is_file()
|
|
assert readme.read_text(encoding="utf-8").strip()
|
|
|
|
|
|
def test_tests_dir_is_not_a_package() -> None:
|
|
assert not (REPO_ROOT / "tests" / "__init__.py").exists()
|
|
|
|
|
|
def test_fixture_levels_dir_exists(isolated_levels_dir: None) -> None:
|
|
levels_dir = Path(os.environ["UNDERCOOKED_LEVELS_DIR"])
|
|
assert levels_dir == REPO_ROOT / "tests" / "fixtures" / "levels"
|
|
assert levels_dir.is_dir()
|
|
# Hermeticity is about *levels*: a default test must never see the shipped
|
|
# campaign. The committed `recipes.yaml` is not one (the loader never treats
|
|
# it as a level) — startup has required a recipe book since phase 02 task 04,
|
|
# and this minimal one is what lets fixture-isolated apps boot at all.
|
|
levels = [p.name for p in levels_dir.glob("*.yaml") if p.name != "recipes.yaml"]
|
|
assert not levels
|
|
|
|
|
|
def test_e2e_tests_follow_the_story_naming_convention() -> None:
|
|
"""`pytest -m e2e --collect-only` selects only `test_usNN_*.py` story suites.
|
|
|
|
Task 01 could only assert "the marker collects nothing yet". Now that the first
|
|
browser suite exists the durable invariant is the naming contract in
|
|
`tests/e2e/README.md`: a story suite that is not named for its story is one that
|
|
nobody can find, and one that cannot be collected is one that never runs.
|
|
"""
|
|
result = subprocess.run( # fixed argv, repo's own interpreter
|
|
[
|
|
sys.executable,
|
|
"-m",
|
|
"pytest",
|
|
"-m",
|
|
"e2e",
|
|
"--collect-only",
|
|
"-q",
|
|
"-o",
|
|
"addopts=", # drop the outer coverage flags: no nested .coverage writes
|
|
"-p",
|
|
"no:cacheprovider",
|
|
],
|
|
cwd=REPO_ROOT,
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
timeout=120,
|
|
)
|
|
assert result.returncode == pytest.ExitCode.OK, result.stdout + result.stderr
|
|
assert "error" not in result.stderr.lower(), result.stderr
|
|
files = sorted({line.split("::")[0] for line in result.stdout.splitlines() if "::" in line})
|
|
assert files, f"the e2e marker selected nothing:\n{result.stdout}"
|
|
for path in files:
|
|
assert path.startswith("tests/e2e/"), path
|
|
assert re.fullmatch(r"test_us\d{2}_.+\.py", Path(path).name), path
|