61c5554c42
Commits merged: - docs(FN-019): complete Step 6 — documentation index, conversation handoff, project brief, and build config - test(FN-019): complete Step 4 — doc validation tests, project-brief.md, and fix mvp-scope placeholders - feat(FN-019): complete Step 3 — draft mvp-scope.md with milestones, dependency order, and open questions - docs(FN-019): fix auth callback flow, Python syntax, dev bypass clarity, add AccessProvider protocol and type stubs - feat(FN-019): complete Step 2 — draft enhanced architecture.md with all 18 required sections Files changed: docs/README.md | 11 +- docs/architecture.md | 1185 ++++++++++++++++++++++++++++++++++----- docs/conversation-handoff.md | 68 +++ docs/mvp-scope.md | 184 ++++++ docs/project-brief.md | 31 + package.json | 3 + tests/docs/__init__.py | 0 tests/docs/test_architecture.py | 120 ++++ 8 files changed, 1444 insertions(+), 158 deletions(-) Fusion-Task-Id: FN-019
121 lines
4.8 KiB
Python
121 lines
4.8 KiB
Python
"""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}"
|