Files
orchestrator/tests/test_onboarding_invariants.py

117 lines
4.9 KiB
Python

"""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"
)