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
22 changes: 18 additions & 4 deletions src/apm_cli/commands/deps/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,17 @@ def _is_absolute_local_path(path: str) -> bool:
return PurePosixPath(path).is_absolute() or PureWindowsPath(path).is_absolute()


def _absolute_local_display(path: str, fallback: str) -> str:
"""Return a portable logical name for an absolute local dependency."""
windows_path = PureWindowsPath(path)
parsed_path = (
windows_path
if windows_path.is_absolute() or path.startswith("~\\")
else PurePosixPath(path)
)
return f"_local/{parsed_path.name}" if parsed_path.name else fallback


def _logical_local_display(physical_name: str) -> str:
"""Strip the physical hash segment from an unmapped local slot.

Expand Down Expand Up @@ -108,6 +119,8 @@ def _dep_display_name(dep: LockedDependency) -> str:
if dep.source == "local":
if dep.local_path and not _is_absolute_local_path(dep.local_path):
key = dep.local_path
elif dep.local_path:
key = _absolute_local_display(dep.local_path, dep.repo_url)
else:
key = dep.repo_url
else:
Expand Down Expand Up @@ -344,18 +357,19 @@ def _resolve_scope_deps(apm_dir, logger, insecure_only=False):
# (``_local/<hash>/pkg``); report and orphan-check against the
# logical lockfile key (``_local/pkg``) instead of leaking the slot.
# A genuinely orphaned slot has no lockfile entry, so it stays keyed by
# its raw physical identity for correct detection -- but its displayed
# name is always the hash-free logical form.
# its raw physical identity for correct detection. Only confirmed
# orphans have their physical hash redacted; declared remote identities
# that happen to match the slot shape remain unchanged.
logical_name = physical_to_logical.get(org_repo_name, org_repo_name)
display_name = _logical_local_display(logical_name)
is_orphaned = logical_name not in declared_with_ancestors
display_name = _logical_local_display(logical_name) if is_orphaned else logical_name
try:
version = "unknown"
if has_apm_yml:
package = APMPackage.from_apm_yml(candidate / APM_YML_FILENAME)
version = package.version or "unknown"
primitives = _count_primitives(candidate)

is_orphaned = logical_name not in declared_with_ancestors
if is_orphaned:
orphaned_packages.append(display_name)

Expand Down
34 changes: 34 additions & 0 deletions tests/unit/commands/test_deps_cli_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
_resolve_scope_deps,
)
from apm_cli.constants import APM_MODULES_DIR, APM_YML_FILENAME, SKILL_MD_FILENAME
from apm_cli.deps.lockfile import LockedDependency, LockFile, get_lockfile_path
from apm_cli.models.dependency.reference import DependencyReference

# ---------------------------------------------------------------------------
# _format_primitive_counts
Expand Down Expand Up @@ -130,6 +132,22 @@ def test_local_dep_with_windows_absolute_path_renders_logical_repo_url(self):
assert "C:" not in result
assert "\\" not in result

def test_parsed_windows_absolute_path_renders_cross_platform_logical_name(self):
"""Display derives a safe name even when parsed on a non-Windows host."""
dep_ref = DependencyReference.parse(r"C:\Users\alice\pkg")
dep = LockedDependency.from_dependency_ref(
dep_ref,
resolved_commit=None,
depth=0,
resolved_by=None,
)

result = _dep_display_name(dep)

assert result == "_local/pkg@latest"
assert "C:" not in result
assert "\\" not in result

def test_local_dep_with_home_prefixed_path_renders_logical_repo_url(self):
"""A ``~``-prefixed declared path embeds home structure; render logical."""
dep = _make_local_dep(local_path="~/pkg", repo_url="_local/pkg")
Expand Down Expand Up @@ -260,6 +278,22 @@ def test_orphan_hashed_slot_is_orphaned_and_shows_no_hash(self, tmp_path):
for name in names + orphaned:
assert _HASH_SLOT not in name

def test_declared_remote_hash_shaped_identity_is_not_redacted(self, tmp_path):
remote_name = f"_local/{_HASH_SLOT}/pkg"
modules_dir = tmp_path / APM_MODULES_DIR
package_dir = modules_dir / remote_name
package_dir.mkdir(parents=True)
(package_dir / APM_YML_FILENAME).write_text("name: pkg\nversion: 1.0.0\n")

dep = LockedDependency(repo_url=remote_name, resolved_commit="a" * 40)
LockFile(dependencies={dep.get_unique_key(): dep}).write(get_lockfile_path(tmp_path))

installed, orphaned = _resolve_scope_deps(tmp_path, _make_logger())

assert installed is not None
assert [package["name"] for package in installed] == [remote_name]
assert orphaned == []


class TestResolveScopeDepsReadFailure:
"""A malformed package apm.yml makes ``APMPackage.from_apm_yml`` raise a
Expand Down
Loading