fix(deps): render logical local dep keys in deps list/tree; refresh stale v0.25.0 release tests#2173
Conversation
…tale v0.25.0 release tests The v0.25.0 release integration suite (run 29184076146) surfaced 13 failures across four independent root causes. Fix the one production regression and reground three stale test fixtures. Production (root cause #4): - `apm deps list` and `apm deps tree` leaked physical, parent-scoped install slots for transitive local deps (`_local/<12-char-hash>/pkg` and `local:/abs/path/pkg`) instead of the logical lockfile identity. PR #2155 intentionally hashes physical slots to avoid sibling collisions; that storage is kept. `_resolve_scope_deps` now builds a physical-slot -> logical-key map from the lockfile and reports/orphan- checks against the logical `repo_url` (`_local/pkg`). `_dep_display_name` renders a local dep's declared `local_path` (`../pkg`) rather than the anchored `local:/...` unique key. Physical hashed storage, remote behavior, and lockfile keys are unchanged. Test fixtures (root causes #1-#3, stale expectations, no source change): - test_dep_url_parsing_e2e: policy cascade first candidate is now `.github-private` (PR #2058); assertion + comments updated to `.github-private -> .github -> .apm -> _apm`. - test_pack_apm_provenance_e2e: deployed_files listed a skill dir only while hashing per-child files -- an impossible install shape. Make it production-realistic (directory entry + every contained file) in both the tamper and symlink-escape tests. Fail-closed verification unchanged. - test_plugin_e2e: the assembled context-engineering plugin now lives on upstream `marketplace` (default `main` is a thin stub with absent component paths). Point PLUGIN_REF at `...context-engineering#marketplace` and add a ref-stripped PLUGIN_PATH for on-disk assertions. - test_transitive_chain_e2e: prune assertion expected a flat `_local/pkg/apm.yml` for transitive deps; align it with the hashed-slot reality (glob) exactly as the sibling three-level chain test does. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a0678eed-0238-4a73-a540-a5a63948b1dc
There was a problem hiding this comment.
Pull request overview
Fixes a production UX regression where apm deps list / apm deps tree could surface physical on-disk local dependency slots (including hashed parent-scoped slots and, in the tree case, absolute local:/... paths) instead of the logical dependency identity the user declared / the lockfile represents. Also refreshes several integration tests that drifted behind intentional earlier behavior changes.
Changes:
- Map scanned local dependency install paths back to a logical lockfile identity in
_resolve_scope_deps, sodeps list/orphan detection keys on the logical name instead of the hashed slot. - Render local dependencies in
deps treeusing their declaredlocal_path(e.g.../pkg) to avoid leaking host absolute paths. - Update integration tests/fixtures to match current policy cascade behavior, pack provenance shapes, plugin ref selection, and hashed-slot materialization.
Show a summary per file
| File | Description |
|---|---|
src/apm_cli/commands/deps/cli.py |
Adjusts deps list/tree naming for local dependencies by mapping physical slots to logical identities and displaying declared local paths. |
tests/integration/test_dep_url_parsing_e2e.py |
Updates policy discovery expectation to .github-private first in the cascade. |
tests/integration/test_pack_apm_provenance_e2e.py |
Regrounds pack provenance fixtures to include directory + contained files in deployed_files. |
tests/integration/test_plugin_e2e.py |
Pins the upstream plugin install to #marketplace and fixes on-disk path assertions accordingly. |
tests/integration/test_transitive_chain_e2e.py |
Updates post-prune assertions to locate transitive local deps in hashed parent-scoped slots via globbing. |
Review details
- Files reviewed: 5/5 changed files
- Comments generated: 1
- Review effort level: Low
| if dep.source == "local": | ||
| install_path = dep.to_dependency_ref().get_install_path(apm_modules_path) | ||
| dep_key = install_path.relative_to(apm_modules_path).as_posix() | ||
| physical_key = install_path.relative_to(apm_modules_path).as_posix() | ||
| dep_key = dep.repo_url | ||
| physical_to_logical[physical_key] = dep_key |
APM Review Panel:
|
| Persona | B | R | N | Takeaway |
|---|---|---|---|---|
| Python Architect | 0 | 0 | 2 | The mapping is internally consistent and correctly scoped. |
| CLI Logging Expert | 0 | 0 | 1 | Logical identity is restored, but one warning still uses the physical slot. |
| DevX UX Expert | 0 | 0 | 2 | The mental model is restored; harden the old-lockfile fallback. |
| Supply Chain Security Expert | 0 | 0 | 0 | Hashed storage, lock identity, and fail-closed verification remain intact. |
| OSS Growth Hacker | 0 | 0 | 2 | Trust improves without requiring a conversion-surface change. |
| Test Coverage Expert | 0 | 1 | 1 | Integration coverage is strong; add focused branch and negative guards. |
| Performance Expert | 0 | 1 | 2 | Production complexity is unchanged; repeated network E2Es remain costly. |
B = blocking-severity findings, R = recommended, N = nits.
Counts are signal strength, not gates. The maintainer ships.
Top 5 follow-ups
- [Test Coverage Expert] Add unit tests for local display with and without
local_path-- this cheaply guards the fallback that prevents path leakage. - [CLI Logging Expert] Use
logical_namein the read-failure warning -- otherwise a failure can still expose_local/<hash>/pkg. - [DevX UX Expert] Use
local_pathorrepo_urlbeforeget_unique_key()for local deps -- this prevents old lock entries from exposinglocal:<absolute-path>. - [Test Coverage Expert] Assert that hash slots and absolute local identities are absent from CLI output -- positive assertions alone would permit duplicate physical output.
- [Performance Expert] Reduce repeated installs in the real-network plugin class -- retain one real lifecycle signal while addressing the pre-existing timeout risk separately.
Architecture
classDiagram
class LockedDependency {
+repo_url str
+source str
+local_path str
+get_unique_key() str
+to_dependency_ref() DependencyReference
}
class DependencyReference {
+get_install_path(Path) Path
}
class LockFile {
+dependencies dict
}
class DepsInventory {
+physical_to_logical dict
+resolve_scope_deps() list
+display_name() str
}
LockFile *-- LockedDependency
LockedDependency ..> DependencyReference
DepsInventory ..> LockFile
DepsInventory ..> DependencyReference
flowchart TD
CLI["apm deps list/tree"] --> LF["Read lockfile"]
LF --> MAP["Map physical local slot to logical repo_url"]
MAP --> SCAN["Scan apm_modules"]
SCAN --> VIEW["Render logical package identity"]
LF --> TREE["Render declared local_path in tree"]
Recommendation
The release recovery is sound. Fold the three bounded path-leak and test completions, let CI certify the updated head, then merge. Track the heavyweight network-test redesign separately.
Full per-persona findings
Python Architect
- [nit]
_dep_display_nameuses defensivegetattron a typed domain object atsrc/apm_cli/commands/deps/cli.py:72.
ALockedDependencytype hint would improve static clarity, but runtime behavior is correct. - [nit] No pattern extraction is warranted.
The one-shot translation dictionary is the simplest correct design.
CLI Logging Expert
- [nit] Read-failure warning still interpolates the physical scan name at
src/apm_cli/commands/deps/cli.py:317.
Uselogical_nameso failures do not reintroduce the hash-slot leak.
DevX UX Expert
- [nit]
deps listanddeps treeuse different logical representations.
List shows_local/pkg, while tree shows the declared../pkg; both are valid and better than physical identities. - [nit] Local tree fallback can expose a raw unique key at
src/apm_cli/commands/deps/cli.py:69.
Preferlocal_pathorrepo_urlbeforeget_unique_key().
Supply Chain Security Expert
No findings.
OSS Growth Hacker
- [nit] The identity-leak fix is too narrow for standalone launch communication.
- [nit] The PR body models strong release-recovery practice for future contributor guidance.
Auth Expert -- inactive
The touched dependency-display and integration-test files do not change authentication or credential flows.
Doc Writer -- inactive
Existing documentation uses remote dependency examples and does not assert the old physical local-path output.
Test Coverage Expert
- [recommended] No unit test directly exercises the new local branch in
_dep_display_nameattests/unit/commands/test_deps_cli_helpers.py.
Proof (missing at):tests/unit/commands/test_deps_cli_helpers.py::TestDepDisplayName::test_local_source_renders_local_path-- proves: tree output uses the declared relative path instead of a host path. - [nit] The E2E has positive logical-name assertions but no explicit negative hash-slot assertion at
tests/integration/test_transitive_chain_e2e.py.
Proof (passed):tests/integration/test_transitive_chain_e2e.py::test_deps_commands_follow_full_lock_graph_and_ignore_embedded_manifests-- focused test passed in 3.34s.
Performance Expert
- [nit] The new dictionary is populated during the existing lockfile pass and adds constant-time lookup only.
- [nit] Two defensive attribute reads per displayed dependency are negligible.
- [recommended] Repeated installs from the assembled marketplace branch can make the full network class slow at
tests/integration/test_plugin_e2e.py:601.
Preserve the real-network signal but reduce redundant setup in a separate change.
This panel is advisory. It does not block merge. Re-apply the
panel-review label after addressing feedback to re-run.
Panel review findings on #2173: 1. _resolve_scope_deps: the scan-loop exception warning already emits the logical name (computed before the try) so a package read failure cannot leak _local/<hash>/pkg into the warning. 2. _dep_display_name: type-hinted to LockedDependency and switched from unsafe getattr to direct attribute access. Local deps now prefer the declared local_path and fall back to the logical repo_url (never the absolute local:/ unique-key slot) before any other fallback. 3. Focused unit tests for _dep_display_name: local_path present renders the relative path; local_path absent falls back to the logical repo_url. Both assert the absolute local:/ slot never surfaces. 4. Strengthened the transitive integration test with bounded-regex negative guards: neither _local/<12-hex>/ nor local:/ may appear in list/tree output. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a0678eed-0238-4a73-a540-a5a63948b1dc
Docs sync advisoryVerdict: no_change * Pages affected: 0 * LLM calls: 1/15 No documentation impact detected at head b668d61. The synchronized diff only hardens path sanitization and regression coverage; it changes no documented command, flag, schema, or workflow. |
Pre-merge review found three reachable residual leaks in `deps list`. Fixed with TDD (RED tests added first, then production change): 1. Absolute local_path display: absolute local paths are explicitly supported (DependencyReference.parse), but _dep_display_name rendered any truthy local_path, leaking `/Users/...`, `~/...`, or `C:\...`. A new display-only _is_absolute_local_path check (PurePosixPath + PureWindowsPath, plus `~` prefix) keeps relative declared paths and falls back to the logical repo_url for absolute ones. No heavy resolver import. 2. True orphan physical slots: a `_local/<12hex>/pkg` slot left after its lockfile entry is removed had no physical_to_logical mapping, so its name and warning exposed the hash. Orphan *detection* now keeps the raw physical identity; a new _logical_local_display strips the hash segment for the *displayed* name (`_local/pkg`). 3. Read-failure exception: APMPackage.from_apm_yml raises malformed-YAML errors embedding the absolute apm.yml path. The scan-loop warning no longer forwards the exception; it emits a stable, actionable public message keyed on the hash-free display name. Also strengthened the transitive integration negative guard to reject any host-absolute path (workspace root + bounded Windows-drive regex), not just `local:/`. Remote behavior and physical hashed storage are unchanged. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a0678eed-0238-4a73-a540-a5a63948b1dc
fix(deps): render logical local dep keys in deps list/tree; refresh stale v0.25.0 release tests
TL;DR
The v0.25.0 release integration suite (run 29184076146) went red with 13 failures across four independent root causes. Exactly one is a production bug:
apm deps listandapm deps treeleak physical, parent-scoped install slots for transitive local dependencies instead of their logical lockfile identity. This PR fixes that display regression and regrounds three stale test fixtures that drifted behind intentional prior changes (#2058, #2155, and upstreamawesome-copilot). No production behavior other than local-dep display changes; physical hashed storage, remote behavior, and lockfile keys are untouched.Problem (WHY)
Thirteen failures, one production bug, three stale tests:
deps list/deps tree): for a local transitive chain,apm deps listprinted_local/<12-char-hash>/pkg-depth-Nandapm deps treeprintedlocal:/abs/host/path/pkg-depth-N. Both leak the parent-scoped physical slot (introduced by refactor(core): consolidate red-team lifecycle fixes behind canonical owners #2155 to avoid sibling collisions) and, in the tree case, the absolute host filesystem path into user-facing output. The logical identity the user typed is../pkg-depth-N(lockfilerepo_url_local/pkg-depth-N).test_emu_ssh_remote_routes_to_correct_org_policy_repoasserted the first policy candidate iscontoso/.github, but Add .github-private as preferred policy repo candidate #2058 intentionally made.github-privatefirst in the cascade.test_pack_apm_provenance_e2ecases built a lockfile shape no real install produces -- a skill directory indeployed_fileswith per-child hashes. refactor(core): consolidate red-team lifecycle fixes behind canonical owners #2155's deployment ledger rebuilds hashes only for paths present indeployed_files, so the fixture never exercised the intended path.test_plugin_e2einstalledgithub/awesome-copilot/plugins/context-engineeringfrom defaultmain, which upstream has since reduced to a thin stub whose manifest declares absent component paths. APM correctly fails closed ([BUG] Missing declared plugin components are silently ignored #2103/refactor(core): consolidate red-team lifecycle fixes behind canonical owners #2155); the assembled plugin now lives on the upstreammarketplacebranch.The user-facing leak is the one that matters: the CLI's job is to show the logical graph the user declared, not internal storage. Grounding output in the declared identity keeps the surface honest, consistent with PROSE's "Grounding outputs in deterministic tool execution transforms probabilistic generation into verifiable action."
Approach (WHAT)
_resolve_scope_deps; renderlocal_pathin_dep_display_name.github-privatefirst; update cascade commentsdeployed_files...context-engineering#marketplace; add ref-stripped path constantImplementation (HOW)
src/apm_cli/commands/deps/cli.py(only production file)._resolve_scope_depsnow builds aphysical_to_logicalmap while reading the lockfile: for eachsource == "local"dep it recordsinstall_path.relative_to(apm_modules) -> dep.repo_url(logical). The scan loop resolves each scanned candidate's name through that map, so locked-membership, orphan detection, insecure lookup, and the reportednameall key on the logical_local/pkg, never the hash slot._dep_display_namerenders a local dep's declaredlocal_path(../pkg) instead ofget_unique_key(), which for anchored transitive local deps is an absolutelocal:/...slot. Hashed physical storage, remote deps, and lockfile keys are unchanged.tests/integration/test_dep_url_parsing_e2e.py-- assertion and cascade comments updated to.github-private -> .github -> .apm -> _apm. Production source untouched.tests/integration/test_pack_apm_provenance_e2e.py-- both the tamper and directory-symlink-escape cases now setdeployed_files=[skill_dir + "/", *file_rels], matching a real install. Fail-closed verification (verify_attested_file) is unchanged.tests/integration/test_plugin_e2e.py--PLUGIN_REFgains the#marketplacesuffix; a new ref-strippedPLUGIN_PATHconstant backs the five on-disk path assertions (the ref suffix is stripped from the install path byDependencyReference.get_install_path).tests/integration/test_transitive_chain_e2e.py-- the post-prune assertion expected a flat_local/pkg/apm.ymlfor transitive deps; it now globs the hashed slot exactly as the siblingtest_three_level_apm_chain_resolves_all_levelsalready does.Diagram
Legend: how a scanned physical slot is resolved to the logical name that
deps listreports, after this fix.flowchart LR A["scan apm_modules/<br/>_local/<hash>/pkg-depth-2"] --> B{"source == local?<br/>(lockfile)"} B -->|yes| C["physical_to_logical<br/><hash>/pkg-depth-2 → _local/pkg-depth-2"] B -->|no| D["canonical string<br/>owner/repo"] C --> E["report logical name<br/>_local/pkg-depth-2"] D --> E E --> F["orphan check · insecure lookup · display"]Trade-offs
get_install_path's hashing. refactor(core): consolidate red-team lifecycle fixes behind canonical owners #2155 hashes physical slots to prevent sibling collisions; that is correct and the task scope preserves it. The logical/physical split is intentional -- the fix reconciles the view, not the storage._resolve_scope_depsto_dep_display_name. The traced root cause named onlydeps list. Testing showeddeps treeleaked the same class of value (absolutelocal:/...), so the one-line_dep_display_namechange is included to make the tree logical too. Both live in the same file and the same conceptual fix.Benefits
apm deps listandapm deps treeshow the logical dependency the user declared (_local/pkg,../pkg) for local chains of any depth.local:/Users/...) leaks into CLI output.Validation
Note
Local
mainin this worktree is stale; the branch is exactly one commit on top oforigin/main(3880677f, the v0.25.0 release commit). The PR diff is 5 files.Focused failing tests (the four root causes) -- all green
Full relevant test files + broader deps coverage
Plugin ref -- real network install from #marketplace (token available)
Branch/tree verified live before editing:
marketplaceexists (4ce0b479),plugins/context-engineeringcontainsagents/andskills/.Lint mirror (canonical contract) -- all green
Scenario Evidence
deps listshows the logical graph for a local transitive chaintest_transitive_chain_e2e.py::test_deps_commands_follow_full_lock_graph_and_ignore_embedded_manifests(list assertion)deps treeshows declared refs, no host path leak.github-privatefirsttest_emu_ssh_remote_routes_to_correct_org_policy_repotest_apm_pack_rejects_tampered_file_in_deployed_directoryTestPluginNetworkE2E::test_install_real_pluginHow to test
../pkg-a -> ../pkg-b -> ../pkg-cchain andapm install ../pkg-a.apm deps list; confirm names read_local/pkg-b,_local/pkg-c(no hash prefix).apm deps tree; confirm rows read../pkg-b,../pkg-c(nolocal:/abs/path).APM_RUN_INTEGRATION_TESTS=1 APM_E2E_TESTS=1.Panel review follow-up (commit
7733c8ac3)Folded four in-scope panel findings on this branch:
_resolve_scope_depsscan-loop exceptionwarning emits the logical name (resolved before the
try), so a packageread failure cannot surface
_local/<hash>/pkg._dep_display_name(dep: LockedDependency)dropsgetattrfor direct attribute access; local deps preferlocal_path, elsethe logical
repo_url, never the absolutelocal:/unique-key slot.test_deps_cli_helpers.py: local_path presentrenders the relative path; local_path absent falls back to
repo_url. Bothassert
local:/never appears andget_unique_key()is not consulted._local/<12-hex>/norlocal:/appears indeps list/deps treeoutput.The repeated network-E2E setup redesign is intentionally deferred; tracked
separately after the release.
Pre-merge review round 2 (commit
b668d6141)Three reachable residual leaks in
deps list, each fixed TDD (RED first):_dep_display_namerendered any truthylocal_path, leaking/Users/...,~/...,C:\.... New display-only_is_absolute_local_path(PurePosixPath + PureWindowsPath +~) keepsrelative declared paths, falls back to logical
repo_urlfor absolute._local/<12hex>/pkgleft after itslockfile entry is removed has no mapping. Orphan detection keeps the raw
physical identity; new
_logical_local_displaystrips the hash for thedisplayed name (
_local/pkg).ValueErrorembeds theabsolute apm.yml path. The scan-loop warning no longer forwards the
exception; it emits a stable public message keyed on the hash-free name.
Integration negative guard strengthened to reject any host-absolute path
(workspace root + bounded Windows-drive regex), not just
local:/.Remote behavior and physical hashed storage unchanged.
Important
Remote CI has not yet run on this branch; the "green" claims above are the local lint/test mirror only. Do not treat CI as green until the PR checks complete. Merge, tag moves, and release re-trigger are intentionally left to the maintainer.
Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com