Skip to content
Merged
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- `apm install` (user scope): `init_link_resolver` now scopes `discover_primitives` to `~/.apm/` instead of `~/`, preventing recursive-glob across the entire home directory. Fixes #830 (#850)
- Audit blindness for local `.apm/` content -- `apm audit --ci` now detects drift, missing files, and content tampering on locally-authored files (not just installed packages). (#887)
- Packer leak risk: local-content fields (`local_deployed_files`, `local_deployed_file_hashes`) are now stripped from bundled lockfiles, preventing phantom self-entries on unpack. (#887)

Expand Down
8 changes: 7 additions & 1 deletion src/apm_cli/integration/base_integrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,13 @@ def init_link_resolver(self, package_info, project_root: Path) -> None:
"""Initialise and register the link resolver for a package."""
self.link_resolver = UnifiedLinkResolver(project_root)
try:
primitives = discover_primitives(package_info.install_path)
scan_root = package_info.install_path
# When install_path is $HOME (user-scope local package),
# only scan the .apm/ subdirectory to avoid recursive-
# globbing the entire home tree. See issue #830.
if scan_root == Path.home():
scan_root = scan_root / ".apm"
primitives = discover_primitives(scan_root)
self.link_resolver.register_contexts(primitives)
except Exception:
self.link_resolver = None
Expand Down
39 changes: 39 additions & 0 deletions tests/unit/integration/test_base_integrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from unittest.mock import MagicMock, patch

from apm_cli.integration.base_integrator import BaseIntegrator, IntegrationResult
from apm_cli.primitives.discovery import discover_primitives


# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -592,3 +593,41 @@ class TestShouldIntegrate:
def test_always_returns_true(self):
bi = BaseIntegrator()
assert bi.should_integrate(Path("/any/path")) is True


# ---------------------------------------------------------------------------
# init_link_resolver — home-directory scoping (#830)
# ---------------------------------------------------------------------------

class TestInitLinkResolverHomeScoping:
"""When install_path is $HOME, init_link_resolver must scope
discover_primitives to ~/.apm/ to avoid recursive-globbing the
entire home directory. See issue #830."""

@patch("apm_cli.integration.base_integrator.discover_primitives")
@patch("apm_cli.integration.base_integrator.UnifiedLinkResolver")
def test_scopes_to_apm_subdir_when_install_path_is_home(
self, mock_resolver_cls, mock_discover
):
mock_discover.return_value = []
bi = BaseIntegrator()
pkg_info = MagicMock()
pkg_info.install_path = Path.home()

bi.init_link_resolver(pkg_info, Path.home())

mock_discover.assert_called_once_with(Path.home() / ".apm")

@patch("apm_cli.integration.base_integrator.discover_primitives")
@patch("apm_cli.integration.base_integrator.UnifiedLinkResolver")
def test_uses_install_path_when_not_home(
self, mock_resolver_cls, mock_discover, tmp_path
):
mock_discover.return_value = []
bi = BaseIntegrator()
pkg_info = MagicMock()
pkg_info.install_path = tmp_path

bi.init_link_resolver(pkg_info, tmp_path)

mock_discover.assert_called_once_with(tmp_path)
18 changes: 17 additions & 1 deletion tests/unit/test_local_content_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,14 +174,30 @@ def test_skips_root_skill_md(self, mock_integrate, tmp_path):

@patch("apm_cli.install.services.integrate_package_primitives")
def test_package_info_install_path_is_project_root(self, mock_integrate, tmp_path):
"""The synthetic PackageInfo must point to project_root, not .apm/."""
"""The synthetic PackageInfo must point to project_root at project scope."""
mock_integrate.return_value = _zero_counters()

_integrate_local_content(tmp_path, **_make_integrators())

package_info = mock_integrate.call_args[0][0]
assert package_info.install_path == tmp_path

@patch("apm_cli.install.services.integrate_package_primitives")
def test_user_scope_install_path_stays_project_root(self, mock_integrate, tmp_path):
"""At user scope, install_path must remain project_root so that
integrators can still find <project_root>/.apm/<type>/.
The recursive-glob fix lives in init_link_resolver, not here.
Regression check for #830."""
from apm_cli.core.scope import InstallScope

mock_integrate.return_value = _zero_counters()
(tmp_path / ".apm").mkdir(exist_ok=True)

_integrate_local_content(tmp_path, **_make_integrators(), scope=InstallScope.USER)

package_info = mock_integrate.call_args[0][0]
assert package_info.install_path == tmp_path

Comment thread
guwenqing marked this conversation as resolved.
@patch("apm_cli.install.services.integrate_package_primitives")
def test_returns_zero_counters_when_nothing_deployed(self, mock_integrate, tmp_path):
"""When nothing is deployed the result counters are all zero."""
Expand Down
Loading