Spaces:
Build error
Build error
File size: 11,748 Bytes
37c815b 6f3bc03 37c815b 2d19f32 37c815b 6f3bc03 37c815b 7984228 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 | """Tests for _decode_project_path and _greedy_path_decode (issue #47).
Directory names that contain dots (e.g. ``GitHub.nosync``) or multiple
hyphens (e.g. ``my-cool-project``) were silently dropped because
_greedy_path_decode only tried joining two consecutive tokens with a hyphen,
making it impossible to reconstruct names formed from three or more tokens.
"""
from __future__ import annotations
from pathlib import Path
from uuid import uuid4
import pytest
from headroom.learn.scanner import _decode_project_path, _greedy_path_decode
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _make_dirs(base: Path, *rel_paths: str) -> None:
"""Create one or more relative directory paths under *base*."""
for rel in rel_paths:
(base / rel).mkdir(parents=True, exist_ok=True)
# ---------------------------------------------------------------------------
# _greedy_path_decode
# ---------------------------------------------------------------------------
class TestGreedyPathDecode:
"""Unit tests for _greedy_path_decode."""
def test_simple_directory(self, tmp_path: Path) -> None:
_make_dirs(tmp_path, "headroom")
result = _greedy_path_decode(tmp_path, ["headroom"])
assert result == tmp_path / "headroom"
def test_single_hyphen_in_dirname(self, tmp_path: Path) -> None:
"""Directory name contains one literal hyphen."""
_make_dirs(tmp_path, "my-project")
result = _greedy_path_decode(tmp_path, ["my", "project"])
assert result == tmp_path / "my-project"
def test_multiple_hyphens_in_dirname(self, tmp_path: Path) -> None:
"""Directory name contains multiple literal hyphens (the regression case)."""
_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_dot_only_in_dirname(self, tmp_path: Path) -> None:
"""Directory name contains a dot but no hyphen (e.g. GitHub.nosync)."""
_make_dirs(tmp_path, "GitHub.nosync")
result = _greedy_path_decode(tmp_path, ["GitHub.nosync"])
assert result == tmp_path / "GitHub.nosync"
def test_dot_and_single_hyphen_in_dirname(self, tmp_path: Path) -> None:
"""Directory name has both a dot and a single hyphen (e.g. my-project.nosync)."""
_make_dirs(tmp_path, "my-project.nosync")
result = _greedy_path_decode(tmp_path, ["my", "project.nosync"])
assert result == tmp_path / "my-project.nosync"
def test_dot_and_multiple_hyphens_in_dirname(self, tmp_path: Path) -> None:
"""Directory name has a dot and multiple hyphens (e.g. my-cool-project.nosync).
This was the primary regression: the old code only joined pairs, so it
could never reconstruct a three-token hyphenated name.
"""
_make_dirs(tmp_path, "my-cool-project.nosync")
result = _greedy_path_decode(tmp_path, ["my", "cool", "project.nosync"])
assert result == tmp_path / "my-cool-project.nosync"
def test_dot_dir_containing_hyphenated_subdir(self, tmp_path: Path) -> None:
"""Path like GitHub.nosync/my-project β dot parent + hyphen child."""
_make_dirs(tmp_path, "GitHub.nosync/my-project")
result = _greedy_path_decode(tmp_path, ["GitHub.nosync", "my", "project"])
assert result == tmp_path / "GitHub.nosync" / "my-project"
def test_dot_dir_with_multi_hyphen_subdir(self, tmp_path: Path) -> None:
"""Path like GitHub.nosync/my-cool-app β dot parent + multi-hyphen child."""
_make_dirs(tmp_path, "GitHub.nosync/my-cool-app")
result = _greedy_path_decode(tmp_path, ["GitHub.nosync", "my", "cool", "app"])
assert result == tmp_path / "GitHub.nosync" / "my-cool-app"
def test_multi_hyphen_dot_dir_containing_subproject(self, tmp_path: Path) -> None:
"""Path like my-cool-project.nosync/headroom β hardest combination."""
_make_dirs(tmp_path, "my-cool-project.nosync/headroom")
result = _greedy_path_decode(tmp_path, ["my", "cool", "project.nosync", "headroom"])
assert result == tmp_path / "my-cool-project.nosync" / "headroom"
def test_dot_flattened_into_separate_tokens(self, tmp_path: Path) -> None:
"""Flattened encoding like GitHub-nosync should map back to GitHub.nosync."""
_make_dirs(tmp_path, "GitHub.nosync/thebest")
result = _greedy_path_decode(tmp_path, ["GitHub", "nosync", "thebest"])
assert result == tmp_path / "GitHub.nosync" / "thebest"
def test_hybrid_hyphen_and_dot_flattening(self, tmp_path: Path) -> None:
"""Flattened encoding should reconstruct mixed separators in one component."""
_make_dirs(tmp_path, "my-cool-project.nosync/headroom")
result = _greedy_path_decode(tmp_path, ["my", "cool", "project", "nosync", "headroom"])
assert result == tmp_path / "my-cool-project.nosync" / "headroom"
def test_nonexistent_path_returns_none(self, tmp_path: Path) -> None:
result = _greedy_path_decode(tmp_path, ["does", "not", "exist"])
assert result is None
def test_empty_parts_returns_base_when_exists(self, tmp_path: Path) -> None:
result = _greedy_path_decode(tmp_path, [])
assert result == tmp_path
def test_empty_parts_returns_none_when_not_exists(self) -> None:
result = _greedy_path_decode(Path("/nonexistent/path"), [])
assert result is None
# ---------------------------------------------------------------------------
# _decode_project_path
# ---------------------------------------------------------------------------
class TestDecodeProjectPath:
"""Integration-level tests for _decode_project_path.
Note: _decode_project_path's greedy branch only activates for paths whose
first component is ``Users`` (the common macOS home prefix). Tests that
exercise the greedy decoder therefore synthesise an encoded name rooted at
``/Users/<username>/β¦`` inside a real temporary directory created under
that prefix. When the temp directory does not exist under ``/Users`` the
tests fall back to ``/tmp`` and rely only on the fast simple-replace path.
"""
def test_returns_none_for_non_absolute_encoded_name(self) -> None:
assert _decode_project_path("Users-foo-bar") is None
def test_simple_replace_finds_dot_path(self, users_tmp: Path) -> None:
"""Simple replace-all works when no dir names contain hyphens.
The encoded name maps directly to the real path because every ``-`` is
a path separator; dots in directory names are preserved unchanged.
"""
project = users_tmp / "GitHub.nosync" / "headroom"
project.mkdir(parents=True)
# Build the encoded name exactly as Claude Code does (/ β -)
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
# ------------------------------------------------------------------
# Greedy-decoder tests β require a /Users-rooted path to activate.
# We try to create a temp dir under the real /Users tree; if that is
# not writable we skip rather than fail (CI typically runs as a real
# macOS user whose home IS under /Users).
# ------------------------------------------------------------------
@pytest.fixture()
def users_tmp(self, tmp_path: Path) -> Path:
"""Return a temporary directory whose path starts with /Users/β¦
On macOS the system temp dir is under /private/var, so we create a
disposable directory directly inside the real user's home instead.
Falls back to tmp_path so tests still run on non-macOS platforms
(where the greedy branch isn't reached but no crash occurs either).
"""
home = Path.home()
if str(home).startswith("/Users/"):
base = home / ".pytest_headroom_tmp"
try:
base.mkdir(exist_ok=True)
except PermissionError:
pytest.skip("Cannot create /Users-rooted temp dir in this environment")
# Use a sub-directory unique to this test invocation
unique = base / uuid4().hex
try:
unique.mkdir()
except PermissionError:
pytest.skip("Cannot create /Users-rooted temp dir in this environment")
yield unique
import shutil
shutil.rmtree(unique, ignore_errors=True)
else:
yield tmp_path
def test_dot_and_hyphen_in_dirname_via_greedy(self, users_tmp: Path) -> None:
"""GitHub.nosync/my-project β dot parent + hyphenated child (issue #47).
Simple replace-all gives ``β¦/GitHub.nosync/my/project`` which does not
exist, so the greedy decoder must reconstruct ``my-project``.
"""
project = users_tmp / "GitHub.nosync" / "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:
# Greedy branch not reached outside /Users; just confirm no crash
assert result is None or result == project
def test_multi_hyphen_dot_dirname_via_greedy(self, users_tmp: Path) -> None:
"""my-cool-project.nosync/app β primary regression from issue #47.
Three tokens joined by hyphens form the parent dir name; the old code
only tried pairs and therefore could never reconstruct this component.
"""
project = users_tmp / "my-cool-project.nosync" / "app"
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_flattened_dot_dirname_via_greedy(self, users_tmp: Path) -> None:
"""GitHub.nosync/thebest should decode from GitHub-nosync-thebest."""
project = users_tmp / "GitHub.nosync" / "thebest"
project.mkdir(parents=True)
encoded = "-" + str(project)[1:].replace("/", "-").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
result = _decode_project_path("-C-MQ2-macros")
if sys.platform == "win32":
# On Windows: tries C:\\MQ2\\macros, may or may not exist
assert result is None or str(result).startswith("C:")
else:
# On Unix: drive detection runs but path doesn't exist β falls through
# Then Unix paths tried β also don't exist β returns None
assert result is None
def test_windows_users_path(self) -> None:
"""Encoded name -C-Users-foo-project detects drive letter."""
import sys
result = _decode_project_path("-C-Users-foo-project")
if sys.platform == "win32":
assert result is None or "Users" in str(result)
else:
assert result is None
|