fix(update): re-check transitive dependencies' own semver ranges during apm update#2053
fix(update): re-check transitive dependencies' own semver ranges during apm update#2053nadav-y wants to merge 6 commits into
Conversation
…ed/dry-run apm update cannot leave apm_modules/ ahead of apm.lock.yaml download_callback materializes a re-resolved semver dep's new content to apm_modules/ during the resolve phase, before apm update's plan-confirmation gate runs. When that gate does not commit -- the user declines, the shell is non-interactive (no TTY, no --yes), or --dry-run is passed -- the new content was already written to disk while apm.lock.yaml stayed on the old version, silently leaving the project in a torn state. Reproduced live for both registry-sourced and git-sourced dependencies. _purge_cached_semver_paths_for_update (moved to the new update_backup.py, which also keeps install/phases/resolve.py under its LOC budget) now moves a semver dep's existing install path aside instead of deleting it outright, but only when apm update's plan_callback is present -- apm install --update has no decline path and keeps the original delete-and-redownload behavior unchanged. Once the plan-confirmation gate resolves, pipeline.py's new try/finally calls restore_update_backups: on commit, backups for successfully re-downloaded deps are discarded; otherwise every purged dep is reverted to its original content, and any freshly-downloaded dep with no prior backup (a new add swept into the same resolve pass) is removed outright. The finally block runs even when the non-interactive path raises SystemExit, so the rollback cannot be skipped by that abort. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ups on resolve-phase failure, fix collision-prone naming
Four real issues from the automated Copilot review, all confirmed by reading
the actual code paths before fixing:
- restore_update_backups was only ever invoked from the plan-confirmation
gate in pipeline.py. If resolver.resolve_dependencies() itself raises
(e.g. a network error on a *different* dep, or a bad transitive manifest)
after purge_cached_semver_paths_for_update already moved some dep aside,
nothing restored it -- the backup was orphaned in .apm-update-backup/
with the dep's install path left missing. Added a try/except around the
resolve phase call in pipeline.py that restores staged backups before
re-raising, with two new pipeline tests covering both the failure path
and the (still correct) no-op when no backups were staged.
- restore_update_backups looked up dep objects via ctx.deps_to_install,
which is only populated after resolve_dependencies() returns
successfully -- exactly the failure window above. Switched to
ctx.all_apm_deps (populated before resolve starts, and guaranteed to
contain every dep purge_cached_semver_paths_for_update could have
purged) merged with ctx.deps_to_install (extends coverage to transitive
adds when resolution did complete).
- _sanitize_backup_name collapsed distinct dep keys to the same backup
directory name (e.g. "owner/repo" and "owner_repo" both became
"owner_repo"), risking one dep's backup silently overwriting another's.
Appended a short hash of the original key. New unit tests assert
distinct keys never collide.
- purge_cached_semver_paths_for_update logged "cleared cached install
path" unconditionally even when the rename/rmtree inside the
suppress(Exception) block actually failed, misleadingly implying
semver re-resolution would occur when it might not. Now only logs on
confirmed success.
Also added type hints (per repo convention) and fixed a stale docstring
reference to the old (now-public, no-underscore) function name.
The isinstance-based coercion this required in restore_update_backups
(rather than a plain `... or {}` fallback) is defensive against any
caller whose ctx doesn't populate these fields as real dicts/lists --
irrelevant in production (InstallContext's dataclass defaults guarantee
this), but it also happens to make the function robust against loosely-
mocked ctx objects in unrelated tests that now incidentally reach this
code path via the new pipeline.py exception handler.
Full unit suite: 18162 passed, 6 pre-existing failures (confirmed
identical on main, unrelated to this change), 0 new failures. ruff clean.
Re-verified the live repro (registry + git-sourced, decline/non-interactive/
dry-run/happy-path) end-to-end after every change in this commit.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The Mode B detector flags this PR's changes under src/apm_cli/install/ as net-new normative behaviour requiring a spec anchor + manifest row + Appendix C entry + conformance test. It isn't: apm update's plan- confirmation gate already documents (in its own code comment) that confirmation happens "before downloads begin so a 'no' answer cancels cleanly without touching the cache." This PR fixes a bug where that promise wasn't actually kept for semver re-resolution -- it restores the existing contract, it doesn't define a new one. apm-spec-waiver: bug fix restoring existing update-confirm-gate no-side-effect contract
…ng apm update APMDependencyResolver only invoked download_callback when a dependency's install path didn't already exist on disk. The prior fix for the plan-confirmation torn-state bug (microsoft#2050) forced this recheck for direct dependencies only (a pre-purge pass before BFS resolution starts), so any transitive dependency's own semver range was never re-evaluated against the remote, no matter how many newer matching versions had been published -- e.g. pkg1 -> pkg2 -> pkg3, all pinned to ^1.0.0: publishing pkg3 1.0.4 was silently ignored by `apm update` in pkg1, since pkg2 (and therefore pkg3) already existed locally. Replace the direct-only pre-purge with a per-node resolver gate (APMDependencyResolver._should_force_recheck, threaded via a new update_refs constructor flag) that widens the existing-path check for any non-local, non-artifactory-proxied, semver-ranged dependency -- covering every depth reached during BFS, not just direct deps. Move the backup-before-overwrite staging inline into download_callback (update_backup.backup_before_overwrite), since a transitive dependency's existence isn't known until its parent's manifest is read -- a pre-pass can't know what to back up ahead of time. ctx.update_backups now stores (dep_ref, backup_path) tuples so a transitive dep's backup can be restored even if it never appears in ctx.all_apm_deps or ctx.deps_to_install (e.g. resolution fails before those populate). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR fixes apm update so that semver-ranged dependencies are re-evaluated against the remote at any depth (including transitives), and extends the staged-backup/rollback mechanism so update-plan confirmation can safely decline/abort without leaving apm_modules/ ahead of apm.lock.yaml.
Changes:
- Widen dependency resolution so existing install paths can still invoke
download_callbackwhenupdate_refs=Trueand the dep is a non-local, non-proxied semver ref. - Move update rollback staging to an inline
backup_before_overwrite()call insidedownload_callback, and reconcile staged backups at the plan-confirmation gate (plus early restore on resolve-phase exceptions). - Add/refresh unit tests for the new recheck predicate and the inline backup/restore behavior; add changelog entries for #2050 and this fix.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
src/apm_cli/deps/apm_resolver.py |
Adds update_refs + _should_force_recheck() and widens the existing-path gate so transitive semver deps can be rechecked. |
src/apm_cli/install/phases/resolve.py |
Removes direct-only pre-purge and stages backups inline before overwrites; threads update_refs into the resolver. |
src/apm_cli/install/phases/update_backup.py |
Introduces the inline backup + restore reconciliation helpers for update-plan gating. |
src/apm_cli/install/pipeline.py |
Plumbs plan_callback into context and ensures backups are reconciled on plan decision (and restored on resolve exceptions when staged). |
src/apm_cli/install/context.py |
Documents and adds plan_callback and the new update_backups shape on InstallContext. |
tests/unit/deps/test_apm_resolver_edge_cases.py |
Adds coverage for _should_force_recheck() and for callback invocation on existing paths when updating. |
tests/unit/install/phases/test_update_backup.py |
New unit tests for sanitization + inline staging + restore behavior (including a transitive-only restore case). |
tests/unit/test_install_pipeline_orchestration.py |
Adds coverage ensuring resolve-phase failures trigger restore when backups were staged. |
CHANGELOG.md |
Adds Unreleased fixes entries for #2050 and #2053. |
Addresses Copilot review on microsoft#2053: backup_before_overwrite suppressed every exception during staging and returned False, so a rename/mkdir failure on an existing install path let the caller proceed straight to overwriting it with no rollback point staged. A later declined/aborted update would then see that dep in ctx.callback_downloaded but absent from ctx.update_backups -- indistinguishable from a fresh add -- and delete it outright, permanently losing the original content. Now raises once staging is actually required (plan_callback set and install_path exists), instead of swallowing the error. The resolve-phase exception handler already added in pipeline.py (for microsoft#2050's Copilot review) restores any backups staged for other deps this run before re-raising, so the whole apm update aborts cleanly -- recoverable, unlike a silent, undetected loss of the original content. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
APM Review Panel:
|
| Persona | B | R | N | Takeaway |
|---|---|---|---|---|
| Python Architect | 0 | 1 | 2 | Well-decomposed backup/restore module with correct fail-closed semantics; duplicated predicate is the main structural concern. |
| CLI Logging Expert | 0 | 2 | 1 | Backup/restore lifecycle is completely silent -- no verbose_detail on stage, no user-facing message on restore after decline/abort. |
| DevX UX Expert | 0 | 0 | 2 | Transitive semver re-resolution fix is UX-sound: update reaches full depth, declined plans restore cleanly, install is unaffected, no command-surface changes. |
| Supply Chain Security Expert | 0 | 1 | 1 | Backup module uses ad-hoc sanitization instead of the sanctioned path_security guards; suppress(Exception) in restore silences restore failures. |
| OSS Growth Hacker | 0 | 0 | 3 | High-trust correctness fix with excellent CHANGELOG prose; minor release-note framing opportunity missed but nothing blocks merge. |
| Auth Expert | 0 | 0 | 0 | No auth surface affected: auth_resolver still flows to resolver unchanged; new backup/restore module is auth-agnostic; wider download_callback reach reuses existing per-dep token resolution. |
| Doc Writer | 0 | 2 | 2 | Bug 3 (fail-closed staging failure) has no CHANGELOG entry; update.md omits transitive semver recheck behavior; two nits on mechanism wording and attribution style. |
| Test Coverage Expert | 0 | 2 | 1 | Strong unit coverage on predicate + backup/restore module; pipeline decline path missing restore assertion; no integration-tier test for transitive semver re-resolution flow. |
| Performance Expert | 0 | 1 | 4 | Correct approach; added cost is O(unique_repos) network RTTs for semver transitive deps during apm update, zero regression on apm install; backup I/O is ~1 rename per dep. |
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 assertion that decline path calls restore_update_backups(ctx, keep_new=False) -- Missing automated guardrail on a secure-by-default surface: the user promise 'decline an update and nothing changes' has no test proving restore is invoked.
- [Supply Chain Security Expert] Route backup_path through ensure_path_within and use safe_rmtree instead of _rrm in restore -- APM's security model requires all path deletions to flow through sanctioned chokepoints. Zero-behavioral-change fix.
- [Python Architect] Extract shared predicate from _should_force_recheck and _force_semver_resolve -- Three panelists independently flagged the duplicated gate condition. A shared function prevents silent drift on a correctness-critical predicate.
- [Doc Writer] Add CHANGELOG entry for Bug 3 (fail-closed on backup-staging failure) and update update.md to mention transitive semver re-evaluation -- Bug 3 has no CHANGELOG bullet; docs page scopes re-resolution to direct deps only, which is now inaccurate.
- [CLI Logging Expert] Emit verbose_detail on backup staging and user-facing summary on restore after decline/abort -- Replacement for the removed _purge log; restore after decline should confirm rollback happened.
Architecture
classDiagram
direction LR
class APMDependencyResolver {
<<Strategy>>
+_update_refs bool
+_download_callback DownloadCallback
+_should_force_recheck(dep_ref) bool
+_try_load_dependency_package(dep_ref, parent_chain, parent_pkg) APMPackage
}
class InstallContext {
<<ValueObject>>
+plan_callback Any
+update_backups dict
+callback_downloaded dict
+apm_dir Path
+apm_modules_dir Path
}
class DependencyReference {
<<ValueObject>>
+ref_kind str
+is_local bool
+get_unique_key() str
+get_install_path(modules_dir) Path
}
class backup_before_overwrite {
<<Pure Function>>
+__call__(ctx, dep_ref, install_path) bool
}
class restore_update_backups {
<<Pure Function>>
+__call__(ctx, keep_new) None
}
class download_callback {
<<Closure>>
+_force_semver_resolve bool
}
class run_install_pipeline {
<<IOBoundary>>
}
APMDependencyResolver ..> DependencyReference : checks ref_kind
APMDependencyResolver ..> download_callback : invokes when gate opens
download_callback ..> backup_before_overwrite : calls before overwrite
download_callback ..> InstallContext : writes callback_downloaded
backup_before_overwrite ..> InstallContext : writes update_backups
backup_before_overwrite ..> DependencyReference : reads unique key
run_install_pipeline ..> restore_update_backups : calls on resolve failure and plan gate
restore_update_backups ..> InstallContext : reads update_backups and callback_downloaded
classDef touched fill:#fff3b0,stroke:#d47600
class APMDependencyResolver:::touched
class backup_before_overwrite:::touched
class restore_update_backups:::touched
class InstallContext:::touched
class download_callback:::touched
flowchart TD
A["apm update CLI"] --> B["run_install_pipeline\npipeline.py"]
B --> C["_run_phase resolve\npipeline.py"]
C --> D["APMDependencyResolver.resolve\napm_resolver.py"]
D --> E{"install_path.exists()\nAND NOT _should_force_recheck?"}
E -->|"Yes: skip"| F["Load cached APMPackage\nfrom disk"]
E -->|"No: gate open"| G["download_callback\nresolve.py"]
G --> H{"_force_semver_resolve?"}
H -->|"Yes"| I["[NET] git ls-remote"]
H -->|"No"| J["Registry or git path"]
I --> K["[FS] backup_before_overwrite\nupdate_backup.py"]
J --> K
K --> L{"plan_callback set\nAND path exists?"}
L -->|"Yes"| M["[FS] install_path.rename backup_path"]
L -->|"No"| N["No-op"]
M --> O["ctx.update_backups[dep_key] = entry"]
N --> P["[NET] download/clone to install_path"]
O --> P
P --> Q["Return to BFS"]
C -->|"Exception"| R["[FS] restore_update_backups\nkeep_new=False"]
R --> S["Re-raise"]
B --> T["build_update_plan"]
T --> U["plan_callback confirm gate"]
U -->|"Declined / abort"| V["[FS] restore_update_backups\nkeep_new=False"]
U -->|"Confirmed"| W["[FS] restore_update_backups\nkeep_new=True"]
V --> X["Return empty InstallResult"]
W --> Y["Continue to integrate phase"]
Recommendation
This is a correctness fix for a real silent-failure gap in apm update, architecturally sound, with strong test coverage from a contributor. No panelist raised a blocking finding. The five follow-ups (decline-path test assertion, path containment, predicate dedup, CHANGELOG/docs gap, verbose logging) should be filed as issues and resolved before the next release, but none block merge of this PR.
Full per-persona findings
Python Architect
-
[recommended] _should_force_recheck duplicates _force_semver_resolve -- extract shared predicate at
src/apm_cli/deps/apm_resolver.py:886
APMDependencyResolver._should_force_recheck and download_callback's _force_semver_resolve encode the same boolean condition with a 'kept in sync deliberately' comment. Two copies of a gate predicate that must agree is a maintenance hazard. Extractis_semver_recheck_eligible(dep_ref, update_refs)into a shared location.
Suggested: Extract todeps/semver_utils.pyor a DependencyReference property. -
[nit] backup_before_overwrite uses getattr defensively on a typed dataclass at
src/apm_cli/install/phases/update_backup.py:97
getattr guards are for loosely-mocked ctx in tests but undocumented in the function. A comment prevents future contributors from removing these. -
[nit] plan_callback typed as Any at
src/apm_cli/install/context.py:82
Known signature (plan: UpdatePlan) -> bool warrantsCallable[[UpdatePlan], bool] | None.
CLI Logging Expert
-
[recommended] backup_before_overwrite is completely silent at
src/apm_cli/install/phases/update_backup.py:67
Removed _purge_cached_semver_paths_for_update had a verbose_detail line. New backup emits nothing. Function returns True when staged so the caller can log -- but no caller uses the return value.
Suggested:if backup_before_overwrite(ctx, dep_ref, install_path): logger.verbose_detail(f'[*] --update: staged rollback backup for {dep_ref.get_unique_key()}') -
[recommended] restore_update_backups emits no user-facing output when rolling back a declined/aborted update at
src/apm_cli/install/phases/update_backup.py:129
User declined the update but sees no confirmation rollback happened. Consider returning count of restored deps and emitting '[i] Restored N package(s) to pre-update state'. -
[nit] backup_before_overwrite docstring promises 'so the caller can log accordingly' but no caller uses the return value at
src/apm_cli/install/phases/update_backup.py:102
DevX UX Expert
-
[nit] No user-visible diagnostic when backup staging raises at
src/apm_cli/install/phases/update_backup.py:114
Raw OSError propagates without an actionable message. Consider wrapping with catch-and-reraise adding a one-line human hint. -
[nit] Duplicated predicate between _should_force_recheck and _force_semver_resolve at
src/apm_cli/deps/apm_resolver.py:886
Supply Chain Security Expert
-
[recommended] backup_path is not validated with ensure_path_within; deletions use _rrm instead of safe_rmtree at
src/apm_cli/install/phases/update_backup.py:113
Codebase security model requires path containment through ensure_path_within / safe_rmtree. Ad-hoc regex sanitization is a documented bug pattern. Zero-behavioral-change fix.
Suggested:from apm_cli.utils.path_security import ensure_path_within; ensure_path_within(backup_path, backup_root)after computing backup_path. Use safe_rmtree in restore_update_backups. -
[nit] suppress(Exception) in restore_update_backups silences restore failures at
src/apm_cli/install/phases/update_backup.py:178
If restore fails, user silently ends up with missing package while lockfile references old version. Log a warning on suppressed exceptions.
OSS Growth Hacker
-
[nit] CHANGELOG entry is maintainer-facing, not user-facing at
CHANGELOG.md
'download_callback only ran for a dependency whose install path didn't already exist' is implementation detail. User-facing rewrite: 'apm update now picks up newly published versions of transitive dependencies constrained by semver ranges (e.g. ^1.0.0)'. -
[nit] Two related entries (fix(update): stop declined/dry-run apm update from mutating apm_modules/ #2050 + fix(update): re-check transitive dependencies' own semver ranges during apm update #2053) could share a single story beat for stronger release impact.
-
[nit] Story angle worth surfacing at release time: 'apm update now re-evaluates semver ranges at every depth -- not just direct deps.'
Auth Expert
No findings.
Doc Writer
-
[recommended] Bug 3 (fail-closed on backup-staging failure) is absent from CHANGELOG at
CHANGELOG.md
Commit 813d1fb introduces a distinct user-visible safety guarantee with no CHANGELOG bullet.
Suggested: Add: 'apm updatenow fails closed when staging a rollback point fails before overwriting an existing dependency: a rename or mkdir error during backup staging aborts the whole update cleanly instead of silently losing original content. (by @nadav-y) (fix(update): re-check transitive dependencies' own semver ranges during apm update #2053)' -
[recommended] docs/reference/cli/update.md does not mention transitive semver ranges are now re-evaluated at
docs/src/content/docs/reference/cli/update.md:91
Docs scopes re-resolution to direct deps only. Users with deep dep trees may see more packages updated than before. -
[nit] fix(update): stop declined/dry-run apm update from mutating apm_modules/ #2050 CHANGELOG entry says 'staged and rolled back' -- mechanism is backup-and-restore, not download staging.
-
[nit] Attribution format deviates from predominant 0.24.0 style: use '(by @nadav-y, fix(update): re-check transitive dependencies' own semver ranges during apm update #2053)' not '(by @nadav-y) (fix(update): re-check transitive dependencies' own semver ranges during apm update #2053)'.
Test Coverage Expert
-
[recommended] Plan callback decline path does not assert restore_update_backups is called with keep_new=False at
tests/unit/test_install_pipeline_orchestration.py:204
test_plan_callback_false_returns_early has no guardrail on the critical safety net for 'say no to update'.
Proof (missing):tests/unit/test_install_pipeline_orchestration.py::TestRunInstallPipelinePlanCallback::test_plan_callback_false_returns_early_asserts_restore-- proves: A declined apm update restores backed-up install paths [devx, secure-by-default] -
[recommended] No integration-tier test for end-to-end transitive semver re-resolution backup/restore cycle at
tests/integration/test_update_e2e.py
Cross-module contract covered only by unit mocks. Install pipeline floor tier is integration-with-fixtures.
Proof (missing):tests/integration/test_update_e2e.py::TestUpdateE2E::test_update_dry_run_transitive_semver_restores_modules[devx, secure-by-default] -
[nit] test_plan_callback_true_continues does not assert restore_update_backups(ctx, keep_new=True) is called at
tests/unit/test_install_pipeline_orchestration.py:244
Performance Expert
-
[recommended] Added network cost is O(unique_repos) RTTs via ref_resolver_cache sharing -- acceptable for explicit apm update.
get_shared_ref_resolver reuses one RefResolver per (host, token) pair. For 5-15 transitive deps across 3-4 repos: 0-4 extra ls-remote calls. Acceptable. -
[nit] _should_force_recheck is zero-cost on apm install (update_refs=False short-circuits).
-
[nit] Duplicate predicate is sync-maintenance risk, not a perf bug.
-
[nit] backup_before_overwrite adds ~3ms I/O for 30 transitive deps -- negligible.
-
[nit] Dedup gate prevents O(n^2) re-checks in diamond deps. Correct.
This panel is advisory. It does not block merge. Re-apply the panel-review label after addressing feedback to re-run.
Summary
This branch's diff contains three bugs in
apm update's dependency resolution and rollback safety. Bug 1 was already fixed by #2050 and is only present here because this branch stacks on it. Bugs 2 and 3 are new in this PR.Bug 1 (fixed in #2050): torn state on declined/aborted update. Resolving a semver range downloads the candidate version to disk before the user confirms the update plan. A declined confirmation, a non-interactive abort, or
--dry-runused to leaveapm_modules/already advanced whileapm.lock.yamlstayed on the old version.Bug 2 (this PR): transitive dependencies never get re-checked.
apm updatewas not picking up a new version of a transitive dependency, even when the intermediate package's manifest still allowed it:Publishing a new
org/pkg3matching^1.0.0was silently ignored byapm updaterun fromorg/pkg1, even thoughorg/pkg2's manifest hadn't changed. Root cause:APMDependencyResolver._try_load_dependency_packageonly callsdownload_callback(which re-resolves a semver range against the remote) when the install path doesn't already exist:#2050 forced this recheck for direct dependencies only (a pre-purge pass that ran before BFS resolution started, since transitive deps aren't known until their parent's manifest is read). Once
org/pkg2exists locally,org/pkg3's own range is never re-evaluated -- the walk sees its path already exists and stops.Bug 3 (this PR, caught in Copilot review): a failed backup silently allowed an unprotected overwrite.
backup_before_overwriteswallowed every exception during staging and returnedFalse. If staging failed (e.g. a rename error) on an existing install path, the caller proceeded to overwrite it anyway with no rollback point. A later declined/aborted update would then see that dep as downloaded but with no backup entry -- indistinguishable from a fresh add -- and delete it outright, permanently losing the original content.Fix
apm_resolver.py: AddedAPMDependencyResolver._should_force_recheck(dep_ref), gated on a newupdate_refsflag. Widens the existing-path check toif not install_path.exists() or self._should_force_recheck(dep_ref):, sodownload_callbackruns for any non-local, non-artifactory-proxied, semver-ranged dependency at any depth. (fixes Bug 2)resolve.py: Removed the direct-only pre-purge pass (no longer needed); threadsupdate_refsinto the resolver.update_backup.py: Moved backup staging inline intodownload_callback, called immediately before a dependency's install path is overwritten, for direct and transitive deps alike -- sidesteps the chicken-and-egg problem of not knowing transitive deps ahead of time.ctx.update_backupsnow storesdep_key -> (dep_ref, backup_path)tuples so a transitive dep's backup survives even if it never reachesctx.all_apm_deps/ctx.deps_to_install. (fixes Bug 2's backup coverage)update_backup.py:backup_before_overwriteno longer suppresses staging exceptions -- it now raises when a backup is actually required, so the caller never proceeds to overwrite without one. The existing resolve-phase exception handler inpipeline.pyrestores any backups already staged for other deps this run before re-raising, so the whole update aborts cleanly instead of silently losing content. (fixes Bug 3)context.py/pipeline.py: Comment updates only, no functional change.A regression surfaced during development: widening only the resolver gate (without widening backup coverage) caused
restore_update_backupsto treat newly-reachable transitive deps as fresh adds with no backup, deleting them outright on decline. Caught via live repro before landing the inline-backup fix.Test plan
test_apm_resolver_edge_cases.py(TestShouldForceRecheck,TestTryLoadDependencyPackageForceRecheck) covering the recheck predicate and gate across semver/literal ref kinds, local deps, artifactory-proxied deps, andupdate_refson/off.test_update_backup.pyfor the inline, tuple-based API, including a transitive-dep-not-in-all_apm_deps regression test and a staging-failure test asserting it raises with the original content untouched.mcp_integrator/global_mcp_scope/lifecycle_executor), 0 new failures.^1.0.0: update reaches depth 3 across successive publishes.2.0.0against^1.0.0): correctly ignored.pkg1pinspkg2exactly,pkg2rangespkg3): innermost dep still updates through the exact-pinned intermediate.-y) andapm install --update(no confirm gate): both apply cleanly.Branch note
Stacked on #2050 (
fix/update-plan-gate-torn-state) -- sameupdate_backup.pymechanism, so the diff includes #2050's commits until it merges. Please review/merge #2050 first; commits unique to this PR start after2683e8ea.