"""Documentation acceptance for the MCP restart governance ADR (#656). Enforces issue #656 acceptance criteria: * AC1 — policy document exists with an authorization matrix and the recorded v1 decision (controller approval + automated safety gates). * AC2 — restart is stated as a last resort with enumerated narrower recoveries. * AC3 — a unilateral LLM full restart with affected sessions is forbidden. * AC4 — break-glass conditions are listed. * AC5 — the ADR is linked to #655, #652, #653, #630, #642, and is cross-linked from the safety model and the web-console deployment boundary docs. """ from pathlib import Path REPO_ROOT = Path(__file__).resolve().parent.parent ADR = REPO_ROOT / "docs" / "architecture" / "mcp-restart-governance.md" ADR_BASENAME = "mcp-restart-governance.md" CROSS_LINK_DOCS = ( REPO_ROOT / "docs" / "safety-model.md", REPO_ROOT / "docs" / "webui-deployment.md", ) LINKED_ISSUES = ("#655", "#652", "#653", "#630", "#642") POLICY_IDS = ("RG-01", "RG-02", "RG-03", "RG-04", "RG-05", "RG-06", "RG-07", "RG-08") def _read(path: Path) -> str: assert path.is_file(), f"missing {path.relative_to(REPO_ROOT)}" return path.read_text(encoding="utf-8") def test_ac1_adr_exists_with_matrix_and_v1_decision(): text = _read(ADR) lower = text.lower() assert text.lstrip().startswith("#"), "ADR lacks a title" assert "#656" in text assert "authorization matrix" in lower # The matrix is a real table with the worker and privileged roles. for role in ("author", "reviewer", "merger", "reconciler", "controller", "operator", "admin"): assert role in lower, f"authorization matrix missing role {role!r}" # Recorded v1 decision. assert "restart-governance/v1" in text assert "controller approval" in lower and "automated safety gates" in lower def test_ac2_restart_is_last_resort_with_narrower_recoveries(): text = _read(ADR) lower = text.lower() assert "last resort" in lower # Enumerated narrower recoveries precede full restart on the ladder. for rung in ("reconnect", "rebind", "scoped restart", "full restart", "host"): assert rung in lower, f"recovery ladder missing rung {rung!r}" def test_ac3_forbids_unilateral_llm_full_restart_with_affected_sessions(): text = _read(ADR) lower = text.lower() assert "forbidden" in lower assert "llm" in lower and "restart" in lower assert "unilateral" in lower # A worker role must not perform or authorize full/host restart. assert "must not" in lower def test_ac4_break_glass_conditions_listed(): text = _read(ADR) lower = text.lower() assert "break-glass" in lower assert "incident" in lower assert "audit" in lower def test_ac5_adr_links_issue_lineage(): text = _read(ADR) for issue in LINKED_ISSUES: assert issue in text, f"ADR must link issue {issue}" def test_ac5_safety_model_and_deployment_cross_link_adr(): for path in CROSS_LINK_DOCS: text = _read(path) assert ADR_BASENAME in text, ( f"{path.relative_to(REPO_ROOT)} must cross-link {ADR_BASENAME} " f"(issue #656 acceptance criterion 5)" ) def test_policy_ids_present_for_enforcement_code(): text = _read(ADR) for pid in POLICY_IDS: assert pid in text, f"policy id {pid} missing from ADR" def test_failure_behavior_denies_on_ambiguity(): text = _read(ADR) lower = text.lower() assert "ambiguous" in lower and "deny" in lower def test_cross_links_do_not_embed_secrets(): for path in (ADR,) + CROSS_LINK_DOCS: text = _read(path) for marker in ("ghp_", "BEGIN PRIVATE KEY", "Authorization: Bearer"): assert marker not in text, f"{path} contains {marker!r}"