549 lines
23 KiB
Python
549 lines
23 KiB
Python
|
|
"""
|
||
|
|
Tests for scanner exclusion pipeline (task 2.5).
|
||
|
|
|
||
|
|
Verifies invariant #9: frozen/ignored/append-only files are present in the
|
||
|
|
tree but absent from the shortlist; .dochygiene/ and excluded dirs are never
|
||
|
|
scanned.
|
||
|
|
|
||
|
|
sys.path manipulation is intentional per task constraints (no shared conftest).
|
||
|
|
"""
|
||
|
|
|
||
|
|
import os
|
||
|
|
import shutil
|
||
|
|
import sys
|
||
|
|
import tempfile
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
# Prepend scripts/ to sys.path so we can import scanner without a package
|
||
|
|
_SCRIPTS_DIR = Path(__file__).parent.parent / "scripts"
|
||
|
|
if str(_SCRIPTS_DIR) not in sys.path:
|
||
|
|
sys.path.insert(0, str(_SCRIPTS_DIR))
|
||
|
|
|
||
|
|
from scanner import Scanner # noqa: E402
|
||
|
|
|
||
|
|
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
# Helpers
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
def _make_tree(tmp: Path, files: dict) -> None:
|
||
|
|
"""Create files in *tmp* from a dict of rel_path -> content."""
|
||
|
|
for rel, content in files.items():
|
||
|
|
p = tmp / rel
|
||
|
|
p.parent.mkdir(parents=True, exist_ok=True)
|
||
|
|
p.write_text(content, encoding="utf-8")
|
||
|
|
|
||
|
|
|
||
|
|
def _run(root: Path, **kwargs) -> dict:
|
||
|
|
"""Run scanner on *root* with no git and frozen clock."""
|
||
|
|
return Scanner(
|
||
|
|
root=root,
|
||
|
|
git_log_fn=lambda p: [],
|
||
|
|
now_fn=lambda: 0.0,
|
||
|
|
**kwargs,
|
||
|
|
).run()
|
||
|
|
|
||
|
|
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
# 2.5a: hygiene: frozen file is excluded
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
class TestFrozenFrontmatter:
|
||
|
|
def test_frozen_file_absent_from_shortlist(self, tmp_path):
|
||
|
|
_make_tree(tmp_path, {
|
||
|
|
"frozen_doc.md": "---\nhygiene: frozen\n---\n\n# Frozen\nContent.",
|
||
|
|
"live_doc.md": "# Live\nThis is live content.",
|
||
|
|
})
|
||
|
|
result = _run(tmp_path)
|
||
|
|
assert "frozen_doc.md" not in result["shortlist"], (
|
||
|
|
"frozen_doc.md should be excluded by hygiene: frozen frontmatter"
|
||
|
|
)
|
||
|
|
|
||
|
|
def test_frozen_file_has_no_signals(self, tmp_path):
|
||
|
|
_make_tree(tmp_path, {
|
||
|
|
"frozen_doc.md": "---\nhygiene: frozen\n---\n\n# Frozen\nContent.",
|
||
|
|
})
|
||
|
|
result = _run(tmp_path)
|
||
|
|
assert "frozen_doc.md" not in result["signals"], (
|
||
|
|
"no signals should be computed for a frozen file"
|
||
|
|
)
|
||
|
|
|
||
|
|
def test_non_frozen_file_present_in_shortlist(self, tmp_path):
|
||
|
|
_make_tree(tmp_path, {
|
||
|
|
"frozen_doc.md": "---\nhygiene: frozen\n---\n\n# Frozen",
|
||
|
|
"live_doc.md": "# Live\nNormal content.",
|
||
|
|
})
|
||
|
|
result = _run(tmp_path)
|
||
|
|
assert "live_doc.md" in result["shortlist"], (
|
||
|
|
"live_doc.md should be shortlisted"
|
||
|
|
)
|
||
|
|
|
||
|
|
def test_frozen_only_checked_at_frontmatter_not_content(self, tmp_path):
|
||
|
|
"""A file mentioning 'frozen' in body but not frontmatter is NOT excluded."""
|
||
|
|
_make_tree(tmp_path, {
|
||
|
|
"doc.md": "# Not frozen\nThis doc mentions hygiene: frozen in the body.",
|
||
|
|
})
|
||
|
|
result = _run(tmp_path)
|
||
|
|
assert "doc.md" in result["shortlist"]
|
||
|
|
|
||
|
|
def test_fixture_frozen_doc(self):
|
||
|
|
"""Smoke: use the checked-in fixture tree."""
|
||
|
|
fixture_root = Path(__file__).parent / "fixtures" / "scanner" / "frozen"
|
||
|
|
if not fixture_root.exists():
|
||
|
|
return # fixture not present, skip
|
||
|
|
result = _run(fixture_root)
|
||
|
|
assert "frozen_doc.md" not in result["shortlist"]
|
||
|
|
assert "live_doc.md" in result["shortlist"]
|
||
|
|
|
||
|
|
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
# 2.5b: .dochygiene-ignore match is excluded
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
class TestIgnorePatterns:
|
||
|
|
def test_ignored_file_absent_from_shortlist(self, tmp_path):
|
||
|
|
_make_tree(tmp_path, {
|
||
|
|
".dochygiene-ignore": "ignored_doc.md\n",
|
||
|
|
"ignored_doc.md": "# Ignored\nThis should be excluded.",
|
||
|
|
"normal_doc.md": "# Normal\nThis should be shortlisted.",
|
||
|
|
})
|
||
|
|
result = _run(tmp_path)
|
||
|
|
assert "ignored_doc.md" not in result["shortlist"]
|
||
|
|
|
||
|
|
def test_ignored_file_has_no_signals(self, tmp_path):
|
||
|
|
_make_tree(tmp_path, {
|
||
|
|
".dochygiene-ignore": "ignored_doc.md\n",
|
||
|
|
"ignored_doc.md": "# Ignored",
|
||
|
|
})
|
||
|
|
result = _run(tmp_path)
|
||
|
|
assert "ignored_doc.md" not in result["signals"]
|
||
|
|
|
||
|
|
def test_non_ignored_file_present(self, tmp_path):
|
||
|
|
_make_tree(tmp_path, {
|
||
|
|
".dochygiene-ignore": "ignored_doc.md\n",
|
||
|
|
"normal_doc.md": "# Normal",
|
||
|
|
"ignored_doc.md": "# Ignored",
|
||
|
|
})
|
||
|
|
result = _run(tmp_path)
|
||
|
|
assert "normal_doc.md" in result["shortlist"]
|
||
|
|
|
||
|
|
def test_glob_pattern_in_ignore(self, tmp_path):
|
||
|
|
_make_tree(tmp_path, {
|
||
|
|
".dochygiene-ignore": "temp_*.md\n",
|
||
|
|
"temp_notes.md": "# Temp",
|
||
|
|
"design.md": "# Design",
|
||
|
|
})
|
||
|
|
result = _run(tmp_path)
|
||
|
|
assert "temp_notes.md" not in result["shortlist"]
|
||
|
|
assert "design.md" in result["shortlist"]
|
||
|
|
|
||
|
|
def test_comment_lines_in_ignore_file_skipped(self, tmp_path):
|
||
|
|
_make_tree(tmp_path, {
|
||
|
|
".dochygiene-ignore": "# This is a comment\nnotes.md\n",
|
||
|
|
"notes.md": "# Notes",
|
||
|
|
"design.md": "# Design",
|
||
|
|
})
|
||
|
|
result = _run(tmp_path)
|
||
|
|
assert "notes.md" not in result["shortlist"]
|
||
|
|
assert "design.md" in result["shortlist"]
|
||
|
|
|
||
|
|
def test_no_ignore_file_means_nothing_excluded(self, tmp_path):
|
||
|
|
_make_tree(tmp_path, {"doc.md": "# Doc"})
|
||
|
|
result = _run(tmp_path)
|
||
|
|
assert "doc.md" in result["shortlist"]
|
||
|
|
|
||
|
|
def test_fixture_ignored_doc(self):
|
||
|
|
"""Smoke: use the checked-in fixture tree with a live .dochygiene-ignore."""
|
||
|
|
fixture_root = Path(__file__).parent / "fixtures" / "scanner" / "ignored"
|
||
|
|
if not fixture_root.exists():
|
||
|
|
return
|
||
|
|
# Write a .dochygiene-ignore into a temp copy so we don't modify the fixture dir
|
||
|
|
import shutil, tempfile
|
||
|
|
with tempfile.TemporaryDirectory() as tmp:
|
||
|
|
tmp_path = Path(tmp)
|
||
|
|
shutil.copytree(fixture_root, tmp_path / "tree")
|
||
|
|
tree = tmp_path / "tree"
|
||
|
|
(tree / ".dochygiene-ignore").write_text("ignored_doc.md\n")
|
||
|
|
result = _run(tree)
|
||
|
|
assert "ignored_doc.md" not in result["shortlist"]
|
||
|
|
assert "normal_doc.md" in result["shortlist"]
|
||
|
|
|
||
|
|
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
# 2.5c: append-only log is excluded (positive + negative fixture)
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
class TestAppendOnlyExclusion:
|
||
|
|
def test_append_only_explicit_marker_excluded(self, tmp_path):
|
||
|
|
_make_tree(tmp_path, {
|
||
|
|
"changelog.md": "<!-- append-only -->\n\n# Changelog\n\n- Entry 1\n",
|
||
|
|
"normal.md": "# Normal doc\nContent here.",
|
||
|
|
})
|
||
|
|
result = _run(tmp_path)
|
||
|
|
assert "changelog.md" not in result["shortlist"], (
|
||
|
|
"file with explicit append-only marker should be excluded"
|
||
|
|
)
|
||
|
|
|
||
|
|
def test_append_only_explicit_marker_no_signals(self, tmp_path):
|
||
|
|
_make_tree(tmp_path, {
|
||
|
|
"changelog.md": "<!-- append-only -->\n# Log\n- Item\n",
|
||
|
|
})
|
||
|
|
result = _run(tmp_path)
|
||
|
|
assert "changelog.md" not in result["signals"]
|
||
|
|
|
||
|
|
def test_dated_sections_changelog_excluded(self, tmp_path):
|
||
|
|
"""Positive fixture: multi-entry dated changelog is append-only."""
|
||
|
|
content = (
|
||
|
|
"# Changelog\n\n"
|
||
|
|
"## 2026-06-18\n\n- Feature added\n\n"
|
||
|
|
"## 2026-06-17\n\n- Bug fixed\n\n"
|
||
|
|
"## 2026-06-16\n\n- Project started\n"
|
||
|
|
)
|
||
|
|
_make_tree(tmp_path, {"CHANGELOG.md": content})
|
||
|
|
result = _run(tmp_path)
|
||
|
|
assert "CHANGELOG.md" not in result["shortlist"], (
|
||
|
|
"multi-entry dated changelog should be detected as append-only"
|
||
|
|
)
|
||
|
|
|
||
|
|
def test_regular_doc_not_excluded(self, tmp_path):
|
||
|
|
"""Negative fixture: a regular architecture doc is NOT append-only."""
|
||
|
|
content = (
|
||
|
|
"# Architecture Overview\n\n"
|
||
|
|
"## Components\n\nThe system has three components.\n\n"
|
||
|
|
"## Rationale\n\nWe chose this design for clarity.\n\n"
|
||
|
|
"## Trade-offs\n\nThe main trade-off is simplicity vs flexibility.\n"
|
||
|
|
)
|
||
|
|
_make_tree(tmp_path, {"ARCHITECTURE.md": content})
|
||
|
|
result = _run(tmp_path)
|
||
|
|
assert "ARCHITECTURE.md" in result["shortlist"], (
|
||
|
|
"regular architecture doc (negative fixture) should NOT be excluded"
|
||
|
|
)
|
||
|
|
|
||
|
|
def test_fixture_positive_changelog(self):
|
||
|
|
"""Smoke: checked-in changelog fixture is excluded."""
|
||
|
|
p = Path(__file__).parent / "fixtures" / "scanner" / "append_only" / "changelog_positive.md"
|
||
|
|
if not p.exists():
|
||
|
|
return
|
||
|
|
import tempfile
|
||
|
|
with tempfile.TemporaryDirectory() as tmp:
|
||
|
|
tmp_path = Path(tmp)
|
||
|
|
shutil.copy(p, tmp_path / p.name)
|
||
|
|
result = _run(tmp_path)
|
||
|
|
assert p.name not in result["shortlist"]
|
||
|
|
|
||
|
|
def test_fixture_negative_regular_doc(self):
|
||
|
|
"""Smoke: checked-in regular doc fixture IS shortlisted."""
|
||
|
|
p = Path(__file__).parent / "fixtures" / "scanner" / "append_only" / "regular_doc_negative.md"
|
||
|
|
if not p.exists():
|
||
|
|
return
|
||
|
|
import tempfile
|
||
|
|
with tempfile.TemporaryDirectory() as tmp:
|
||
|
|
tmp_path = Path(tmp)
|
||
|
|
shutil.copy(p, tmp_path / p.name)
|
||
|
|
result = _run(tmp_path)
|
||
|
|
assert p.name in result["shortlist"]
|
||
|
|
|
||
|
|
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
# 2.5d: excluded dirs are never descended
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
class TestExcludedDirs:
|
||
|
|
def test_default_excluded_dirs_not_scanned(self, tmp_path):
|
||
|
|
# Bare directory-name excludes (matched at any depth). "golden" is NOT
|
||
|
|
# here — it is a path-aware "examples/golden" pair, covered separately.
|
||
|
|
bare_excludes = ["build", "vendor", "archive", "graphify-out", "fixtures"]
|
||
|
|
for excluded_dir in bare_excludes:
|
||
|
|
_make_tree(tmp_path, {
|
||
|
|
f"{excluded_dir}/secret.md": f"# In {excluded_dir}\nShould not be scanned.",
|
||
|
|
})
|
||
|
|
_make_tree(tmp_path, {"live.md": "# Live"})
|
||
|
|
result = _run(tmp_path)
|
||
|
|
for excluded_dir in bare_excludes:
|
||
|
|
for path in result["shortlist"]:
|
||
|
|
assert not path.startswith(f"{excluded_dir}/"), (
|
||
|
|
f"file under {excluded_dir}/ should not appear in shortlist"
|
||
|
|
)
|
||
|
|
|
||
|
|
def test_dochygiene_self_excluded_always(self, tmp_path):
|
||
|
|
"""State dir is always self-excluded regardless of configuration."""
|
||
|
|
_make_tree(tmp_path, {
|
||
|
|
".dochygiene/state.json": '{"last_check": null}',
|
||
|
|
".dochygiene/state_doc.md": "# This should never be scanned",
|
||
|
|
"real_doc.md": "# Real doc",
|
||
|
|
})
|
||
|
|
result = _run(tmp_path)
|
||
|
|
for path in result["shortlist"]:
|
||
|
|
assert not path.startswith(".dochygiene/"), (
|
||
|
|
".dochygiene/ contents should never appear in shortlist"
|
||
|
|
)
|
||
|
|
assert "real_doc.md" in result["shortlist"]
|
||
|
|
|
||
|
|
def test_dochygiene_excluded_even_if_not_in_config(self, tmp_path):
|
||
|
|
"""Even with an empty excluded_dirs config, .dochygiene/ is self-excluded."""
|
||
|
|
_make_tree(tmp_path, {
|
||
|
|
".dochygiene/report.md": "# Should be excluded",
|
||
|
|
"live.md": "# Live",
|
||
|
|
})
|
||
|
|
# Pass an empty excluded_dirs — .dochygiene/ must still be excluded
|
||
|
|
result = Scanner(
|
||
|
|
root=tmp_path,
|
||
|
|
excluded_dirs=[], # explicitly empty
|
||
|
|
git_log_fn=lambda p: [],
|
||
|
|
now_fn=lambda: 0.0,
|
||
|
|
).run()
|
||
|
|
for path in result["shortlist"]:
|
||
|
|
assert not path.startswith(".dochygiene/")
|
||
|
|
assert "live.md" in result["shortlist"]
|
||
|
|
|
||
|
|
def test_custom_excluded_dir_not_scanned(self, tmp_path):
|
||
|
|
_make_tree(tmp_path, {
|
||
|
|
"myexcluded/doc.md": "# Should not appear",
|
||
|
|
"live.md": "# Live",
|
||
|
|
})
|
||
|
|
result = Scanner(
|
||
|
|
root=tmp_path,
|
||
|
|
excluded_dirs=["myexcluded"],
|
||
|
|
git_log_fn=lambda p: [],
|
||
|
|
now_fn=lambda: 0.0,
|
||
|
|
).run()
|
||
|
|
for path in result["shortlist"]:
|
||
|
|
assert not path.startswith("myexcluded/")
|
||
|
|
assert "live.md" in result["shortlist"]
|
||
|
|
|
||
|
|
def test_excluded_dir_files_cost_no_files_scanned_count(self, tmp_path):
|
||
|
|
"""Files inside excluded dirs must not increment files_scanned."""
|
||
|
|
_make_tree(tmp_path, {
|
||
|
|
"build/output.md": "# Build output",
|
||
|
|
"live.md": "# Live",
|
||
|
|
})
|
||
|
|
result = _run(tmp_path)
|
||
|
|
# Only live.md should have been scanned (build/ is pruned before walk)
|
||
|
|
assert result["files_scanned"] == 1
|
||
|
|
|
||
|
|
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
# 2.5e: exclusion precedes signal computation
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
class TestExclusionPrecedesSignals:
|
||
|
|
def test_frozen_file_with_broken_link_has_no_signals(self, tmp_path):
|
||
|
|
"""Frozen file with a broken reference should produce NO signals."""
|
||
|
|
_make_tree(tmp_path, {
|
||
|
|
"frozen.md": (
|
||
|
|
"---\nhygiene: frozen\n---\n\n"
|
||
|
|
"# Frozen\n[missing](./nonexistent.md)\n"
|
||
|
|
),
|
||
|
|
})
|
||
|
|
result = _run(tmp_path)
|
||
|
|
assert "frozen.md" not in result["shortlist"]
|
||
|
|
assert "frozen.md" not in result["signals"]
|
||
|
|
|
||
|
|
def test_ignored_file_with_broken_link_has_no_signals(self, tmp_path):
|
||
|
|
_make_tree(tmp_path, {
|
||
|
|
".dochygiene-ignore": "ignored.md\n",
|
||
|
|
"ignored.md": "# Ignored\n[missing](./gone.md)\n",
|
||
|
|
})
|
||
|
|
result = _run(tmp_path)
|
||
|
|
assert "ignored.md" not in result["signals"]
|
||
|
|
|
||
|
|
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
# 2.5f: output shape contains no classifier fields
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
class TestOutputShape:
|
||
|
|
FORBIDDEN_FIELDS = {"entries", "category", "op", "op_type", "safety_tier",
|
||
|
|
"exact_edit", "token_estimate"}
|
||
|
|
|
||
|
|
def test_no_classifier_fields_in_output(self, tmp_path):
|
||
|
|
_make_tree(tmp_path, {"doc.md": "# Doc"})
|
||
|
|
result = _run(tmp_path)
|
||
|
|
for field in self.FORBIDDEN_FIELDS:
|
||
|
|
assert field not in result, f"forbidden field '{field}' found in scanner output"
|
||
|
|
|
||
|
|
def test_scan_metadata_fields_present(self, tmp_path):
|
||
|
|
_make_tree(tmp_path, {"doc.md": "# Doc"})
|
||
|
|
result = _run(tmp_path)
|
||
|
|
assert "project_root" in result
|
||
|
|
assert "scope_globs" in result
|
||
|
|
assert "excluded_dirs" in result
|
||
|
|
assert "files_scanned" in result
|
||
|
|
assert "shortlist" in result
|
||
|
|
assert "signals" in result
|
||
|
|
|
||
|
|
def test_project_root_is_absolute(self, tmp_path):
|
||
|
|
_make_tree(tmp_path, {"doc.md": "# Doc"})
|
||
|
|
result = _run(tmp_path)
|
||
|
|
assert os.path.isabs(result["project_root"])
|
||
|
|
|
||
|
|
def test_files_scanned_count_is_correct(self, tmp_path):
|
||
|
|
_make_tree(tmp_path, {
|
||
|
|
"a.md": "# A",
|
||
|
|
"b.md": "# B",
|
||
|
|
"c.md": "# C",
|
||
|
|
})
|
||
|
|
result = _run(tmp_path)
|
||
|
|
assert result["files_scanned"] == 3
|
||
|
|
|
||
|
|
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
# 2.5g: test-fixture and golden-example dirs are excluded by default
|
||
|
|
#
|
||
|
|
# These cover the real false-positive case: a project-level scan of a repo
|
||
|
|
# that contains test/golden input trees full of intentionally-stale docs.
|
||
|
|
#
|
||
|
|
# Two distinct semantics:
|
||
|
|
# * "fixtures" — bare directory-NAME match at any depth (broad).
|
||
|
|
# * "examples/golden" — PATH-AWARE: a 'golden' dir is pruned only when its
|
||
|
|
# immediate parent is 'examples' (depth-independent), so
|
||
|
|
# unrelated projects' 'golden/' dirs are NOT skipped.
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
class TestFixtureAndGoldenExclusion:
|
||
|
|
"""'fixtures' (bare name) and 'examples/golden' (path-aware) are default-excluded."""
|
||
|
|
|
||
|
|
def test_fixtures_subtree_not_scanned_by_default(self, tmp_path):
|
||
|
|
"""Files under any child dir named 'fixtures' are not scanned."""
|
||
|
|
_make_tree(tmp_path, {
|
||
|
|
"tests/fixtures/scanner/stale_fixture.md": "# Intentionally stale fixture",
|
||
|
|
"tests/fixtures/scanner/another.md": "# Another scanner fixture",
|
||
|
|
"live_doc.md": "# Real live doc",
|
||
|
|
})
|
||
|
|
result = _run(tmp_path)
|
||
|
|
for path in result["shortlist"]:
|
||
|
|
assert "fixtures/" not in path, (
|
||
|
|
f"file under fixtures/ sub-dir should not appear in shortlist, got: {path}"
|
||
|
|
)
|
||
|
|
assert "live_doc.md" in result["shortlist"], (
|
||
|
|
"live_doc.md at project root must still be shortlisted"
|
||
|
|
)
|
||
|
|
|
||
|
|
def test_examples_golden_subtree_not_scanned_by_default(self, tmp_path):
|
||
|
|
"""A 'golden' dir under an 'examples' parent is pruned (nested, monorepo-style)."""
|
||
|
|
_make_tree(tmp_path, {
|
||
|
|
# Mirrors the real monorepo path: examples/golden nested under a pkg dir.
|
||
|
|
"doc-hygiene/examples/golden/classifier/1-orphaned/input/docs/orphan.md": (
|
||
|
|
"# Orphaned\nThis deliberately-stale golden input must not be flagged."
|
||
|
|
),
|
||
|
|
"live_doc.md": "# Real live doc",
|
||
|
|
})
|
||
|
|
result = _run(tmp_path)
|
||
|
|
for path in result["shortlist"]:
|
||
|
|
assert "examples/golden/" not in path, (
|
||
|
|
f"file under examples/golden/ should not appear in shortlist, got: {path}"
|
||
|
|
)
|
||
|
|
assert "live_doc.md" in result["shortlist"]
|
||
|
|
|
||
|
|
def test_bare_golden_dir_without_examples_parent_is_scanned(self, tmp_path):
|
||
|
|
"""A 'golden/' dir NOT under an 'examples' parent is legitimate and IS scanned.
|
||
|
|
|
||
|
|
This is the false-negative the path-aware narrowing exists to prevent:
|
||
|
|
an unrelated project with its own top-level golden/ dir must not be skipped.
|
||
|
|
"""
|
||
|
|
_make_tree(tmp_path, {
|
||
|
|
"golden/master_record.md": "# Golden source of truth\nLegit project content.",
|
||
|
|
"data/golden/values.md": "# Golden values\nMore legit content.",
|
||
|
|
"live.md": "# Live",
|
||
|
|
})
|
||
|
|
result = _run(tmp_path)
|
||
|
|
assert "golden/master_record.md" in result["shortlist"], (
|
||
|
|
"a top-level golden/ dir (parent != examples) must be scanned"
|
||
|
|
)
|
||
|
|
assert "data/golden/values.md" in result["shortlist"], (
|
||
|
|
"a golden/ dir under 'data' (parent != examples) must be scanned"
|
||
|
|
)
|
||
|
|
|
||
|
|
def test_fixtures_files_cost_no_files_scanned_count(self, tmp_path):
|
||
|
|
"""Files inside fixtures/ dirs must not increment files_scanned (pruned at walk)."""
|
||
|
|
_make_tree(tmp_path, {
|
||
|
|
"tests/fixtures/scanner/input.md": "# fixture",
|
||
|
|
"live.md": "# Live",
|
||
|
|
})
|
||
|
|
result = _run(tmp_path)
|
||
|
|
# Only live.md should be scanned (fixtures/ is pruned before walk descends)
|
||
|
|
assert result["files_scanned"] == 1, (
|
||
|
|
f"expected 1 file scanned, got {result['files_scanned']}"
|
||
|
|
)
|
||
|
|
|
||
|
|
def test_examples_golden_files_cost_no_files_scanned_count(self, tmp_path):
|
||
|
|
"""Files inside examples/golden/ must not increment files_scanned (pruned at walk)."""
|
||
|
|
_make_tree(tmp_path, {
|
||
|
|
"examples/golden/1-case/input/docs/doc.md": "# golden input",
|
||
|
|
"live.md": "# Live",
|
||
|
|
})
|
||
|
|
result = _run(tmp_path)
|
||
|
|
assert result["files_scanned"] == 1, (
|
||
|
|
f"expected 1 file scanned, got {result['files_scanned']}"
|
||
|
|
)
|
||
|
|
|
||
|
|
def test_explicit_excluded_dirs_override_does_not_auto_add_fixtures(self, tmp_path):
|
||
|
|
"""When excluded_dirs is passed explicitly, fixtures/examples-golden are NOT
|
||
|
|
auto-added (they are defaults, not invariant-forced like .dochygiene/)."""
|
||
|
|
_make_tree(tmp_path, {
|
||
|
|
"fixtures/stale.md": "# stale fixture",
|
||
|
|
"examples/golden/g.md": "# golden",
|
||
|
|
"live.md": "# Live",
|
||
|
|
})
|
||
|
|
# Pass an explicit override list that does NOT include them
|
||
|
|
result = Scanner(
|
||
|
|
root=tmp_path,
|
||
|
|
excluded_dirs=["build"], # custom override — fixtures/golden not listed
|
||
|
|
git_log_fn=lambda p: [],
|
||
|
|
now_fn=lambda: 0.0,
|
||
|
|
).run()
|
||
|
|
# Both should now be scanned (override didn't include them)
|
||
|
|
assert "fixtures/stale.md" in result["shortlist"], (
|
||
|
|
"with explicit excluded_dirs override omitting 'fixtures', "
|
||
|
|
"fixtures/stale.md should be scanned"
|
||
|
|
)
|
||
|
|
assert "examples/golden/g.md" in result["shortlist"], (
|
||
|
|
"with explicit excluded_dirs override omitting 'examples/golden', "
|
||
|
|
"examples/golden/g.md should be scanned"
|
||
|
|
)
|
||
|
|
assert "live.md" in result["shortlist"]
|
||
|
|
|
||
|
|
def test_path_aware_pair_passable_as_explicit_override(self, tmp_path):
|
||
|
|
"""A 'parent/child' entry passed explicitly applies the path-aware match."""
|
||
|
|
_make_tree(tmp_path, {
|
||
|
|
"examples/golden/g.md": "# excluded",
|
||
|
|
"golden/top.md": "# scanned (no examples parent)",
|
||
|
|
"live.md": "# Live",
|
||
|
|
})
|
||
|
|
result = Scanner(
|
||
|
|
root=tmp_path,
|
||
|
|
excluded_dirs=["examples/golden"],
|
||
|
|
git_log_fn=lambda p: [],
|
||
|
|
now_fn=lambda: 0.0,
|
||
|
|
).run()
|
||
|
|
assert "examples/golden/g.md" not in result["shortlist"]
|
||
|
|
assert "golden/top.md" in result["shortlist"]
|
||
|
|
assert "live.md" in result["shortlist"]
|
||
|
|
|
||
|
|
def test_golden_harness_root_scan_is_unaffected(self, tmp_path):
|
||
|
|
"""The golden test harness points the scanner at input/ as the root.
|
||
|
|
|
||
|
|
os.walk never prunes the root itself (only child dirs), and the
|
||
|
|
examples→golden parent/child pair lives in the ANCESTRY above the walk
|
||
|
|
root — never re-encountered as a child below input/. So files under the
|
||
|
|
harness root are fully scanned even with the path-aware exclude active.
|
||
|
|
"""
|
||
|
|
# Simulate what test_classifier_golden.py does: root = .../1-orphaned/input/
|
||
|
|
input_root = tmp_path / "examples" / "golden" / "1-orphaned" / "input"
|
||
|
|
_make_tree(tmp_path, {
|
||
|
|
"examples/golden/1-orphaned/input/docs/orphan.md": (
|
||
|
|
"# Orphaned\n[missing](./gone.md)\n"
|
||
|
|
),
|
||
|
|
})
|
||
|
|
# Run scanner with root = input/ (exactly as the harness does)
|
||
|
|
result = Scanner(
|
||
|
|
root=input_root,
|
||
|
|
git_log_fn=lambda p: [],
|
||
|
|
now_fn=lambda: 0.0,
|
||
|
|
).run()
|
||
|
|
# The file must still be scanned (root is never pruned; the examples→golden
|
||
|
|
# pair is above the root, not a child below it).
|
||
|
|
assert "docs/orphan.md" in result["shortlist"], (
|
||
|
|
"golden harness: scanner rooted at input/ must still find docs/orphan.md"
|
||
|
|
)
|