Fix headroom learn failing on project paths with underscores (#159)

_component_tokenizations only split on `-` and `.`, so directory names
like `my_project` could never be reconstructed from the dash-encoded
slug. Add `_` as a separator so the greedy decoder matches snake_case
directory names correctly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
chopratejas 2026-04-14 15:59:49 -07:00
parent 7a7b8b608f
commit 7c91fe1e4b
2 changed files with 68 additions and 4 deletions

View file

@ -375,9 +375,9 @@ def _component_tokenizations(component: str) -> list[list[str]]:
add([component])
for separator in ("-", ".", None):
for separator in ("-", ".", "_", None):
if separator is None:
tokens = [token for token in re.split(r"[-.]", component) if token]
tokens = [token for token in re.split(r"[-._]", component) if token]
else:
tokens = [token for token in component.split(separator) if token]
add(tokens)
@ -385,9 +385,9 @@ def _component_tokenizations(component: str) -> list[list[str]]:
if component.startswith(".") and len(component) > 1:
hidden_component = component[1:]
add(["", hidden_component])
for separator in ("-", ".", None):
for separator in ("-", ".", "_", None):
if separator is None:
tokens = [token for token in re.split(r"[-.]", hidden_component) if token]
tokens = [token for token in re.split(r"[-._]", hidden_component) if token]
else:
tokens = [token for token in hidden_component.split(separator) if token]
add(["", *tokens])

View file

@ -103,6 +103,38 @@ class TestGreedyPathDecode:
result = _greedy_path_decode(tmp_path, ["my", "cool", "project", "nosync", "headroom"])
assert result == tmp_path / "my-cool-project.nosync" / "headroom"
# ---- Underscore tests (issue #159) ----
def test_single_underscore_in_dirname(self, tmp_path: Path) -> None:
"""Directory name contains one literal underscore (e.g. my_project)."""
_make_dirs(tmp_path, "my_project")
result = _greedy_path_decode(tmp_path, ["my", "project"])
assert result == tmp_path / "my_project"
def test_multiple_underscores_in_dirname(self, tmp_path: Path) -> None:
"""Directory name contains multiple underscores (e.g. my_cool_project)."""
_make_dirs(tmp_path, "my_cool_project")
result = _greedy_path_decode(tmp_path, ["my", "cool", "project"])
assert result == tmp_path / "my_cool_project"
def test_underscore_nested_path(self, tmp_path: Path) -> None:
"""Nested path like org/my_project should decode correctly."""
_make_dirs(tmp_path, "org/my_project")
result = _greedy_path_decode(tmp_path, ["org", "my", "project"])
assert result == tmp_path / "org" / "my_project"
def test_mixed_underscore_and_hyphen_in_dirname(self, tmp_path: Path) -> None:
"""Directory with both hyphens and underscores (e.g. my-cool_project)."""
_make_dirs(tmp_path, "my-cool_project")
result = _greedy_path_decode(tmp_path, ["my", "cool", "project"])
assert result == tmp_path / "my-cool_project"
def test_underscore_dir_containing_hyphen_subdir(self, tmp_path: Path) -> None:
"""Path like my_app/sub-module — underscore parent + hyphen child."""
_make_dirs(tmp_path, "my_app/sub-module")
result = _greedy_path_decode(tmp_path, ["my", "app", "sub", "module"])
assert result == tmp_path / "my_app" / "sub-module"
def test_nonexistent_path_returns_none(self, tmp_path: Path) -> None:
result = _greedy_path_decode(tmp_path, ["does", "not", "exist"])
assert result is None
@ -236,6 +268,38 @@ class TestDecodeProjectPath:
else:
assert result is None or result == project
def test_underscore_dirname_via_greedy(self, users_tmp: Path) -> None:
"""my_project — underscore in directory name (issue #159).
Claude Code encodes /Users/foo/org/my_project as
-Users-foo-org-my-project. Simple replace gives
/org/my/project which does not exist, so the greedy decoder
must reconstruct my_project from tokens ['my', 'project'].
"""
project = users_tmp / "org" / "my_project"
project.mkdir(parents=True)
encoded = "-" + str(project)[1:].replace("/", "-")
result = _decode_project_path(encoded)
if str(users_tmp).startswith("/Users/"):
assert result == project
else:
assert result is None or result == project
def test_multi_underscore_dirname_via_greedy(self, users_tmp: Path) -> None:
"""my_cool_project — multiple underscores (issue #159)."""
project = users_tmp / "my_cool_project"
project.mkdir(parents=True)
encoded = "-" + str(project)[1:].replace("/", "-")
result = _decode_project_path(encoded)
if str(users_tmp).startswith("/Users/"):
assert result == project
else:
assert result is None or result == project
def test_windows_drive_letter_pattern(self) -> None:
"""Encoded name -C-MQ2-macros should detect Windows drive letter."""
import sys