From cc9ccb8df6ad37b36b1c40f7de0fa9d6083eed66 Mon Sep 17 00:00:00 2001 From: Copilot Date: Thu, 30 Apr 2026 23:51:50 +0200 Subject: [PATCH 1/2] test(windows): patch USERPROFILE so Path.home() honors fake HOME The CLAUDE_CONFIG_DIR scope tests added in #1055 patched only HOME, but Path.home() on Windows uses USERPROFILE (with HOMEDRIVE+HOMEPATH as fallback) and ignores HOME. As a result, Path.home() returned the real runner profile, relative_to(home) succeeded against the AppData/Local tmp_path, and the assertions on root_dir failed on windows-latest. Add a small _set_home() helper in both scope test modules that also sets USERPROFILE / HOMEDRIVE / HOMEPATH on Windows, and use it in the three failing tests: - test_user_scope_with_claude_config_dir - test_user_scope_outside_home_keeps_absolute - test_user_scope_collapses_dotdot_segments plus test_user_scope_expands_tilde for consistency. Also resolve() the expected outside path in test_user_scope_outside_home_keeps_absolute to match the source's abs_path.expanduser().resolve() output (e.g. /var -> /private/var on macOS, 8.3 short-name expansion on Windows). Fixes failures from CI run 25190857376. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../test_scope_install_uninstall.py | 20 +++++++++++++- .../integration/test_scope_integration.py | 27 ++++++++++++++++--- 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/tests/unit/integration/test_scope_install_uninstall.py b/tests/unit/integration/test_scope_install_uninstall.py index ba20667c2..5257c0875 100644 --- a/tests/unit/integration/test_scope_install_uninstall.py +++ b/tests/unit/integration/test_scope_install_uninstall.py @@ -7,6 +7,7 @@ - Files at wrong-scope paths are never created """ +import os import shutil import tempfile from datetime import datetime @@ -23,6 +24,23 @@ from apm_cli.models.dependency.types import GitReferenceType, ResolvedReference from apm_cli.models.validation import PackageType + +def _set_home(monkeypatch, home: Path) -> None: + """Portably set the user's home directory for ``Path.home()``. + + On Windows, ``Path.home()`` ignores ``HOME`` and uses ``USERPROFILE`` + (or ``HOMEDRIVE`` + ``HOMEPATH``). + """ + home_str = str(home) + monkeypatch.setenv("HOME", home_str) + if os.name == "nt": + monkeypatch.setenv("USERPROFILE", home_str) + drive, _, tail = home_str.partition(":") + if tail: + monkeypatch.setenv("HOMEDRIVE", f"{drive}:") + monkeypatch.setenv("HOMEPATH", tail) + + # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- @@ -380,7 +398,7 @@ def test_user_scope(self, monkeypatch): def test_user_scope_with_claude_config_dir(self, monkeypatch): """CLAUDE_CONFIG_DIR override: deploy lands at custom root and uninstall cleans it.""" - monkeypatch.setenv("HOME", str(self.project_root)) + _set_home(monkeypatch, self.project_root) custom = self.project_root / ".config" / "test-claude" monkeypatch.setenv("CLAUDE_CONFIG_DIR", str(custom)) custom.mkdir(parents=True) diff --git a/tests/unit/integration/test_scope_integration.py b/tests/unit/integration/test_scope_integration.py index b17461f70..90d11c84f 100644 --- a/tests/unit/integration/test_scope_integration.py +++ b/tests/unit/integration/test_scope_integration.py @@ -5,6 +5,7 @@ Uses real integrators against temp directories -- no mocks. """ +import os import shutil import tempfile from datetime import datetime @@ -21,6 +22,24 @@ from apm_cli.models.validation import PackageType +def _set_home(monkeypatch, home: Path) -> None: + """Set the user's home directory portably across POSIX and Windows. + + ``Path.home()`` consults ``HOME`` on POSIX but ``USERPROFILE`` (with + ``HOMEDRIVE`` + ``HOMEPATH`` fallback) on Windows. Setting only ``HOME`` + is a no-op on Windows and causes ``relative_to(Path.home())`` checks in + code under test to compare against the real user's profile. + """ + home_str = str(home) + monkeypatch.setenv("HOME", home_str) + if os.name == "nt": + monkeypatch.setenv("USERPROFILE", home_str) + drive, _, tail = home_str.partition(":") + if tail: + monkeypatch.setenv("HOMEDRIVE", f"{drive}:") + monkeypatch.setenv("HOMEPATH", tail) + + def _make_package_info(install_path, name="test-pkg"): """Create a minimal PackageInfo for testing.""" package = APMPackage( @@ -228,7 +247,7 @@ def test_all_primitives_available_at_user_scope(self): assert "agents" in resolved.primitives def test_user_scope_expands_tilde(self, tmp_path, monkeypatch): - monkeypatch.setenv("HOME", str(tmp_path)) + _set_home(monkeypatch, tmp_path) monkeypatch.setenv("CLAUDE_CONFIG_DIR", "~/.config/claude") scoped = KNOWN_TARGETS["claude"].for_scope(user_scope=True) assert scoped is not None @@ -243,19 +262,19 @@ def test_user_scope_blank_falls_back_to_default(self, monkeypatch): def test_user_scope_outside_home_keeps_absolute(self, tmp_path, monkeypatch): home = tmp_path / "home" outside = tmp_path / "elsewhere" - monkeypatch.setenv("HOME", str(home)) + _set_home(monkeypatch, home) monkeypatch.setenv("CLAUDE_CONFIG_DIR", str(outside)) scoped = KNOWN_TARGETS["claude"].for_scope(user_scope=True) assert scoped is not None # Paths outside $HOME are not normalized; preserve the absolute string. - assert scoped.root_dir == str(outside) + assert scoped.root_dir == str(outside.resolve()) def test_user_scope_collapses_dotdot_segments(self, tmp_path, monkeypatch): # ``..`` must be resolved before relative_to(home) so traversal # cannot leak into root_dir and later escape project_root / root_dir. home = tmp_path / "home" home.mkdir() - monkeypatch.setenv("HOME", str(home)) + _set_home(monkeypatch, home) monkeypatch.setenv("CLAUDE_CONFIG_DIR", str(home / ".." / "outside")) scoped = KNOWN_TARGETS["claude"].for_scope(user_scope=True) assert scoped is not None From 82cebf3e769a81db93d3d4f9c3b795699f58cca5 Mon Sep 17 00:00:00 2001 From: Copilot Date: Fri, 1 May 2026 00:28:19 +0200 Subject: [PATCH 2/2] test: address review nits on outside-home assertion - Update comment to reflect that paths outside $HOME are resolved (normalized), not preserved verbatim. - Make the .resolve() call explicit with strict=False to mirror the implementation in for_scope() and document the expected behavior for non-existent paths. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/unit/integration/test_scope_integration.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/integration/test_scope_integration.py b/tests/unit/integration/test_scope_integration.py index 90d11c84f..9e8c17362 100644 --- a/tests/unit/integration/test_scope_integration.py +++ b/tests/unit/integration/test_scope_integration.py @@ -266,8 +266,8 @@ def test_user_scope_outside_home_keeps_absolute(self, tmp_path, monkeypatch): monkeypatch.setenv("CLAUDE_CONFIG_DIR", str(outside)) scoped = KNOWN_TARGETS["claude"].for_scope(user_scope=True) assert scoped is not None - # Paths outside $HOME are not normalized; preserve the absolute string. - assert scoped.root_dir == str(outside.resolve()) + # Paths outside $HOME remain absolute and are resolved/normalized. + assert scoped.root_dir == str(outside.resolve(strict=False)) def test_user_scope_collapses_dotdot_segments(self, tmp_path, monkeypatch): # ``..`` must be resolved before relative_to(home) so traversal