diff --git a/desloppify/base/discovery/source.py b/desloppify/base/discovery/source.py index 25e2ce93..d6da5f74 100644 --- a/desloppify/base/discovery/source.py +++ b/desloppify/base/discovery/source.py @@ -149,6 +149,13 @@ def collect_exclude_dirs( if "*" not in pat: patterns.add(pat) patterns.update(p for p in resolved_exclusions if p and "*" not in p) + # External tools (e.g. bandit) match --exclude against absolute walked paths, + # so a relative scan_root — commonpath is often Path(".") — would make every + # exclude silently match nothing. Anchor relative roots to an absolute path + # (matching the .resolve() bandit applies to its own scan target); leave + # already-absolute roots byte-stable so callers' paths are unchanged. + if not scan_root.is_absolute(): + scan_root = scan_root.resolve() return [str(scan_root / p) for p in sorted(patterns) if p] diff --git a/desloppify/tests/detectors/test_external_adapters.py b/desloppify/tests/detectors/test_external_adapters.py index eb5aec35..cd0c44f8 100644 --- a/desloppify/tests/detectors/test_external_adapters.py +++ b/desloppify/tests/detectors/test_external_adapters.py @@ -1010,3 +1010,20 @@ def test_deduplicates(self, tmp_path): result = collect_exclude_dirs(tmp_path) node_entries = [p for p in result if p.endswith("/node_modules")] assert len(node_entries) == 1 + + def test_relative_scan_root_yields_absolute_paths(self): + """A relative scan_root must still yield absolute exclude dirs. + + ``scan_root_from_files`` derives the root from ``os.path.commonpath``, + which is relative (often ``Path('.')``) when scanning with a relative + ``--path``. bandit matches ``--exclude`` against *absolute* walked paths, + so a relative entry like ``.worktrees`` silently excludes nothing — the + bug behind ~2000 phantom B101 findings from worktree copies. + """ + with patch( + "desloppify.base.discovery.source.get_exclusions", + return_value=(".worktrees",), + ): + result = collect_exclude_dirs(Path(".")) + assert all(Path(p).is_absolute() for p in result), result + assert any(p.endswith("/.worktrees") for p in result), result diff --git a/desloppify/tests/lang/common/test_bash_unused_imports.py b/desloppify/tests/lang/common/test_bash_unused_imports.py index c74dee94..d41cba21 100644 --- a/desloppify/tests/lang/common/test_bash_unused_imports.py +++ b/desloppify/tests/lang/common/test_bash_unused_imports.py @@ -4,6 +4,14 @@ import textwrap +import pytest + +from desloppify.languages._framework.treesitter import is_available + +pytestmark = pytest.mark.skipif( + not is_available(), reason="tree-sitter-language-pack not installed" +) + def _detect(tmp_path, contents: str): from desloppify.languages._framework.treesitter.analysis.unused_imports import ( diff --git a/desloppify/tests/review/review_commands_cases.py b/desloppify/tests/review/review_commands_cases.py index 0158e4fd..2b9c0c2e 100644 --- a/desloppify/tests/review/review_commands_cases.py +++ b/desloppify/tests/review/review_commands_cases.py @@ -873,7 +873,10 @@ def test_do_run_batches_dry_run_generates_packet_and_prompts( assert len(packet_files) == 1 blind_packet = tmp_path / ".desloppify" / "review_packet_blind.json" assert blind_packet.exists() - prompt_files = list(runs_dir.glob("*/prompts/batch-*.md")) + # Sort: prompt files are named batch-1.md, batch-2.md; glob order is + # filesystem-dependent (differs Linux vs macOS) and the assertions + # below assume batch-1 (high_level_elegance, with historical_focus) first. + prompt_files = sorted(runs_dir.glob("*/prompts/batch-*.md")) assert len(prompt_files) == 2 prompt_text = prompt_files[0].read_text() assert "Blind packet:" in prompt_text