"""ORCH-009 TC-21: pipeline invariants are untouched by the onboarding capability. The onboarding kit/CLI lives entirely OUTSIDE the runtime (NFR-1): `src/**` is byte-for-byte untouched. These tests pin that contract: * a literal snapshot of ``STAGE_TRANSITIONS`` (the stage machine) and of the ``QG_CHECKS`` registry — any drift fails loudly; * ``src/**`` never references the onboarding tree (no runtime coupling); * the CLI's read-only imports from ``src`` stay within the CLOSED list of ADR-001 D4 (ORCH-009) — extending the list requires an ADR update; * kit prompt templates name only real quality gates (no phantom ``check_*``). """ import ast import os import re from src.qg.checks import QG_CHECKS from src.stages import STAGE_TRANSITIONS _REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) _SCRIPT_PATH = os.path.join(_REPO_ROOT, "scripts", "onboard_project.py") _KIT_AGENTS = os.path.join(_REPO_ROOT, "onboarding", "repo-skeleton", ".openclaw", "agents") # Literal snapshot of the stage machine (src/stages.py). Byte-exact NFR-1 pin: # the onboarding work item must not move a single edge/agent/gate. _EXPECTED_TRANSITIONS = { "created": {"next": "analysis", "agent": "analyst", "qg": None}, "analysis": {"next": "architecture", "agent": "architect", "qg": "check_analysis_approved"}, "architecture": {"next": "development", "agent": "developer", "qg": "check_architecture_done"}, "development": {"next": "review", "agent": "reviewer", "qg": "check_ci_green"}, "review": {"next": "testing", "agent": "tester", "qg": "check_reviewer_verdict"}, "testing": {"next": "deploy-staging", "agent": "deployer", "qg": "check_tests_passed"}, "deploy-staging": {"next": "deploy", "agent": "deployer", "qg": "check_staging_status"}, "deploy": {"next": "done", "agent": None, "qg": "check_deploy_status"}, "done": {"next": None, "agent": None, "qg": None}, "cancelled": {"next": None, "agent": None, "qg": None}, } # Snapshot of the QG registry KEYS (src/qg/checks.py::QG_CHECKS). _EXPECTED_QG_KEYS = { "check_analysis_approved", "check_analysis_complete", "check_architecture_done", "check_ci_green", "check_review_approved", "check_tests_passed", "check_reviewer_verdict", "check_tests_local", "check_deploy_status", "check_staging_status", "check_branch_mergeable", "check_staging_image_fresh", "check_security_gate", "check_coverage_gate", } # Closed read-only import list of the onboarding CLI (ADR-001 D4 ORCH-009). _ALLOWED_SRC_IMPORTS = {"src.config", "src.plane_sync", "src.projects"} def test_tc21_stage_transitions_snapshot(): assert STAGE_TRANSITIONS == _EXPECTED_TRANSITIONS, ( "STAGE_TRANSITIONS drifted — ORCH-009 must not touch the stage machine (NFR-1)" ) def test_tc21_qg_checks_registry_snapshot(): assert set(QG_CHECKS) == _EXPECTED_QG_KEYS, ( "QG_CHECKS registry drifted — ORCH-009 must not touch the gates (NFR-1)" ) def test_tc21_src_never_references_onboarding(): """No runtime coupling: src/** must not import/reference the onboarding tree.""" offenders = [] for root, _dirs, files in os.walk(os.path.join(_REPO_ROOT, "src")): for name in files: if not name.endswith(".py"): continue path = os.path.join(root, name) with open(path, encoding="utf-8") as f: if "onboard" in f.read().lower(): offenders.append(os.path.relpath(path, _REPO_ROOT)) assert not offenders, f"src/** references onboarding: {offenders}" def test_tc21_cli_src_imports_stay_in_closed_list(): """ADR-001 D4: the CLI may import ONLY src.config / src.plane_sync / src.projects.""" with open(_SCRIPT_PATH, encoding="utf-8") as f: tree = ast.parse(f.read()) found = set() for node in ast.walk(tree): if isinstance(node, ast.ImportFrom) and node.module and node.module.startswith("src"): found.add(node.module) elif isinstance(node, ast.Import): for alias in node.names: if alias.name.startswith("src"): found.add(alias.name) assert found, "the CLI must use the closed src imports (round-trip via real parser)" assert found <= _ALLOWED_SRC_IMPORTS, ( f"onboard_project.py imports outside the closed ADR D4 list: " f"{sorted(found - _ALLOWED_SRC_IMPORTS)} — extend ONLY via an ADR update" ) def test_tc21_kit_prompts_name_only_real_gates(): """A kit prompt naming a phantom gate would mislead every onboarded project.""" pattern = re.compile(r"check_[a-z_]+") for name in sorted(os.listdir(_KIT_AGENTS)): path = os.path.join(_KIT_AGENTS, name) with open(path, encoding="utf-8") as f: text = f.read() for gate in sorted(set(pattern.findall(text))): assert gate in QG_CHECKS, ( f"kit prompt {name} references gate {gate!r} absent from QG_CHECKS" )