Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions loopx/contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -750,16 +750,22 @@ def iter_scan_files(scan_root: Path) -> list[Path]:
files: list[Path] = []
tracked_files = _tracked_scan_files(scan_root)
root_parts = set(scan_root.parts)
if any(part in DEFAULT_SKIP_DIRS or part.endswith(".egg-info") for part in root_parts):
# Dependency pruning never overrides tracked repository ownership.
if (
any(part in DEFAULT_SKIP_DIRS or part.endswith(".egg-info") for part in root_parts)
or os.path.isfile(scan_root / "pyvenv.cfg")
):
return sorted(tracked_files)

for dir_path, dir_names, file_names in os.walk(scan_root):
current_dir = Path(dir_path)
dir_names[:] = [
name
for name in dir_names
if name not in DEFAULT_SKIP_DIRS and not name.endswith(".egg-info")
if name not in DEFAULT_SKIP_DIRS
and not name.endswith(".egg-info")
and not os.path.isfile(current_dir / name / "pyvenv.cfg")
]
current_dir = Path(dir_path)
for file_name in file_names:
path = (current_dir / file_name).resolve()
if path.name.endswith(".local.json"):
Expand Down
29 changes: 29 additions & 0 deletions tests/test_contract_scan_unreadable_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,32 @@ def test_scan_public_boundary_reports_unreadable_file(tmp_path: Path) -> None:

assert payload["ok"] is True
assert payload["unreadable_files"] == ["locked.md: Permission denied"]


def test_virtualenv_pruning_preserves_tracked_and_explicit_scan_targets(tmp_path: Path) -> None:
repo = tmp_path / "repo"
env = repo / "custom-python"
env.mkdir(parents=True)
(env / "pyvenv.cfg").write_text("include-system-site-packages = false\n")
payload = 'AUTH = "' + "tok" + 'en=syntheticsyntheticsynthetic"\n'
dependency = env / "dependency.py"
owned = env / "owned.py"
unmarked = repo / "unmarked" / "dependency.py"
unmarked.parent.mkdir()
for path in (dependency, owned, unmarked):
path.write_text(payload)
subprocess.run(["git", "init", "-q", str(repo)], check=True, capture_output=True)
subprocess.run(["git", "-C", str(repo), "add", "custom-python/owned.py"], check=True)

# Marked dependencies are omitted; owned content cannot hide behind a marker.
assert iter_scan_files(repo) == sorted([owned, unmarked])
assert iter_scan_files(env) == [owned]
result = scan_public_boundary([repo])
assert result["scanned_files"] == 2
assert result["ok"] is False
assert any("custom-python/owned.py" in hit for hit in result["hits"])
# An explicit file remains an explicit request even inside a pruned tree.
assert iter_scan_files(dependency) == [dependency]
assert scan_public_boundary([dependency])["hits"]
(env / "pyvenv.cfg").unlink()
assert dependency in iter_scan_files(repo)