Files
pulse/scripts/tests/test_docs_mirror.py
rcourtman d28666c396 Guard shipped docs mirror sync at commit time
Docs under frontend-modern/public/docs are byte-for-byte copies of repo
docs, but the only guard was a CI vitest the git hooks never run. On
2026-09-01 two commits (f4886c2dfb, f313882a7b) each edited a mirrored
doc without its copy, passed the hooks, and broke main's Frontend job.

Add scripts/check_docs_mirror.py with an index-blob --staged mode wired
into the pre-commit hook: a commit that stages either side of an
out-of-sync pair (or an orphan shipped copy) fails with the exact sync
command, while pre-existing drift from other commits only warns. The
worktree mode runs as a named step in the public-docs workflow, with
unit tests in scripts/tests picked up by the existing runner. The
docsLinks vitest stays as the CI backstop.

Build-time generation of public/docs was considered and rejected: the
shipped set is a curated subset (61 of 421 docs), so generation still
needs a hand-maintained manifest while adding build, dev-server, and
test-order coupling.
2026-09-01 11:16:05 +01:00

164 lines
5.9 KiB
Python

#!/usr/bin/env python3
"""Guard the shipped-docs mirror check that backs the pre-commit hook."""
from __future__ import annotations
import importlib.util
from pathlib import Path
import subprocess
import sys
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[2]
SCRIPT = ROOT / "scripts" / "check_docs_mirror.py"
SPEC = importlib.util.spec_from_file_location("check_docs_mirror", SCRIPT)
assert SPEC is not None and SPEC.loader is not None
docs_mirror = importlib.util.module_from_spec(SPEC)
sys.modules[SPEC.name] = docs_mirror
SPEC.loader.exec_module(docs_mirror)
def run_git(root: Path, *args: str) -> None:
subprocess.run(
[
"git",
"-C",
str(root),
"-c",
"user.email=test@example.invalid",
"-c",
"user.name=test",
*args,
],
check=True,
capture_output=True,
)
def write(root: Path, relative: str, content: str) -> None:
path = root / relative
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content, encoding="utf-8")
class DocsMirrorMappingTest(unittest.TestCase):
def test_docs_sourced_mapping(self) -> None:
self.assertEqual(
docs_mirror.source_for("frontend-modern/public/docs/i18n/de/README.md"),
"docs/i18n/de/README.md",
)
def test_root_sourced_mapping(self) -> None:
self.assertEqual(
docs_mirror.source_for("frontend-modern/public/docs/SECURITY.md"),
"SECURITY.md",
)
class DocsMirrorStagedTest(unittest.TestCase):
def setUp(self) -> None:
self._temporary = tempfile.TemporaryDirectory()
self.addCleanup(self._temporary.cleanup)
self.root = Path(self._temporary.name)
run_git(self.root, "init", "-q")
def test_synced_staged_pair_passes(self) -> None:
write(self.root, "docs/GUIDE.md", "# Guide\n")
write(self.root, "frontend-modern/public/docs/GUIDE.md", "# Guide\n")
run_git(self.root, "add", "docs/GUIDE.md", "frontend-modern/public/docs/GUIDE.md")
errors, warnings = docs_mirror.check_staged(self.root)
self.assertEqual(errors, [])
self.assertEqual(warnings, [])
def test_staged_source_with_stale_mirror_fails(self) -> None:
write(self.root, "docs/GUIDE.md", "# Guide v1\n")
write(self.root, "frontend-modern/public/docs/GUIDE.md", "# Guide v1\n")
run_git(self.root, "add", "-A")
run_git(self.root, "commit", "-q", "-m", "seed")
write(self.root, "docs/GUIDE.md", "# Guide v2\n")
run_git(self.root, "add", "docs/GUIDE.md")
errors, warnings = docs_mirror.check_staged(self.root)
self.assertEqual(len(errors), 1)
self.assertIn("docs/GUIDE.md and frontend-modern/public/docs/GUIDE.md", errors[0])
self.assertIn("git show :docs/GUIDE.md", errors[0])
self.assertEqual(warnings, [])
def test_staged_root_sourced_doc_with_stale_mirror_fails(self) -> None:
write(self.root, "SECURITY.md", "# Security v1\n")
write(self.root, "frontend-modern/public/docs/SECURITY.md", "# Security v1\n")
run_git(self.root, "add", "-A")
run_git(self.root, "commit", "-q", "-m", "seed")
write(self.root, "SECURITY.md", "# Security v2\n")
run_git(self.root, "add", "SECURITY.md")
errors, _warnings = docs_mirror.check_staged(self.root)
self.assertEqual(len(errors), 1)
self.assertIn("SECURITY.md and frontend-modern/public/docs/SECURITY.md", errors[0])
def test_staged_orphan_mirror_fails(self) -> None:
write(self.root, "frontend-modern/public/docs/NEW.md", "# New\n")
run_git(self.root, "add", "frontend-modern/public/docs/NEW.md")
errors, _warnings = docs_mirror.check_staged(self.root)
self.assertEqual(len(errors), 1)
self.assertIn("no repo source docs/NEW.md", errors[0])
def test_preexisting_drift_only_warns_on_unrelated_commit(self) -> None:
write(self.root, "docs/GUIDE.md", "# Guide v2\n")
write(self.root, "frontend-modern/public/docs/GUIDE.md", "# Guide v1\n")
run_git(self.root, "add", "-A")
run_git(self.root, "commit", "-q", "-m", "seed drift")
write(self.root, "unrelated.txt", "x\n")
run_git(self.root, "add", "unrelated.txt")
errors, warnings = docs_mirror.check_staged(self.root)
self.assertEqual(errors, [])
self.assertEqual(len(warnings), 1)
self.assertIn("docs/GUIDE.md and frontend-modern/public/docs/GUIDE.md", warnings[0])
def test_staged_pair_with_github_tree_link_fails(self) -> None:
content = "See https://github.com/rcourtman/Pulse/blob/main/docs/OTHER.md\n"
write(self.root, "docs/GUIDE.md", content)
write(self.root, "frontend-modern/public/docs/GUIDE.md", content)
run_git(self.root, "add", "-A")
errors, _warnings = docs_mirror.check_staged(self.root)
self.assertEqual(len(errors), 1)
self.assertIn("must not link to", errors[0])
class DocsMirrorWorktreeTest(unittest.TestCase):
def test_worktree_drift_and_sync(self) -> None:
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
write(root, "docs/GUIDE.md", "# Guide v2\n")
write(root, "frontend-modern/public/docs/GUIDE.md", "# Guide v1\n")
write(root, "SECURITY.md", "# Security\n")
write(root, "frontend-modern/public/docs/SECURITY.md", "# Security\n")
errors, checked = docs_mirror.check_worktree(root)
self.assertEqual(checked, 2)
self.assertEqual(len(errors), 1)
self.assertIn("cp docs/GUIDE.md frontend-modern/public/docs/GUIDE.md", errors[0])
def test_repo_shipped_docs_are_synced(self) -> None:
errors, checked = docs_mirror.check_worktree(ROOT)
self.assertEqual(errors, [])
self.assertGreater(checked, 0)
if __name__ == "__main__":
unittest.main()