fix: skip unreachable reparse points in detect_rooms_from_folders (#558)

On Windows, projects containing git-submodule junctions or dev-drive
reparse points cause iterdir() to list the entry successfully but
Path.is_dir() to raise OSError when it calls stat() internally.

Reproducer: any Windows project with a submodule checked out as a
junction (e.g. skills/pr-perfect) crashes mempalace init with:
  OSError: [WinError 448] The path cannot be traversed because it
  contains an untrusted mount point

Fix: wrap every is_dir() call in detect_rooms_from_folders with
try/except OSError so the scanner skips inaccessible entries and
continues rather than aborting.

Covers both the top-level pass and the one-level-deep nested pass.
Two new tests mock the OSError on specific paths and verify the
function returns correct rooms from the remaining accessible entries.
This commit is contained in:
Ahmad Othman Ammar Adi.
2026-04-12 01:16:06 +02:00
committed by GitHub
parent 1056018b52
commit 9c4b7302cc
2 changed files with 107 additions and 4 deletions
+76
View File
@@ -59,6 +59,82 @@ def test_detect_rooms_from_folders_empty_dir(tmp_path):
assert any(r["name"] == "general" for r in rooms)
def test_detect_rooms_from_folders_skips_oserror_at_top_level(tmp_path):
"""Windows reparse points (junctions, untrusted mount points) raise OSError
when stat()'d. The scanner must skip them and continue — not crash.
Reproduces WinError 448: "The path cannot be traversed because it contains
an untrusted mount point", seen on Windows when a project folder contains
a git-submodule junction or a dev-drive reparse point.
"""
(tmp_path / "frontend").mkdir()
bad = tmp_path / "untrusted_junction"
bad.mkdir()
original_is_dir = bad.__class__.is_dir
def patched_is_dir(self):
if self == bad:
raise OSError(
"[WinError 448] The path cannot be traversed because it contains an untrusted mount point"
)
return original_is_dir(self)
with patch.object(bad.__class__, "is_dir", patched_is_dir):
rooms = detect_rooms_from_folders(str(tmp_path))
room_names = {r["name"] for r in rooms}
assert "frontend" in room_names
def test_detect_rooms_from_folders_skips_oserror_nested(tmp_path):
"""Same WinError 448 guard applies one level deeper (nested iterdir pass)."""
skills = tmp_path / "skills"
skills.mkdir()
(skills / "docs").mkdir()
bad = skills / "bad_junction"
bad.mkdir()
original_is_dir = bad.__class__.is_dir
def patched_is_dir(self):
if self == bad:
raise OSError(
"[WinError 448] The path cannot be traversed because it contains an untrusted mount point"
)
return original_is_dir(self)
with patch.object(bad.__class__, "is_dir", patched_is_dir):
rooms = detect_rooms_from_folders(str(tmp_path))
room_names = {r["name"] for r in rooms}
assert "documentation" in room_names
def test_detect_rooms_from_folders_skips_iterdir_oserror(tmp_path):
"""iterdir() itself can raise OSError on some Windows reparse points even
when is_dir() succeeds. The nested pass must guard the iterdir() call too."""
outer = tmp_path / "src"
outer.mkdir()
(tmp_path / "docs").mkdir()
original_iterdir = outer.__class__.iterdir
def patched_iterdir(self):
if self == outer:
raise OSError(
"[WinError 448] The path cannot be traversed because it contains an untrusted mount point"
)
return original_iterdir(self)
with patch.object(outer.__class__, "iterdir", patched_iterdir):
rooms = detect_rooms_from_folders(str(tmp_path))
room_names = {r["name"] for r in rooms}
# docs is accessible; src fails on iterdir — neither should crash
assert "documentation" in room_names
def test_detect_rooms_from_folders_skips_git(tmp_path):
(tmp_path / ".git").mkdir()
(tmp_path / "node_modules").mkdir()