Skip to content
Open
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed

- `apm install --dry-run` no longer lists the project's own `includes: auto`
self-managed files under "Files that would be removed"; the orphan preview
now excludes the synthesized lockfile self-entry, matching the real install
which never removes them. — by @mia106dev (#2069)

## [0.24.0] - 2026-07-05

### Added
Expand Down
9 changes: 9 additions & 0 deletions src/apm_cli/drift.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,10 +202,19 @@ def detect_orphans(
are no longer in the manifest. The caller is responsible for actually
removing the files.
"""
from apm_cli.deps.lockfile import _SELF_KEY

orphaned: builtins.set = builtins.set()
if only_packages or not existing_lockfile:
return orphaned
for dep_key, dep in existing_lockfile.dependencies.items():
# The synthesized self-entry (key ".") holds the project's own
# includes:auto content, which the real install always re-deploys
# (never removes). It can never appear in the manifest-derived
# intended set, so skip it to avoid over-counting self-includes as
# removals in the dry-run preview.
if dep_key == _SELF_KEY:
continue
if dep_key not in intended_dep_keys:
orphaned.update(dep.deployed_files)
return orphaned
Expand Down
49 changes: 49 additions & 0 deletions tests/unit/test_drift_detection.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,55 @@ def test_none_only_packages_treated_as_empty(self):
result = detect_orphans(lf, set(), only_packages=None)
self.assertEqual(result, {"a.md"})

def test_self_entry_never_orphaned(self):
"""The synthesized self-entry (key ".") must never be reported as orphan.

from_yaml() injects a virtual ``_SELF_KEY`` dependency carrying the
project's own includes:auto content. That key can never appear in the
manifest-derived intended set, and the real install re-deploys it
(drift.py replay guards ``local_path == _SELF_KEY``). Reporting it here
over-counts self-includes as removals in the dry-run preview.
"""
from apm_cli.deps.lockfile import _SELF_KEY

self_dep = _LockedDep(
repo_url="<self>",
source="local",
local_path=_SELF_KEY,
deployed_files=["self_a.md", "self_b.md"],
)
orphan = _LockedDep(repo_url="owner/gone", deployed_files=["gone.md"])
lf = _LockFile(dependencies={_SELF_KEY: self_dep, "owner/gone": orphan})
result = detect_orphans(lf, set(), only_packages=[])
# Only the genuinely dropped package is an orphan; self-includes are not.
self.assertEqual(result, {"gone.md"})

def test_self_include_survives_real_from_yaml_roundtrip(self):
"""End-to-end guard using the real LockFile.from_yaml synthesis.

The stub-based test above could drift from how from_yaml actually
synthesizes the self-entry. This exercises the real parse -> detect
path so a change to either side (self-key value, synthesis shape)
that reintroduces the dry-run false alarm fails here. from_yaml is a
pure string parse -- no I/O.
"""
from apm_cli.deps.lockfile import LockFile

lock = LockFile.from_yaml(
"lockfile_version: '2'\n"
"generated_at: ''\n"
"dependencies:\n"
" - repo_url: owner/kept\n"
" source: git\n"
" deployed_files: ['.apm/x.md']\n"
"local_deployed_files:\n"
" - .claude/skills/self/SKILL.md\n"
)
# Manifest now empty -> only the real dropped package is an orphan;
# the synthesized self-entry's includes are never reported.
result = detect_orphans(lock, set(), only_packages=[])
self.assertEqual(result, {".apm/x.md"})


# ---------------------------------------------------------------------------
# detect_config_drift
Expand Down
Loading