"""Unit tests for Phase-2 discovery additions (plan ยง4.1). Proves the `custom_tests` discovery covers exactly the per-recipe `functional/` + `playwright/` subdirs as well as the top-level dir, while still excluding lifecycle `test_.py` names and honouring the HC2 repo-local approval gate. Run with: `cc-ci-run -m pytest tests/unit`. Located under tests/unit/ so the orchestrator never picks these up as overlays/custom tests. """ from __future__ import annotations import os import sys sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "runner")) from harness import discovery # noqa: E402 def _approve(tmp_path, *recipes): f = tmp_path / "approved.txt" f.write_text("".join(f"{r}\n" for r in recipes)) os.environ["CCCI_REPO_LOCAL_APPROVED_FILE"] = str(f) def teardown_function(): os.environ.pop("CCCI_REPO_LOCAL_APPROVED_FILE", None) def test_custom_tests_placement_rule_functional_playwright_only(tmp_path, monkeypatch): """Placement rule (rcust P4): custom tests are discovered ONLY under functional/ + playwright/. A top-level non-lifecycle test_*.py is NOT discovered (top level is reserved for lifecycle overlays); lifecycle names inside the subdirs stay excluded (defensive).""" # Point cc-ci's per-recipe dir at a fake recipe in tmp_path fake_recipe = "ccci-phase2-fixture" fake_dir = tmp_path / "tests" / fake_recipe (fake_dir / "functional").mkdir(parents=True) (fake_dir / "playwright").mkdir() (fake_dir / "test_sso_smoke.py").write_text("# top-level โ€” NOT discovered since P4\n") (fake_dir / "functional" / "test_health_check.py").write_text("# parity port\n") (fake_dir / "functional" / "test_content_roundtrip.py").write_text("# recipe-specific\n") (fake_dir / "playwright" / "test_login_flow.py").write_text("# UI flow\n") # lifecycle name in functional/ should be ignored (defensive) (fake_dir / "functional" / "test_install.py").write_text("# misfiled lifecycle name\n") # Patch the cc-ci dir resolver to point at our fixture monkeypatch.setattr(discovery, "cc_ci_dir", lambda r: str(tmp_path / "tests" / r)) customs = discovery.custom_tests(fake_recipe, None) names = sorted((src, os.path.basename(p)) for src, p in customs) # functional/ + playwright/ discovered; top-level custom + lifecycle name are NOT assert ("cc-ci", "test_health_check.py") in names assert ("cc-ci", "test_content_roundtrip.py") in names assert ("cc-ci", "test_login_flow.py") in names assert ("cc-ci", "test_sso_smoke.py") not in names assert ("cc-ci", "test_install.py") not in names def test_custom_tests_repo_local_subdirs_gated(tmp_path, monkeypatch): """HC2 gate still applies to functional/playwright subdirs under repo-local: not approved -> the repo-local subdir contents are ignored even if they exist.""" fake_recipe = "ccci-phase2-fixture" monkeypatch.setattr(discovery, "cc_ci_dir", lambda r: str(tmp_path / "cc-ci" / r)) (tmp_path / "cc-ci" / fake_recipe).mkdir(parents=True) rl = tmp_path / "repo" (rl / "functional").mkdir(parents=True) (rl / "functional" / "test_repo_local_specific.py").write_text("# repo-local custom\n") _approve(tmp_path) # empty allowlist โ†’ default-deny assert discovery.custom_tests(fake_recipe, str(rl)) == [] _approve(tmp_path, fake_recipe) # approved โ†’ repo-local subdir honored customs = discovery.custom_tests(fake_recipe, str(rl)) names = {(src, os.path.basename(p)) for src, p in customs} assert ("repo-local", "test_repo_local_specific.py") in names