mirror of
https://github.com/affaan-m/ECC.git
synced 2026-10-14 12:15:29 +02:00
Treat non-2xx activity responses as feed errors, preserve non-Markdown Copilot agent assets, and apply the reviewed reference escape parity correction with regenerated bindings. Allow the test runner to drain piped output on its existing success or failure exit status. Validate installed workflows and bounded real Node20/24 pipe regressions.
906 lines
33 KiB
Python
906 lines
33 KiB
Python
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](<guide with spaces.md>)\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.md> "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", ("<unterminated ", "unterminated "))
|
|
def test_artifact_scope_checks_link_after_unclosed_destination(
|
|
project: Path, prefix: str
|
|
) -> 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](<guide[ref](v1).md>)\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.md> "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>)", "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>)", "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"
|