diff --git a/tests/test_docs_check.py b/tests/test_docs_check.py new file mode 100644 index 0000000..d4cb2e4 --- /dev/null +++ b/tests/test_docs_check.py @@ -0,0 +1,198 @@ +from __future__ import annotations + +import shutil +import tempfile +import unittest +from pathlib import Path + +from tools.check_documentation import ( + COMMAND_REFERENCE_NOTICE, + MAX_DIAGNOSTICS, + DocumentationPolicy, + check_documentation, +) + +ROOT = Path(__file__).resolve().parents[1] +SCHEMA = ROOT / "schemas/reference-adapter.schema.json" + + +class DocumentationCheckTests(unittest.TestCase): + def repository(self) -> Path: + root = Path(self.enterContext(tempfile.TemporaryDirectory())) + (root / "docs").mkdir() + (root / "schemas").mkdir() + shutil.copyfile(SCHEMA, root / "schemas/reference-adapter.schema.json") + (root / "pending.txt").write_text("", encoding="utf-8") + return root + + @staticmethod + def policy(*required: str) -> DocumentationPolicy: + return DocumentationPolicy( + required_pages=tuple(Path(item) for item in required), + pending_inventory=Path("pending.txt"), + ) + + @staticmethod + def command_reference() -> str: + return f"# DocForge command reference\n\n{COMMAND_REFERENCE_NOTICE}\n\n## CLI commands\n" + + def test_valid_graph_h1_anchors_and_reference_adapter_toml_pass(self) -> None: + root = self.repository() + (root / "README.md").write_text( + "# Documentation\n\n" + "[Guide](docs/GUIDE.md#adapter-setup) and " + "[commands](docs/COMMAND_REFERENCE.md). [Top](#documentation)\n", + encoding="utf-8", + ) + (root / "docs/GUIDE.md").write_text( + "# Guide\n\n" + "## Adapter *setup*\n\n" + "Reference adapter configuration `.docforge/reference-adapter.toml`:\n\n" + "```toml\n" + "schema_version = 1\n" + 'project_id = "example-project"\n' + 'title = "Example"\n' + 'language = "python"\n' + 'source_roots = ["src"]\n' + "```\n\n" + "[Back](../README.md#documentation)\n", + encoding="utf-8", + ) + (root / "docs/COMMAND_REFERENCE.md").write_text( + self.command_reference(), + encoding="utf-8", + ) + + report = check_documentation( + root, + policy=self.policy( + "README.md", + "docs/GUIDE.md", + "docs/COMMAND_REFERENCE.md", + ), + ) + + self.assertTrue(report.ok, [item.render() for item in report.diagnostics]) + self.assertEqual(3, report.markdown_pages) + self.assertEqual(1, report.reference_adapter_examples) + + def test_links_anchors_h1_and_generated_notice_fail_closed(self) -> None: + root = self.repository() + (root / "README.md").write_text( + "# Documentation\n\n" + "[bad anchor](docs/GUIDE.md#missing) " + "[missing file](docs/ABSENT.md) " + "[commands](docs/COMMAND_REFERENCE.md).\n", + encoding="utf-8", + ) + (root / "docs/GUIDE.md").write_text( + "# Guide\n\n# Duplicate\n", + encoding="utf-8", + ) + (root / "docs/COMMAND_REFERENCE.md").write_text( + "# DocForge command reference\n\n## CLI commands\n", + encoding="utf-8", + ) + + report = check_documentation( + root, + policy=self.policy( + "README.md", + "docs/GUIDE.md", + "docs/COMMAND_REFERENCE.md", + ), + ) + + self.assertFalse(report.ok) + self.assertEqual( + {"DOC016", "DOC017", "DOC020", "DOC028"}, + {item.code for item in report.diagnostics}, + ) + + def test_only_prior_milestone_records_are_reachability_exempt(self) -> None: + root = self.repository() + (root / "README.md").write_text("# Documentation\n", encoding="utf-8") + (root / "docs/MILESTONE_2_BASELINE.md").write_text( + "# Historical evidence\n", + encoding="utf-8", + ) + (root / "docs/ORPHAN.md").write_text("# Orphan\n", encoding="utf-8") + + report = check_documentation( + root, + policy=self.policy("README.md"), + ) + + orphans = [item.path for item in report.diagnostics if item.code == "DOC023"] + self.assertEqual(["docs/ORPHAN.md"], orphans) + + def test_pending_page_becomes_a_hard_failure_as_soon_as_it_exists(self) -> None: + root = self.repository() + (root / "README.md").write_text("# Documentation\n", encoding="utf-8") + (root / "pending.txt").write_text("docs/FUTURE.md\n", encoding="utf-8") + policy = self.policy("README.md", "docs/FUTURE.md") + + pending = check_documentation(root, policy=policy) + + self.assertTrue(pending.ok) + self.assertEqual(1, pending.pending_pages) + + (root / "docs/FUTURE.md").write_text("# Future\n", encoding="utf-8") + stale = check_documentation(root, policy=policy) + + self.assertIn("DOC014", {item.code for item in stale.diagnostics}) + self.assertIn("DOC023", {item.code for item in stale.diagnostics}) + + def test_unknown_pending_page_and_invalid_toml_are_reported(self) -> None: + root = self.repository() + (root / "README.md").write_text( + "# Documentation\n\n[Adapter](docs/ADAPTER.md)\n", + encoding="utf-8", + ) + (root / "docs/ADAPTER.md").write_text( + "# Adapter\n\n" + "```toml reference-adapter\n" + "schema_version = 1\n" + 'project_id = "cpp-example"\n' + 'title = "C++"\n' + 'language = "cpp"\n' + 'source_roots = ["src"]\n' + "```\n", + encoding="utf-8", + ) + (root / "pending.txt").write_text("docs/UNKNOWN.md\n", encoding="utf-8") + + report = check_documentation( + root, + policy=self.policy("README.md", "docs/ADAPTER.md"), + ) + + self.assertIn("DOC012", {item.code for item in report.diagnostics}) + schema_failures = [item for item in report.diagnostics if item.code == "DOC026"] + self.assertEqual(1, len(schema_failures)) + self.assertIn("compilation_database", schema_failures[0].message) + + def test_diagnostics_are_deterministically_bounded(self) -> None: + root = self.repository() + (root / "README.md").write_text("# Documentation\n", encoding="utf-8") + for index in range(MAX_DIAGNOSTICS + 7): + (root / "docs" / f"ORPHAN_{index:03d}.md").write_text( + f"# Orphan {index}\n", + encoding="utf-8", + ) + + report = check_documentation( + root, + policy=self.policy("README.md"), + ) + + self.assertEqual(MAX_DIAGNOSTICS, len(report.diagnostics)) + self.assertEqual(7, report.omitted_diagnostics) + self.assertEqual( + sorted(report.diagnostics), + list(report.diagnostics), + ) + + +if __name__ == "__main__": + unittest.main()