Skip to content

Commit 3eddf36

Browse files
committed
fix(util): normalize uppercase WSL drive letters in normalize_path
normalize_path's WSL branch matched only [a-z], so /mnt/C/foo matched neither the WSL-conversion branch (regex miss) nor the Windows drive-lowercasing branch (s[1] != ':') and was returned fully unnormalized — wrong case and unconverted — violating the function's own documented contract. Under a session that emitted an uppercase /mnt/C path, this fragmented the session/cache key for the same physical file. Widen the regex to [a-zA-Z] and lowercase the captured drive so /mnt/C and /mnt/c collapse to one key.
1 parent 87d669e commit 3eddf36

2 files changed

Lines changed: 22 additions & 7 deletions

File tree

src/token_goat/util.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@ def get_logger(name: str) -> Logger:
4848

4949

5050
# Compiled once at import time — avoids recompiling on every normalize_path call.
51-
_WSL_PATH_RE = re.compile(r"^/mnt/([a-z])/(.*)$", re.DOTALL)
51+
# Accept either case for the drive letter ([a-zA-Z]); the captured group is lowercased
52+
# below so /mnt/C/foo and /mnt/c/foo collapse to the same canonical key.
53+
_WSL_PATH_RE = re.compile(r"^/mnt/([a-zA-Z])/(.*)$", re.DOTALL)
5254

5355

5456
def normalize_path(path: str | Path) -> str:
@@ -92,7 +94,7 @@ def normalize_path(path: str | Path) -> str:
9294
# Step 2: convert WSL /mnt/<single-letter-drive>/rest → <drive>:/rest
9395
m = _WSL_PATH_RE.match(s)
9496
if m:
95-
drive_letter = m.group(1) # already lowercase due to [a-z] in regex
97+
drive_letter = m.group(1).lower() # lowercase so /mnt/C and /mnt/c agree
9698
rest = m.group(2)
9799
s = f"{drive_letter}:/{rest}"
98100
else:

tests/test_wsl_path_normalize.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,24 @@ def test_wsl_path_with_subdirs(self) -> None:
2727
"""WSL path with nested subdirectories converts correctly."""
2828
assert normalize_path("/mnt/c/Users/zelys/project/src/foo.py") == "c:/Users/zelys/project/src/foo.py"
2929

30-
def test_wsl_uppercase_drive_in_regex_does_not_apply(self) -> None:
31-
"""The /mnt/<drive>/ pattern only matches lowercase single letters."""
32-
# /mnt/c/ → c:/ (lowercase drive letter preserved)
33-
result = normalize_path("/mnt/c/bar")
34-
assert result == "c:/bar"
30+
def test_wsl_uppercase_c_drive(self) -> None:
31+
"""WSL /mnt/C/... (uppercase drive) normalizes to c:/... — drive lowercased.
32+
33+
Regression: the pre-fix regex was ``[a-z]`` only, so an uppercase /mnt/C/
34+
path matched neither the WSL branch (regex miss) nor the Windows
35+
drive-lowercasing branch (s[1] != ':'), and was returned fully
36+
unnormalized, violating the documented WSL-conversion + lowercasing
37+
contract and fragmenting the session/cache key for the same physical file.
38+
"""
39+
assert normalize_path("/mnt/C/bar") == "c:/bar"
40+
41+
def test_wsl_uppercase_d_drive(self) -> None:
42+
"""WSL /mnt/D/... (uppercase non-C drive) normalizes to d:/..."""
43+
assert normalize_path("/mnt/D/workspace") == "d:/workspace"
44+
45+
def test_wsl_uppercase_and_lowercase_same_key(self) -> None:
46+
"""/mnt/C/foo/bar and /mnt/c/foo/bar collapse to one canonical key."""
47+
assert normalize_path("/mnt/C/foo/bar") == normalize_path("/mnt/c/foo/bar")
3548

3649
def test_wsl_d_drive(self) -> None:
3750
"""WSL /mnt/d/... converts to d:/..."""

0 commit comments

Comments
 (0)