"""Documentation acceptance for the web console architecture ADR (#632 / epic #631). Enforces the acceptance criteria of issue #632: * AC1 — the ADR exists and covers layers, authority, phases, API versioning, and a page map. * AC2 — every #631 child (#632–#651) maps to at least one architectural component. * AC3 — the closed MVP (#425–#436) is stated as foundation, not recreated. * AC4 — forbidden paths are explicit: raw provider incidents as work, browser-held tokens, process-kill recovery. * AC5 — a controller can approve the document without reading chat history. Plus the linkage requirement: ``docs/webui-local-dev.md`` cross-links the ADR. """ from pathlib import Path REPO_ROOT = Path(__file__).resolve().parent.parent ADR = ( REPO_ROOT / "docs" / "architecture" / "webui-control-plane-console-architecture-adr.md" ) ADR_BASENAME = "webui-control-plane-console-architecture-adr.md" LOCAL_DEV = REPO_ROOT / "docs" / "webui-local-dev.md" # Epic #631 children, phases 1-4 (twenty capability areas). EPIC_CHILDREN = tuple(f"#{number}" for number in range(632, 652)) 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_required_sections(): text = _read(ADR) lower = text.lower() assert text.lstrip().startswith("#"), "ADR lacks a title" assert "#631" in text and "#632" in text for heading in ( "## 2. Decision summary", "## 4. Authority boundaries", "## 5. Request flow and the redaction boundary", "## 6. API naming and versioning", "## 7. Page map", "## 8. Component ownership", "## 9. Phase gates", "## 11. Forbidden paths", ): assert heading in text, f"ADR must contain section {heading!r}" assert "browser ui" in lower and "domain loader" in lower assert "control-plane db" in lower and "capability gate" in lower def test_ac1_api_versioning_is_decided_including_legacy_routes(): text = _read(ADR) assert "/api/v1/" in text, "ADR must decide the versioned API prefix" assert "/api/v2/" in text, "ADR must state how breaking changes are handled" lower = text.lower() assert "compatibility alias" in lower, ( "ADR must say what happens to the existing unversioned MVP exports" ) def test_ac1_page_map_covers_mvp_routes(): text = _read(ADR) for route in ("`/`", "`/health`", "`/projects`", "`/prompts`", "`/runtime`", "`/audit`", "`/actions`"): assert route in text, f"page map must account for MVP route {route}" def test_ac2_every_epic_child_maps_to_a_component(): text = _read(ADR) ownership = text.split("## 8. Component ownership", 1)[-1].split("## 9.", 1)[0] missing = [child for child in EPIC_CHILDREN if child not in ownership] assert not missing, ( f"epic #631 children without an architectural component: {missing}" ) def test_ac2_every_child_row_declares_a_phase(): text = _read(ADR) ownership = text.split("## 8. Component ownership", 1)[-1].split("## 9.", 1)[0] for child in EPIC_CHILDREN: row = next( (line for line in ownership.splitlines() if line.startswith(f"| {child} ")), None, ) assert row is not None, f"no ownership row for {child}" assert row.rstrip().endswith(("| 1 |", "| 2 |", "| 3 |", "| 4 |")), ( f"ownership row for {child} must end with its phase: {row!r}" ) def test_ac3_mvp_is_foundation_not_recreated(): text = _read(ADR) assert "#425" in text and "#436" in text lower = text.lower() assert "do not recreate" in lower or "recreating mvp scope" in lower assert "retained and evolved" in lower def test_ac4_forbidden_paths_are_explicit(): text = _read(ADR) forbidden = text.split("## 11. Forbidden paths", 1)[-1].split("## 12.", 1)[0] lower = forbidden.lower() assert "raw provider incidents" in lower and "#612" in forbidden assert "browser-held tokens" in lower assert "process-kill recovery" in lower and "#630" in forbidden assert "ungated browser mutations" in lower def test_ac5_approval_checklist_is_self_contained(): text = _read(ADR) assert "## 12. Approval checklist" in text checklist = text.split("## 12. Approval checklist", 1)[-1].split("## 13.", 1)[0] for marker in ("1.", "2.", "3.", "4.", "5.", "6."): assert marker in checklist, f"approval checklist missing item {marker}" def test_adr_states_the_two_boundary_invariants(): text = _read(ADR) lower = text.lower() assert "no secrets to the browser" in lower assert "no ungated mutations" in lower def test_open_questions_are_recorded_not_implied(): text = _read(ADR) assert "## 13. Open questions and follow-ups" in text section = text.split("## 13. Open questions and follow-ups", 1)[-1] assert "#633" in section, "deferred authorization work must name its issue" def test_local_dev_doc_cross_links_the_adr(): text = _read(LOCAL_DEV) assert ADR_BASENAME in text, ( "docs/webui-local-dev.md must cross-link the console architecture ADR " "(issue #632 scope)" ) def test_docs_do_not_embed_secrets(): for path in (ADR, LOCAL_DEV): text = _read(path) for marker in ("ghp_", "BEGIN PRIVATE KEY", "Authorization: Bearer"): assert marker not in text, f"{path.name} contains {marker!r}"