"""Automated validation suite for documentation structural and content requirements.""" import re from pathlib import Path import pytest DOCS_DIR = Path(__file__).resolve().parent.parent.parent / "docs" ROOT_DIR = Path(__file__).resolve().parent.parent.parent class TestArchitectureDoc: def test_file_exists_and_is_non_empty(self) -> None: path = DOCS_DIR / "architecture.md" assert path.exists(), "docs/architecture.md must exist" assert path.stat().st_size > 0, "docs/architecture.md must be non-empty" def test_contains_required_section_headers(self) -> None: path = DOCS_DIR / "architecture.md" content = path.read_text() required_sections = [ "## 1. Overview", "## 2. System Context", "## 3. Component Boundaries", "## 4. PostgreSQL Domain Model", "## 5. Git Provider Abstraction", "## 6. Repository Credential Model", "## 7. Tool Manifest Model", "## 8. Tool Spawn Lifecycle", "## 9. Docker Runtime Abstraction", "## 10. Traefik Subdomain Routing Model", "## 11. Storage Layout", "## 12. Authentik OIDC Auth Flow", "## 13. Security Considerations", "## 14. Extension Points", "## 15. Environment Assumptions", "## 16. Technology Boundaries", "## 17. Acceptance Criteria for Architecture Compliance", "## 18. Deferred Decisions", ] missing = [s for s in required_sections if s not in content] assert not missing, f"Missing required sections: {missing}" def test_contains_required_references(self) -> None: path = DOCS_DIR / "architecture.md" content = path.read_text() required_refs = [ "GitProvider", "RuntimeProvider", "AccessProvider", "ToolManifest", "Authentik", "Traefik", "Portainer", "PostgreSQL", ] missing = [r for r in required_refs if r not in content] assert not missing, f"Missing required references: {missing}" class TestMvpScopeDoc: def test_file_exists_and_is_non_empty(self) -> None: path = DOCS_DIR / "mvp-scope.md" assert path.exists(), "docs/mvp-scope.md must exist" assert path.stat().st_size > 0, "docs/mvp-scope.md must be non-empty" def test_contains_required_sections(self) -> None: path = DOCS_DIR / "mvp-scope.md" content = path.read_text() required_sections = [ "## 1. Product Vision", "## 2. MVP User Journeys", "## 3. In-Scope", "## 4. Out-of-Scope", "## 5. MVP Milestones", "## 6. Dependency Order", "## 7. Definition of MVP Done", "## 8. Open Questions", ] missing = [s for s in required_sections if s not in content] assert not missing, f"Missing required sections: {missing}" class TestAllDocs: def test_no_todo_or_fixme_in_docs(self) -> None: markdown_files = list(DOCS_DIR.rglob("*.md")) assert markdown_files, "No markdown files found in docs/" violations = [] for path in markdown_files: content = path.read_text() if re.search(r"\bTODO\b", content, re.IGNORECASE): violations.append(f"{path.name} contains TODO") if re.search(r"\bFIXME\b", content, re.IGNORECASE): violations.append(f"{path.name} contains FIXME") assert not violations, f"Documentation contains placeholders: {violations}" def test_internal_links_point_to_existing_files(self) -> None: markdown_files = list(DOCS_DIR.rglob("*.md")) link_pattern = re.compile(r"\]\(([^)]+)\)") violations = [] for path in markdown_files: content = path.read_text() for match in link_pattern.finditer(content): link = match.group(1) # Skip external URLs and anchors if link.startswith("http") or link.startswith("#") or link.startswith("mailto:"): continue # Resolve relative to the docs directory or repo root if link.startswith("docs/"): target = ROOT_DIR / link elif link.startswith("../"): target = path.parent / link elif link.startswith("./"): target = path.parent / link else: # Assume relative to docs dir for bare paths like "architecture.md" target = DOCS_DIR / link if not target.exists(): violations.append(f"{path.name}: broken link to '{link}'") assert not violations, f"Broken internal links found: {violations}"