Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion tests/unit/integration/test_scope_install_uninstall.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- Files at wrong-scope paths are never created
"""

import os
import shutil
import tempfile
from datetime import datetime
Expand All @@ -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
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -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)
Expand Down
29 changes: 24 additions & 5 deletions tests/unit/integration/test_scope_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
Uses real integrators against temp directories -- no mocks.
"""

import os
import shutil
import tempfile
from datetime import datetime
Expand All @@ -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)
Comment on lines +25 to +40
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The _set_home() helper is duplicated across two test modules with the same logic. To avoid drift and keep behavior consistent, consider moving this into a shared test utility (e.g., tests/unit/conftest.py fixture or a tests/unit/utils.py helper) and importing/using it from both files.

Copilot uses AI. Check for mistakes.


def _make_package_info(install_path, name="test-pkg"):
"""Create a minimal PackageInfo for testing."""
package = APMPackage(
Expand Down Expand Up @@ -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
Expand All @@ -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)
# 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
# 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
Expand Down
Loading