"""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}" # --- Coordinator doc stays in lock-step with the tool (#886 review blocker B2) -- # # PR #882 moved the #661 drain-proof hard gate *into* gitea_request_mcp_restart, # but the coordinator document still described the proof as "a separate child" # and omitted both new parameters. Nothing referenced that document, so nothing # caught the drift. These tests bind the prose to the real signature. COORDINATOR_DOC = REPO_ROOT / "docs" / "mcp-restart-coordinator.md" # Affirmative claims that were accurate before #661 landed and are now false. # Matched against whitespace-normalized text so re-wrapping cannot hide them. # Deliberately not the bare phrase "a separate child": the corrected prose uses # it in a negation ("no longer a separate child operation"), and a guard that # forbids naming the old behaviour would block explaining that it changed. STALE_PRE_661_PHRASES = ( "gated by a drain proof (a separate child)", "is a later child gated by a drain proof", "mutative apply path is explicitly out of scope", "apply is gated by a drain proof (a separate child)", ) def _documented_signature_block() -> str: """The fenced signature block for the tool, as published in the doc.""" text = _read(COORDINATOR_DOC) marker = "gitea_request_mcp_restart(" start = text.index(marker) end = text.index("```", start) return text[start:end] def test_documented_signature_matches_the_real_tool_signature(): import inspect import gitea_mcp_server block = _documented_signature_block() real = inspect.signature(gitea_mcp_server.gitea_request_mcp_restart) for name in real.parameters: assert name in block, ( f"docs/mcp-restart-coordinator.md documents no {name!r} parameter; " "the published signature has drifted from the tool" ) def test_drain_proof_and_break_glass_parameters_are_documented(): block = _documented_signature_block() for name in ("drain_proof_json", "request_break_glass"): assert name in block, f"signature block missing {name}" def test_restart_class_and_target_scoping_parameters_survive(): block = _documented_signature_block() for name in ("restart_class", "target_session_id", "target_role", "target_connector"): assert name in block, f"signature block lost #663 parameter {name}" def test_gate_is_documented_as_executing_inside_this_tool(): lower = _read(COORDINATOR_DOC).lower() assert "inside this tool" in lower, ( "the coordinator doc must state that the drain-proof gate executes in " "gitea_request_mcp_restart, not in a later child" ) assert "no longer a separate child operation" in lower def test_stale_pre_661_wording_cannot_return(): normalized = " ".join(_read(COORDINATOR_DOC).split()).lower() for phrase in STALE_PRE_661_PHRASES: assert phrase not in normalized, ( f"stale pre-#661 wording returned to the coordinator doc: {phrase!r}" ) def test_dry_run_versus_apply_behavior_is_documented(): lower = _read(COORDINATOR_DOC).lower() assert "dry_run=true" in lower and "dry_run=false" in lower assert "apply_supported" in lower and "restart_performed" in lower assert "never restarts anything" in lower def test_authorization_ordering_and_conjunction_are_documented(): text = _read(COORDINATOR_DOC) lower = text.lower() assert "authorization ordering" in lower assert "allow_restart" in text assert "apply_authorized" in text # The conjunction itself, and the attribution fields behind it. assert "gate.allow and allow_restart" in text for field in ("drain_gate_allow", "restart_class_authorized"): assert field in text, f"doc omits apply_gate.{field}" def test_break_glass_scope_is_documented_as_drain_proof_only(): text = _read(COORDINATOR_DOC) lower = text.lower() assert "break-glass" in lower assert "drain proof only" in lower, ( "doc must state break-glass never bypasses the restart-class matrix" ) assert "GITEA_BREAKGLASS_RESTART_AUTHORIZATION" in text def test_fail_closed_on_apply_is_documented(): lower = _read(COORDINATOR_DOC).lower() assert "fail closed" in lower for condition in ("expired", "unclean", "tampered", "stale"): assert condition in lower, f"fail-closed list omits {condition!r}" def test_coordinator_doc_embeds_no_secrets(): text = _read(COORDINATOR_DOC) for marker in ("ghp_", "BEGIN PRIVATE KEY", "Authorization: Bearer"): assert marker not in text, f"{COORDINATOR_DOC} contains {marker!r}"