"""Unit tests for the pure helpers in harness.screenshot (Phase 3 U1). The Playwright capture itself needs a live app (exercised in the U1 live demo); here we cover the pure bits: the artifact path and the SCREENSHOT-hook resolution. Run cold: cc-ci-run -m pytest tests/unit/test_screenshot.py -q """ from __future__ import annotations import os import sys sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "runner")) from harness import meta as meta_mod # noqa: E402 from harness import screenshot as S # noqa: E402 def test_screenshot_path(): assert S.screenshot_path("/var/lib/cc-ci-runs/42") == "/var/lib/cc-ci-runs/42/screenshot.png" def test_hook_none_when_absent(): assert S._load_screenshot_hook(None) is None assert S._load_screenshot_hook({}) is None assert S._load_screenshot_hook({"SCREENSHOT": "not-callable"}) is None def test_hook_returned_when_callable(): def hook(page, domain, meta): pass assert S._load_screenshot_hook({"SCREENSHOT": hook}) is hook def test_screenshot_reachable_through_real_load_path(tmp_path): """R2 proof (rcust P1): a recipe SCREENSHOT hook declared in recipe_meta.py arrives at screenshot._load_screenshot_hook through the REAL orchestrator load path (meta.load — the object run_recipe_ci passes to capture()). Under the old six-loader world the orchestrator's L1 allowlist dropped SCREENSHOT, so the hook was unreachable (spec §8 R2).""" d = tmp_path / "shotrecipe" d.mkdir() (d / "recipe_meta.py").write_text( "def SCREENSHOT(page, ctx):\n return None\n", ) meta = meta_mod.load("shotrecipe", tests_dir=str(tmp_path)) hook = S._load_screenshot_hook(meta) assert callable(hook), "SCREENSHOT hook did not survive the orchestrator load path (R2)" assert S._load_screenshot_hook(meta_mod.load("no-such", tests_dir=str(tmp_path))) is None