from __future__ import annotations import json import subprocess import sys from pathlib import Path import pytest ROOT = Path(__file__).resolve().parents[1] SCRIPT = ROOT / "skills" / "docs-governance" / "scripts" / "audit-docs.py" @pytest.fixture def project(tmp_path: Path) -> Path: return tmp_path def run_audit( project: Path, scope: str, *, timeout: float | None = None ) -> subprocess.CompletedProcess[str]: return subprocess.run( [sys.executable, str(SCRIPT), "--root", str(project), "--scope", scope], text=True, capture_output=True, check=False, timeout=timeout, ) def configure_git(project: Path) -> None: subprocess.run(["git", "init"], cwd=project, capture_output=True, check=True) subprocess.run( ["git", "config", "user.email", "test@example.com"], cwd=project, check=True ) subprocess.run(["git", "config", "user.name", "Test"], cwd=project, check=True) def test_artifact_scope_fails_on_broken_markdown_link(project: Path) -> None: (project / "guide.md").write_text("[missing](docs/missing.md)\n", encoding="utf-8") result = run_audit(project, "artifacts") assert result.returncode == 1 assert "Broken Markdown link" in result.stdout def test_artifact_scope_resolves_windows_style_relative_markdown_links( project: Path, ) -> None: target = project / "docs" / "adr" / "0001.md" target.parent.mkdir(parents=True) target.write_text("# ADR\n", encoding="utf-8") (project / "guide.md").write_text( "[adr](docs\\adr\\0001.md)\n", encoding="utf-8" ) result = run_audit(project, "artifacts") assert result.returncode == 0, result.stdout def test_artifact_scope_resolves_root_relative_links_inside_project( project: Path, ) -> None: docs = project / "docs" docs.mkdir() (docs / "guide.md").write_text("[status](/PROJECT_STATUS.md)\n", encoding="utf-8") (project / "PROJECT_STATUS.md").write_text("# Status\n", encoding="utf-8") result = run_audit(project, "artifacts") assert result.returncode == 0, result.stdout def test_artifact_scope_rejects_links_that_escape_project(project: Path) -> None: outside = project.parent / "outside.md" outside.write_text("# Outside\n", encoding="utf-8") try: (project / "guide.md").write_text( "[outside](../outside.md)\n", encoding="utf-8" ) result = run_audit(project, "artifacts") assert result.returncode == 1 assert "Broken Markdown link" in result.stdout finally: outside.unlink(missing_ok=True) def test_artifact_scope_does_not_read_markdown_symlinked_outside_project( project: Path, ) -> None: outside = project.parent / "outside-secret.md" outside.write_text("[secret](missing-token-value.md)\n", encoding="utf-8") try: (project / "outside.md").symlink_to(outside) result = run_audit(project, "artifacts") assert result.returncode == 1 assert "Markdown source resolves outside the project root" in result.stdout assert "missing-token-value" not in result.stdout finally: outside.unlink(missing_ok=True) def test_artifact_scope_reports_a_broken_markdown_symlink(project: Path) -> None: (project / "broken.md").symlink_to(project / "missing.md") result = run_audit(project, "artifacts") assert result.returncode == 1 assert "Markdown source is not a regular file" in result.stdout def test_artifact_scope_reports_non_utf8_markdown(project: Path) -> None: (project / "invalid.md").write_bytes(b"\xff\xfe") result = run_audit(project, "artifacts") assert result.returncode == 1 assert "Cannot read Markdown source as a UTF-8 regular file" in result.stdout def test_artifact_scope_reports_a_nul_encoded_link_without_crashing( project: Path, ) -> None: (project / "guide.md").write_text("[invalid](a%00b.md)\n", encoding="utf-8") result = run_audit(project, "artifacts") assert result.returncode == 1 assert "Broken Markdown link" in result.stdout assert "Traceback" not in result.stderr def test_artifact_scope_ignores_protocol_relative_external_links(project: Path) -> None: (project / "guide.md").write_text( "[asset](//cdn.example.com/file.js)\n", encoding="utf-8" ) result = run_audit(project, "artifacts") assert result.returncode == 0, result.stdout def test_artifact_scope_checks_unc_style_links(project: Path) -> None: (project / "guide.md").write_text( "[missing](\\\\server\\share\\missing.md)\n", encoding="utf-8" ) result = run_audit(project, "artifacts") assert result.returncode == 1 assert "Broken Markdown link" in result.stdout assert "//server/share/missing.md" in result.stdout def test_artifact_scope_ignores_external_uri_schemes_case_insensitively( project: Path, ) -> None: (project / "guide.md").write_text( "[secure](HTTPS://example.com/guide)\n" "[transfer](ftp://example.com/file)\n" "[identifier](urn:isbn:9780140328721)\n" "[paper](doi:10.1000/182)\n" "[text](sms:+15555550123)\n" "[location](geo:37.786971,-122.399677)\n" "[mail](mailto:docs@example.com)\n" "[phone](tel:+15555550123)\n" "[inline](data:text/plain,hello)\n", encoding="utf-8", ) result = run_audit(project, "artifacts") assert result.returncode == 0, result.stdout def test_artifact_scope_ignores_opaque_external_uri_schemes(project: Path) -> None: (project / "guide.md").write_text( "[magnet](magnet:?xt=urn:btih:abcdef)\n" "[chat](irc:channel)\n" "[news](news:comp.lang.python)\n", encoding="utf-8", ) result = run_audit(project, "artifacts") assert result.returncode == 0, result.stdout def test_artifact_scope_checks_windows_drive_paths_as_local_paths( project: Path, ) -> None: (project / "guide.md").write_text( "[slash](C:/docs/guide.md)\n[backslash](C:\\docs\\guide.md)\n", encoding="utf-8", ) result = run_audit(project, "artifacts") assert result.returncode == 1 assert "Broken Markdown link" in result.stdout def test_artifact_scope_checks_colon_shaped_local_paths_as_local_paths( project: Path, ) -> None: (project / "guide.md").write_text("[local](docs:guide.md)\n", encoding="utf-8") result = run_audit(project, "artifacts") assert result.returncode == 1 assert "Broken Markdown link: 'guide.md' -> 'docs:guide.md'" in result.stdout def test_artifact_scope_escapes_percent_encoded_control_characters( project: Path, ) -> None: (project / "guide.md").write_text( "[unsafe](missing%0A%1B%5B2J.md)\n", encoding="utf-8" ) result = run_audit(project, "artifacts") assert result.returncode == 1 assert "missing\\n\\x1b[2J.md" in result.stdout assert "\x1b" not in result.stdout def test_artifact_scope_ignores_query_and_fragment_in_local_links( project: Path, ) -> None: (project / "guide.md").write_text( "[details](target.md?version=2#usage)\n", encoding="utf-8" ) (project / "target.md").write_text("# Target\n", encoding="utf-8") result = run_audit(project, "artifacts") assert result.returncode == 0, result.stdout def test_artifact_scope_resolves_markdown_destinations_with_parentheses_and_spaces( project: Path, ) -> None: (project / "topic (draft).md").write_text("# Draft\n", encoding="utf-8") (project / "guide with spaces.md").write_text("# Guide\n", encoding="utf-8") (project / "index.md").write_text( "[topic](topic (draft).md)\n[guide]()\n", encoding="utf-8", ) result = run_audit(project, "artifacts") assert result.returncode == 0, result.stdout def test_artifact_scope_ignores_optional_markdown_link_titles(project: Path) -> None: (project / "guide.md").write_text("# Guide\n", encoding="utf-8") (project / "index.md").write_text( '[plain](guide.md "Guide")\n[wrapped]( "Guide")\n', encoding="utf-8", ) result = run_audit(project, "artifacts") assert result.returncode == 0, result.stdout def test_artifact_scope_handles_many_unclosed_inline_links_linearly( project: Path, ) -> None: (project / "guide.md").write_text("[link](" * 10_000, encoding="utf-8") result = run_audit(project, "artifacts", timeout=2) assert result.returncode == 0, result.stdout @pytest.mark.unit @pytest.mark.parametrize("prefix", (" None: (project / "index.md").write_text( f"[broken]({prefix}[missing](missing-guide.md)\n", encoding="utf-8", ) result = run_audit(project, "artifacts") assert result.returncode == 1 assert "Broken Markdown link: 'index.md' -> 'missing-guide.md'" in result.stdout def test_artifact_scope_ignores_links_inside_code_examples(project: Path) -> None: (project / "guide.md").write_text( "`[inline](missing-inline.md)`\n\n```md\n[fenced](missing-fenced.md)\n```\n", encoding="utf-8", ) result = run_audit(project, "artifacts") assert result.returncode == 0, result.stdout def test_artifact_scope_ignores_title_with_escaped_quotes(project: Path) -> None: (project / "guide.md").write_text("# Guide\n", encoding="utf-8") (project / "index.md").write_text( '[guide](guide.md "The \\"Guide\\"")\n', encoding="utf-8", ) result = run_audit(project, "artifacts") assert result.returncode == 0, result.stdout def test_artifact_scope_ignores_title_with_closing_parenthesis(project: Path) -> None: (project / "guide.md").write_text("# Guide\n", encoding="utf-8") (project / "index.md").write_text( '[guide](guide.md "Guide)")\n', encoding="utf-8", ) result = run_audit(project, "artifacts") assert result.returncode == 0, result.stdout def test_artifact_scope_ignores_link_like_text_in_title(project: Path) -> None: (project / "guide.md").write_text("# Guide\n", encoding="utf-8") (project / "index.md").write_text( '[guide](guide.md "See [example](missing.md)")\n', encoding="utf-8" ) result = run_audit(project, "artifacts") assert result.returncode == 0, result.stdout def test_artifact_scope_accepts_link_shaped_destination_text(project: Path) -> None: target = project / "guide[ref](v1).md" target.write_text("# Guide\n", encoding="utf-8") (project / "index.md").write_text( "[plain](guide[ref](v1).md)\n[wrapped]()\n", encoding="utf-8", ) result = run_audit(project, "artifacts") assert result.returncode == 0, result.stdout def test_artifact_scope_checks_angle_wrapped_destination_with_title( project: Path, ) -> None: (project / "index.md").write_text( '[missing]( "Guide")\n', encoding="utf-8" ) result = run_audit(project, "artifacts") assert result.returncode == 1 assert "Broken Markdown link: 'index.md' -> 'guide.md'" in result.stdout @pytest.mark.parametrize( "usage", ("[guide][docs]", "[docs][]", "[docs]"), ) def test_artifact_scope_checks_reference_style_markdown_links( project: Path, usage: str ) -> None: (project / "index.md").write_text( f"{usage}\n\n[docs]: missing-guide.md\n", encoding="utf-8" ) result = run_audit(project, "artifacts") assert result.returncode == 1 assert "Broken Markdown link" in result.stdout assert "missing-guide.md" in result.stdout def test_artifact_scope_checks_shortcut_reference_followed_by_colon( project: Path, ) -> None: (project / "index.md").write_text( "See [docs]: details\n\n[docs]: missing-guide.md\n", encoding="utf-8" ) result = run_audit(project, "artifacts") assert result.returncode == 1 assert "Broken Markdown link: 'index.md' -> 'missing-guide.md'" in result.stdout @pytest.mark.unit @pytest.mark.parametrize("target_line", (" missing-guide.md", "missing-guide.md")) def test_artifact_scope_checks_continuation_line_reference_target( project: Path, target_line: str ) -> None: (project / "index.md").write_text( f"[docs]\n\n[docs]:\n{target_line}\n", encoding="utf-8" ) result = run_audit(project, "artifacts") assert result.returncode == 1 assert "Broken Markdown link: 'index.md' -> 'missing-guide.md'" in result.stdout def test_adr_scope_requires_every_file_in_index(project: Path) -> None: adr_dir = project / "docs" / "adr" adr_dir.mkdir(parents=True) (adr_dir / "README.md").write_text("# ADR Index\n", encoding="utf-8") (adr_dir / "0001-storage.md").write_text( "# ADR-0001\n\nStatus: accepted\n", encoding="utf-8" ) result = run_audit(project, "adr") assert result.returncode == 1 assert "ADR is missing from the canonical index" in result.stdout def test_adr_scope_accepts_indexed_decision(project: Path) -> None: adr_dir = project / "docs" / "adr" adr_dir.mkdir(parents=True) (adr_dir / "README.md").write_text( "[decision](ADR-001-Storage.md)\n", encoding="utf-8" ) (adr_dir / "ADR-001-Storage.md").write_text( "# ADR-001\n\nStatus: accepted\n", encoding="utf-8" ) result = run_audit(project, "adr") assert result.returncode == 0, result.stdout def test_adr_scope_accepts_documented_bold_status_metadata(project: Path) -> None: adr_dir = project / "docs" / "adr" adr_dir.mkdir(parents=True) (adr_dir / "README.md").write_text( "[decision](0001-storage.md)\n", encoding="utf-8" ) (adr_dir / "0001-storage.md").write_text( "# ADR-0001\n\n**Status**: accepted\n", encoding="utf-8" ) result = run_audit(project, "adr") assert result.returncode == 0, result.stdout def test_adr_scope_accepts_superseded_status_with_successor(project: Path) -> None: adr_dir = project / "docs" / "adr" adr_dir.mkdir(parents=True) (adr_dir / "README.md").write_text( "[old](0001-storage.md)\n[new](0002-storage.md)\n", encoding="utf-8" ) (adr_dir / "0001-storage.md").write_text( "# ADR-0001\n\n**Status**: superseded by ADR-0002\n", encoding="utf-8" ) (adr_dir / "0002-storage.md").write_text( "# ADR-0002\n\n**Status**: accepted\n", encoding="utf-8" ) result = run_audit(project, "adr") assert result.returncode == 0, result.stdout def test_adr_scope_rejects_missing_inline_superseded_successor(project: Path) -> None: adr_dir = project / "docs" / "adr" adr_dir.mkdir(parents=True) (adr_dir / "README.md").write_text( "[decision](0001-storage.md)\n", encoding="utf-8" ) (adr_dir / "0001-storage.md").write_text( "# ADR-0001\n\n**Status**: superseded by ADR-9999\n", encoding="utf-8" ) result = run_audit(project, "adr") assert result.returncode == 1 assert "ADR inline successor does not exist" in result.stdout def test_adr_scope_rejects_superseded_status_without_successor(project: Path) -> None: adr_dir = project / "docs" / "adr" adr_dir.mkdir(parents=True) (adr_dir / "README.md").write_text( "[decision](0001-storage.md)\n", encoding="utf-8" ) (adr_dir / "0001-storage.md").write_text( "# ADR-0001\n\n**Status**: superseded\n", encoding="utf-8" ) result = run_audit(project, "adr") assert result.returncode == 1 assert "Superseded ADR is missing an inline successor" in result.stdout def test_adr_scope_rejects_short_inline_superseded_successor(project: Path) -> None: adr_dir = project / "docs" / "adr" adr_dir.mkdir(parents=True) (adr_dir / "README.md").write_text( "[decision](0001-storage.md)\n", encoding="utf-8" ) (adr_dir / "0001-storage.md").write_text( "# ADR-0001\n\n**Status**: superseded by ADR-42\n", encoding="utf-8" ) result = run_audit(project, "adr") assert result.returncode == 1 assert "ADR has no parseable status" in result.stdout def test_adr_scope_audits_common_adr_prefixed_filenames(project: Path) -> None: adr_dir = project / "docs" / "adr" adr_dir.mkdir(parents=True) (adr_dir / "README.md").write_text( "[decision](ADR-001-Storage.md)\n", encoding="utf-8" ) (adr_dir / "ADR-001-Storage.md").write_text("# ADR-001\n", encoding="utf-8") result = run_audit(project, "adr") assert result.returncode == 1 assert "ADR has no parseable status" in result.stdout def test_adr_scope_requires_an_actual_index_link(project: Path) -> None: adr_dir = project / "docs" / "adr" adr_dir.mkdir(parents=True) (adr_dir / "README.md").write_text( "TODO: add 0001-storage.md later\n", encoding="utf-8" ) (adr_dir / "0001-storage.md").write_text( "# ADR-0001\n\nStatus: accepted\n", encoding="utf-8" ) result = run_audit(project, "adr") assert result.returncode == 1 assert "ADR is missing from the canonical index" in result.stdout def test_adr_scope_ignores_the_bundled_template(project: Path) -> None: adr_dir = project / "docs" / "adr" adr_dir.mkdir(parents=True) (adr_dir / "README.md").write_text("# ADR index\n", encoding="utf-8") (adr_dir / "template.md").write_text( "# Title\n\nStatus: proposed\n", encoding="utf-8" ) result = run_audit(project, "adr") assert result.returncode == 0, result.stdout def test_adr_scope_does_not_read_an_adr_symlinked_outside_project( project: Path, ) -> None: adr_dir = project / "docs" / "adr" adr_dir.mkdir(parents=True) outside = project.parent / "outside-adr.md" outside.write_text("private ADR body\n", encoding="utf-8") try: (adr_dir / "README.md").write_text( "[outside](0001-outside.md)\n", encoding="utf-8" ) (adr_dir / "0001-outside.md").symlink_to(outside) result = run_audit(project, "adr") assert result.returncode == 1 assert "ADR source resolves outside the project root" in result.stdout assert "private ADR body" not in result.stdout finally: outside.unlink(missing_ok=True) def test_custom_role_map_supports_existing_adr_layout(project: Path) -> None: adr_dir = project / "docs" / "architecture" / "decisions" adr_dir.mkdir(parents=True) (adr_dir / "README.md").write_text("[0001](0001-storage.md)\n", encoding="utf-8") (adr_dir / "0001-storage.md").write_text( "# ADR-0001\n\nStatus: accepted\n", encoding="utf-8" ) governance = project / ".governance" governance.mkdir() (governance / "docs-map.json").write_text( json.dumps( { "adr_dir": "docs/architecture/decisions", "adr_index": "docs/architecture/decisions/README.md", } ), encoding="utf-8", ) result = run_audit(project, "adr") assert result.returncode == 0, result.stdout assert "custom documentation role mapping" in result.stdout def test_adr_scope_rejects_missing_index_link(project: Path) -> None: adr_dir = project / "docs" / "adr" adr_dir.mkdir(parents=True) (adr_dir / "README.md").write_text("[missing](0002-missing.md)\n", encoding="utf-8") result = run_audit(project, "adr") assert result.returncode == 1 assert "ADR index link does not exist" in result.stdout def test_adr_scope_rejects_missing_supersedes_target(project: Path) -> None: adr_dir = project / "docs" / "adr" adr_dir.mkdir(parents=True) (adr_dir / "README.md").write_text("[new](0002-new.md)\n", encoding="utf-8") (adr_dir / "0002-new.md").write_text( "# ADR-0002\n\nStatus: accepted\n\n## Supersedes\n\n[old](0001-old.md)\n", encoding="utf-8", ) result = run_audit(project, "adr") assert result.returncode == 1 assert "ADR Supersedes target does not exist" in result.stdout def test_artifact_scope_rejects_unregistered_test_id_when_tests_ledger_exists( project: Path, ) -> None: docs = project / "docs" docs.mkdir() (project / "TESTS.md").write_text("# TESTS\n\nTEST-ORDER-001\n", encoding="utf-8") (docs / "spec.md").write_text( "Must be verified by TEST-ORDER-002.\n", encoding="utf-8" ) result = run_audit(project, "artifacts") assert result.returncode == 1 assert "TEST-IDs are missing from the test registry" in result.stdout def test_artifact_scope_ignores_lowercase_test_hyphenated_prose( project: Path, ) -> None: (project / "TESTS.md").write_text("# TESTS\n\nTEST-ORDER-001\n", encoding="utf-8") (project / "guide.md").write_text("Use a test-driven workflow.\n", encoding="utf-8") result = run_audit(project, "artifacts") assert result.returncode == 0, result.stdout def test_artifact_scope_reports_a_non_utf8_test_registry(project: Path) -> None: (project / "TESTS.md").write_bytes(b"\xff\xfe") (project / "spec.md").write_text("Verified by TEST-ORDER-001.\n", encoding="utf-8") result = run_audit(project, "artifacts") assert result.returncode == 1 assert "Cannot read test registry as a UTF-8 regular file" in result.stdout assert "Traceback" not in result.stderr def test_log_move_to_archive_preserves_append_only_history(project: Path) -> None: configure_git(project) old = "# LOG\n\n## [2026-01-01] init | one\n## [2026-01-02] fix | two\n" (project / "PROJECT_LOG.md").write_text(old, encoding="utf-8") subprocess.run(["git", "add", "PROJECT_LOG.md"], cwd=project, check=True) subprocess.run( ["git", "commit", "-m", "init"], cwd=project, capture_output=True, check=True ) (project / "PROJECT_LOG.md").write_text( "# LOG\n\n## [2026-01-02] fix | two\n", encoding="utf-8" ) (project / "PROJECT_LOG.archive.md").write_text( "# archive\n\n## [2026-01-01] init | one\n", encoding="utf-8" ) result = run_audit(project, "spine") assert result.returncode == 0, result.stdout assert ( "Active and archived history remain append-only when combined" in result.stdout ) def test_spine_scope_detects_recreated_root_level_deletion_zone_file( project: Path, ) -> None: (project / "PROJECT_STATUS.md").write_text( "# Status\n\n## Deletion Zone\n\n- `legacy_parser.py`\n", encoding="utf-8" ) (project / "legacy_parser.py").write_text("# recreated\n", encoding="utf-8") result = run_audit(project, "spine") assert result.returncode == 1 assert "Deletion-zone target has been recreated" in result.stdout def test_spine_scope_detects_windows_path_deletion_zone_file(project: Path) -> None: target = project / "subdir" / "legacy.py" target.parent.mkdir() target.write_text("# recreated\n", encoding="utf-8") (project / "PROJECT_STATUS.md").write_text( "# Status\n\n## Deletion Zone\n\n- `subdir\\\\legacy.py`\n", encoding="utf-8", ) result = run_audit(project, "spine") assert result.returncode == 1 assert "Deletion-zone target has been recreated" in result.stdout @pytest.mark.parametrize( "value", ( "ftp://example.com/legacy.py", "mailto:legacy@example.com", "urn:legacy:parser", pytest.param( r"..\legacy.py", marks=pytest.mark.skipif( sys.platform.startswith("win"), reason="the Windows path would escape the temporary project", ), ), ), ) def test_spine_scope_ignores_unsafe_deletion_zone_values( project: Path, value: str ) -> None: candidate = project / value candidate.parent.mkdir(parents=True, exist_ok=True) candidate.write_text("# unrelated\n", encoding="utf-8") (project / "PROJECT_STATUS.md").write_text( f"# Status\n\n## Deletion Zone\n\n- `{value}`\n", encoding="utf-8" ) result = run_audit(project, "spine") assert result.returncode == 0, result.stdout def test_spine_scope_handles_missing_git_without_traceback(project: Path) -> None: (project / "PROJECT_LOG.md").write_text( "# LOG\n\n## [2026-01-01] init | one\n", encoding="utf-8" ) result = subprocess.run( [sys.executable, str(SCRIPT), "--root", str(project), "--scope", "spine"], text=True, capture_output=True, check=False, env={"PATH": ""}, ) assert result.returncode == 0, result.stdout assert "Git history is unavailable" in result.stdout assert "Traceback" not in result.stderr def test_spine_scope_warns_when_root_has_no_git_head(project: Path) -> None: (project / "PROJECT_LOG.md").write_text( "# LOG\n\n## [2026-01-01] init | one\n", encoding="utf-8" ) result = run_audit(project, "spine") assert result.returncode == 0, result.stdout assert "Git history is unavailable" in result.stdout assert "append-only when combined" not in result.stdout def test_log_duplicate_removal_is_detected(project: Path) -> None: configure_git(project) event = "## [2026-01-01] fix | same event\n" (project / "PROJECT_LOG.md").write_text( "# LOG\n\n" + event + event, encoding="utf-8" ) subprocess.run(["git", "add", "PROJECT_LOG.md"], cwd=project, check=True) subprocess.run( ["git", "commit", "-m", "init"], cwd=project, capture_output=True, check=True ) (project / "PROJECT_LOG.md").write_text("# LOG\n\n" + event, encoding="utf-8") result = run_audit(project, "spine") assert result.returncode == 0, result.stdout assert "1 working-tree history events differ from HEAD" in result.stdout def test_role_map_rejects_absolute_paths(project: Path) -> None: governance = project / ".governance" governance.mkdir() (governance / "docs-map.json").write_text( json.dumps({"context": str(project / "CONTEXT.md")}), encoding="utf-8" ) result = run_audit(project, "context") assert result.returncode == 1 assert "must be repository-relative" in result.stdout def test_role_map_rejects_a_directory_for_a_file_role(project: Path) -> None: (project / "docs").mkdir() governance = project / ".governance" governance.mkdir() (governance / "docs-map.json").write_text( json.dumps({"context": "docs"}), encoding="utf-8" ) result = run_audit(project, "context") assert result.returncode == 1 assert "file role maps to a non-file" in result.stdout def test_role_map_rejects_a_missing_mapped_source(project: Path) -> None: governance = project / ".governance" governance.mkdir() (governance / "docs-map.json").write_text( json.dumps({"contract": "docs/missing-contract.md"}), encoding="utf-8" ) result = run_audit(project, "artifacts") assert result.returncode == 1 assert "Mapped documentation source does not exist" in result.stdout def test_role_map_does_not_follow_a_mapping_symlinked_outside_project( project: Path, ) -> None: governance = project / ".governance" governance.mkdir() outside = project.parent / "outside-map.json" outside.write_text('{"context": "private-context.md"}\n', encoding="utf-8") try: (governance / "docs-map.json").symlink_to(outside) result = run_audit(project, "context") assert result.returncode == 1 assert "repository-contained regular file" in result.stdout assert "private-context" not in result.stdout finally: outside.unlink(missing_ok=True) def test_default_role_does_not_follow_a_symlink_outside_project(project: Path) -> None: outside = project.parent / "outside-context.md" outside.write_text("private context\n", encoding="utf-8") try: (project / "CONTEXT.md").symlink_to(outside) result = run_audit(project, "context") assert result.returncode == 1 assert "Documentation role resolves outside the project root" in result.stdout assert "private context" not in result.stdout finally: outside.unlink(missing_ok=True) def test_audit_does_not_modify_repository_files(project: Path) -> None: (project / "guide.md").write_text("[missing](docs/missing.md)\n", encoding="utf-8") before = { path.relative_to(project): path.read_bytes() for path in project.rglob("*") if path.is_file() } result = run_audit(project, "full") after = { path.relative_to(project): path.read_bytes() for path in project.rglob("*") if path.is_file() } assert result.returncode == 1 assert after == before @pytest.mark.parametrize("link", ("[guide](it's.md)", '[guide](it\'s.md "Guide)")')) def test_artifact_scope_reports_missing_apostrophe_destination( project: Path, link: str ) -> None: (project / "index.md").write_text(link + "\n", encoding="utf-8") result = run_audit(project, "artifacts") assert result.returncode == 1, result.stdout assert "Broken Markdown link" in result.stdout assert "it's.md" in result.stdout @pytest.mark.parametrize( "link,filename", ( (r"[guide](file\(1\).md)", "file(1).md"), (r"[guide](file\).md)", "file).md"), (r'[guide](file\(1\).md "Guide)")', "file(1).md"), (r"[guide]()", "file(1).md"), ), ) def test_artifact_scope_resolves_escaped_destination_parentheses( project: Path, link: str, filename: str ) -> None: (project / filename).write_text("# Guide\n", encoding="utf-8") (project / "index.md").write_text(link + "\n", encoding="utf-8") result = run_audit(project, "artifacts") assert result.returncode == 0, result.stdout + result.stderr @pytest.mark.parametrize("scope", ("artifacts", "full")) @pytest.mark.parametrize("usage", ("![diagram]", "![diagram][]", "![picture][diagram]")) def test_audit_reports_missing_reference_images( project: Path, scope: str, usage: str ) -> None: (project / "index.md").write_text( usage + "\n\n[diagram]: missing.png\n", encoding="utf-8" ) result = run_audit(project, scope) assert result.returncode == 1, result.stdout assert "Broken Markdown link: 'index.md' -> 'missing.png'" in result.stdout @pytest.mark.parametrize("usage", ("![diagram]", "![diagram][]")) def test_artifact_scope_accepts_existing_reference_images( project: Path, usage: str ) -> None: (project / "diagram.png").write_bytes(b"image fixture") (project / "index.md").write_text( usage + "\n\n[diagram]: diagram.png\n", encoding="utf-8" ) result = run_audit(project, "artifacts") assert result.returncode == 0, result.stdout + result.stderr def test_artifact_scope_ignores_escaped_reference_image_bracket(project: Path) -> None: (project / "index.md").write_text( r"!\[diagram]" + "\n\n[diagram]: missing.png\n", encoding="utf-8" ) result = run_audit(project, "artifacts") assert result.returncode == 0, result.stdout + result.stderr @pytest.mark.parametrize("mapping", (b"\xff", b"{invalid json")) def test_full_audit_reports_bad_role_map_and_continues_artifact_checks( project: Path, mapping: bytes ) -> None: (project / ".governance").mkdir() (project / ".governance/docs-map.json").write_bytes(mapping) (project / "index.md").write_text("[missing](missing-guide.md)\n", encoding="utf-8") result = run_audit(project, "full") assert result.returncode == 1 assert "Cannot read .governance/docs-map.json" in result.stdout assert "Broken Markdown link: 'index.md' -> 'missing-guide.md'" in result.stdout assert "Traceback" not in result.stderr @pytest.mark.parametrize( "link,filename", ( (r"[guide](file\[1\].md)", "file[1].md"), (r"[guide](it\'s.md)", "it's.md"), (r'[guide](file\[1\].md "Guide)")', "file[1].md"), (r"[guide]()", "it's.md"), ("[guide][docs]\n\n[docs]: " + r"file\[1\].md", "file[1].md"), ("[guide][docs]\n\n[docs]: " + r"it\'s.md", "it's.md"), ), ) def test_artifact_scope_resolves_escaped_bracket_and_apostrophe_filenames( project: Path, link: str, filename: str ) -> None: (project / filename).write_text("# Guide\n", encoding="utf-8") (project / "index.md").write_text(link + "\n", encoding="utf-8") result = run_audit(project, "artifacts") assert result.returncode == 0, result.stdout + result.stderr @pytest.mark.parametrize("backslashes", range(5)) @pytest.mark.parametrize("usage", ("[diagram]", "[diagram][]")) @pytest.mark.parametrize("scope", ("artifacts", "full")) def test_audit_checks_reference_usage_after_even_backslash_runs( project: Path, backslashes: int, usage: str, scope: str ) -> None: (project / "index.md").write_text( "\\" * backslashes + usage + "\n\n[diagram]: missing.png\n", encoding="utf-8", ) before = {path: path.read_bytes() for path in project.rglob("*") if path.is_file()} result = run_audit(project, scope) expected = 1 if backslashes % 2 == 0 else 0 assert result.returncode == expected, result.stdout + result.stderr diagnostic = "Broken Markdown link: 'index.md' -> 'missing.png'" assert (diagnostic in result.stdout) == (expected == 1), result.stdout assert "Traceback" not in result.stderr after = {path: path.read_bytes() for path in project.rglob("*") if path.is_file()} assert after == before, "the actual audit CLI must not modify project files"