import importlib.util import pathlib _PATH = pathlib.Path(__file__).resolve().parent.parent / "scripts" / "repo-scan.py" _spec = importlib.util.spec_from_file_location("repo_scan", _PATH) rs = importlib.util.module_from_spec(_spec) _spec.loader.exec_module(rs) GOOD = [ "# ADR-099 — Example\n", "\n", "## Status\n", "\n", "Accepted (2026-06-10)\n", "\n", "## Context\n", "\n", "Why.\n", "\n", "## Decision\n", "\n", "What.\n", "\n", "## Consequences\n", "\n", "So what.\n", ] def _checks(findings): return [f for f in findings if f["check"] == "adr-structure"] def test_good_adr_has_no_findings(): out = rs.adr_structure_findings({"docs/decisions/099-example.md": GOOD}) assert _checks(out) == [] def test_missing_mandatory_section_is_flagged(): lines = [ln for ln in GOOD if not ln.startswith("## Consequences")] out = _checks(rs.adr_structure_findings({"docs/decisions/099-example.md": lines})) assert len(out) == 1 assert "Consequences" in out[0]["detail"] def test_unparseable_status_is_flagged(): lines = [("Designed, not built.\n" if ln == "Accepted (2026-06-10)\n" else ln) for ln in GOOD] out = _checks(rs.adr_structure_findings({"docs/decisions/099-example.md": lines})) assert len(out) == 1 assert "Status not parseable" in out[0]["detail"] def test_superseded_status_is_accepted(): lines = [("Superseded by ADR-100 (2026-06-11)\n" if ln == "Accepted (2026-06-10)\n" else ln) for ln in GOOD] out = _checks(rs.adr_structure_findings({"docs/decisions/099-example.md": lines})) assert out == [] def test_proposed_status_is_accepted(): lines = [("Proposed (2026-06-04)\n" if ln == "Accepted (2026-06-10)\n" else ln) for ln in GOOD] out = _checks(rs.adr_structure_findings({"docs/decisions/099-example.md": lines})) assert out == [] def test_non_numbered_file_is_skipped(): bare = ["# ADR template\n", "\n", "## Status\n", "\n", "\n"] out = _checks(rs.adr_structure_findings({"docs/decisions/adr-template.md": bare})) assert out == [] # --- rename-incomplete ------------------------------------------------------- def _renames(findings): return [f for f in findings if f["check"] == "rename-incomplete"] def test_rename_incomplete_flags_lingering_old_name(): # ADR announces `Foo` -> `Bar`; another decisions file still says Foo present-tense. announcer = {"docs/decisions/050-rename.md": [ "## Decision\n", "We renamed `Foo` to `Bar` across the design docs.\n"]} other = {} # extra_docs (CAPABILITIES/ROADMAP) — none here lingering = {"docs/decisions/030-other.md": [ "The Foo proxy renders config from the catalog.\n"]} announcer.update(lingering) out = _renames(rs.rename_incomplete_findings(announcer, other)) assert len(out) == 1 assert out[0]["path"] == "docs/decisions/030-other.md" assert out[0]["line"] == 1 assert out[0]["severity"] == "medium" assert "Foo" in out[0]["detail"] and "Bar" in out[0]["detail"] def test_rename_incomplete_clean_rename_has_no_findings(): # The rename announced, and no other doc still mentions Foo. adr_files = { "docs/decisions/050-rename.md": [ "## Decision\n", "We renamed `Foo` to `Bar` across the design docs.\n"], "docs/decisions/030-other.md": [ "The Bar proxy renders config from the catalog.\n"], } out = _renames(rs.rename_incomplete_findings(adr_files, {})) assert out == [] def test_rename_incomplete_skips_historical_cue_line(): # Foo lingers only on a line carrying a historical/negation cue → no finding. adr_files = { "docs/decisions/050-rename.md": [ "## Decision\n", "We renamed `Foo` to `Bar` across the design docs.\n"], "docs/decisions/030-other.md": [ "Foo was rejected; we run Bar now.\n", "The history of Foo informs the choice.\n"], } out = _renames(rs.rename_incomplete_findings(adr_files, {})) assert out == [] def test_rename_incomplete_skips_announcing_adr_itself(): # The announcing ADR mentions Foo (it has to) — must not flag itself. adr_files = { "docs/decisions/050-rename.md": [ "## Decision\n", "We renamed `Foo` to `Bar`.\n", "Operators who configured Foo should switch their habits.\n"], } out = _renames(rs.rename_incomplete_findings(adr_files, {})) assert out == [] def test_rename_incomplete_skips_line_naming_new_term(): # A line that mentions both Foo and Bar is explaining the rename → skipped. adr_files = { "docs/decisions/050-rename.md": [ "## Decision\n", "We renamed `Foo` to `Bar`.\n"], "docs/decisions/030-other.md": [ "Foo is being phased out for Bar in this paragraph.\n"], } out = _renames(rs.rename_incomplete_findings(adr_files, {})) assert out == [] def test_rename_incomplete_searches_extra_docs(): # A lingering OLD name in CAPABILITIES.md (an extra_docs file) is flagged. adr_files = {"docs/decisions/050-rename.md": [ "## Decision\n", "We renamed `Foo` to `Bar`.\n"]} extra = {"docs/CAPABILITIES.md": ["The Foo proxy is what we deploy.\n"]} out = _renames(rs.rename_incomplete_findings(adr_files, extra)) assert len(out) == 1 assert out[0]["path"] == "docs/CAPABILITIES.md" def test_rename_incomplete_ignores_ambiguous_adr_pointer_assertion(): # "the ADR-017 prose ... is updated to read Caddy" must NOT parse ADR-017 as the # old name (it is a doc pointer). With ADR-017 rejected, no assertion → no finding, # even though 'ADR-017' appears in many other docs. adr_files = { "docs/decisions/024-reverse-proxy.md": [ "## Consequences\n", '- ADR-017 prose that mentioned Traefik is updated to read "Caddy".\n'], "docs/decisions/008-testing.md": [ "Level 4 UI verification follows ADR-017.\n"], } out = _renames(rs.rename_incomplete_findings(adr_files, {})) assert out == []