Repository navigation
Fix npm v2 revert half-reverting the lock (#1937) - #1938
Conversation
Assisted-by: Claude Code:claude-opus-5-5
After vendoring a lockfileVersion 2 project, a plain `npm install` (npm 7-12) re-saves the lock and drops `resolved` from the legacy `dependencies` mirror node, keeping only `version` and the patched `integrity`. `rollback`, `remove` and `vendor --revert` called that node drift, yet still restored the `packages` entry it mirrors. They exited 1 (or reported "Kept 1 drifted package") over a lock that disagreed with itself, kept the artifact, and npm 6 then failed with EINTEGRITY. A resolved-less mirror node carrying the vendored integrity now counts as ours, the same rule `vex` and `vendor --check` use since #879, so all three commands restore both lock halves and remove the artifact. When a mirror node or its `packages` entry really did drift, neither half is rewritten any more, and the kept half gets a warning naming the drifted one. Fixes #1937 Assisted-by: Claude Code:claude-opus-5-5
Re-running `vendor` or `scan` after `npm install` dropped a v2 mirror node's `resolved` recorded that patched node as its pre-vendor original, so a later revert put the patched integrity back while restoring the `packages` entry to the registry: the same half-reverted lock. The node now counts as already vendored, and its original carries over from the earlier record. The twin gate now only holds back a record the revert would rewrite, so an entry the user already restored is not reported as left alone, and v3 locks skip the dry pass entirely. Refs #1937 Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
|
bugbot run Generated by Claude Code |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit f6f0ee6. Configure here.
|
Ready for review — burn-down agent.
Generated by Claude Code |
Final review briefWhat it does: After vendoring an npm lockfileVersion 2 project, a plain Risk: medium. It changes the npm lock revert path, which deletes artifacts. The new ownership rule is narrow (an exact integrity match), and the twin gate only holds records back, never forward, so when it is wrong it keeps the vendored state rather than losing data. Look here:
Verified:
Changes I made: none. Open questions (not blocking, follow-up material):
Auto-merge (squash) is armed, so approving sends this straight to the merge queue. Generated by Claude Code |
|
[agent] On the final review brief's open questions: both stay out of this PR, as follow-ups.
Generated by Claude Code |
Wenxin Jiang (Wenxin-Jiang)
left a comment
There was a problem hiding this comment.
[agent] Reviewed f6f0ee669789151fd730556d451e2a336e0991e2. Checked atomic npm v2 twin restoration and ownership after npm drops legacy resolved fields. No major correctness issues found. Validation: source and regression-test review plus the available exact-head CI checks; no local test pass is claimed.
LLM Description written by Claude Code:claude-opus-5-5
Fixes #1937
Summary
After vendoring an npm lockfileVersion 2 project, a plain
npm install(npm 7-12) re-saves the lock and dropsresolvedfrom the legacydependenciesmirror node.rollback,removeandvendor --revertthen half-reverted the lock and kept the vendored state. With this PR they restore both halves of the lock and delete the artifact. When something really did drift, they leave both halves alone, so the lock always stays consistent with itself.Root cause
revert_one_record(crates/socket-patch-core/src/vendor/npm_lock.rs) only treated a live fragment as "ours" when itsresolvedwas the vendored one. npm never writesresolvedfor afile:resolution in the v2 mirror, so the node keeps onlyversionand the patchedintegrity. #879 taughtvexandvendor --checkthat this shape is still wired, but the revert engine still called it drift. Each record was also reverted on its own, so thepackagesentry was restored to the registry while its mirror twin was kept. npm 6 then failed EINTEGRITY, and the run said "vendored state left untouched".The vendor side had the same blind spot (found in review). Re-running
vendor/scan, which was the printed remedy, treated the resolved-less node as not vendored and recorded the patched node as its pre-vendororiginal. A later revert would put the patched integrity back.Changes
resolvedand the recorded vendoredintegritycounts as ours (legacy_node_lost_our_resolved, the Vendored npm vex and vendor --check fail after any npm 7–10npm installon a lockfileVersion 2 lock, because npm dropsresolvedfrom the legacy mirror and #813 treats that as an unpatched npm 6 install (regression) #879 rule).TwinGate) finds packages with a drift-kept record. Their other records that would be rewritten are left alone, with avendor_lock_entry_driftedwarning naming the drifted twin. A converged, removed or moved-off twin is not blamed. Locks with no mirror records (v3, v1-free) skip the dry pass.original: None, whichcarry_forward_wiringfills from the previous record.Tests (red → green)
vendor::npm_lock::tests::v2_revert_restores_a_resolved_less_mirror_node_with_our_integrityvendor::npm_lock::tests::v2_revert_keeps_the_packages_twin_of_a_drifted_mirror_nodepackagesentry reverted (half-revert)vendor::npm_lock::tests::v2_revert_keeps_the_packages_twin_of_a_re_resolved_mirror_nodepackagesentry revertedpackagesstays wiredvendor::npm_lock::tests::v2_revendor_of_a_resolved_less_mirror_node_records_no_originaloriginal= patched nodeoriginal: Nonevendor::npm_lock::tests::v2_revert_does_not_blame_a_converged_packages_twine2e_vendor_npm_build::npm_v2_revert_after_npm_install_restores_both_lock_halves(real npm 10.9.9,--lockfile-version 2;rollback,remove <purl>,vendor --revert, re-vendor +vendor --revert)rollbackexits 1rollback_failed; re-vendor case leaves the mirror on the patched integrityCommands run locally:
cargo fmt --all -- --check: cleancargo clippy --workspace --all-features -- -D warnings: cleancargo test -p socket-patch-core --all-features --lib vendor::npm_lock: 122 passedcargo test -p socket-patch-cli --all-features --test e2e_vendor_npm_build -- npm_v2_revert_after_npm_install: passed. Red on the base code:rollback_failed; red without the vendor-side change: the mirror node was left on the patched integrity.cargo test -p socket-patch-core --all-features: 7000 passed, 4 failed. The 4 also fail onmainin this container, because it runs as root and root ignores read-only and unremovable permissions:pypi_poetry::wire_write_failure_maps_error_and_leaves_lock_untouched,pypi_requirements::wire_failure_rolls_back_already_written_files,copy_tree::relax_loop_must_not_traverse_symlinked_root,vlt_heal::an_unremovable_hidden_lock_keeps_every_store_entry.cargo test -p socket-patch-cli --all-features --lib --test e2e_vendor_npm_build: 940 + 44 passedcargo test --workspace --all-featurescould not finish locally: building every integration-test binary ran out of this session's disk allowance (No space left on device). CI runs the full matrix.Per-issue checklist:
npm install, yet half-revert the lock:packagesgoes back to the registry while the legacy mirror keeps the patched integrity, and npm 6 then fails EINTEGRITY #1937: rollback / remove /vendor --revertafternpm installon a v2 lock (e2e plus unit test); no half-revert when one half really drifted (two unit tests); re-vendor remedy path (unit plus e2e).Review notes (
/code-review high), not changedintegrityas an exact string. That is what Vendored npm vex and vendor --check fail after any npm 7–10npm installon a lockfileVersion 2 lock, because npm dropsresolvedfrom the legacy mirror and #813 treats that as an unpatched npm 6 install (regression) #879's vex rule does too (pins_a_wired_refcompares the SRI strings exactly), so revert and vex agree.legacy_pointer_packages_keymaps/dependencies/ws/dependencies/xtonode_modules/ws/node_modules/x, notpackages/ws/node_modules/x.lock_inventoryuses the same pre-existing convention. For a nested dependency of a workspace member, the twin gate falls back to the previous per-record behavior: no regression, but no gate either. Possible follow-up.vendor_lock_entry_driftedcode. That is the same codeRevertOutcome::drift_skippeduses to keep state, so the two can't disagree.Wrappers
Only Rust core and CLI tests change. The npm, PyPI and gem wrappers only dispatch the binary.
https://claude.ai/code/session_011f9aeNkhAFT7jRyt8GURGB
Note
Medium Risk
Changes npm lock revert and re-vendor wiring logic on a critical path; behavior is heavily tested but incorrect twin pairing could still leave locks inconsistent in edge cases.
Overview
Fixes half-reverts of npm lockfileVersion 2 locks after a normal
npm install(npm 7–12), which stripsresolvedfrom the legacydependenciesmirror while leaving the vendored integrity.Revert now treats a mirror node with no
resolvedbut the recorded vendored integrity as still ours (same rule as vex /vendor --check, #879), sorollback,remove, andvendor --revertcan restore both thepackagesentry and its mirror and remove the artifact. ATwinGatedry-run skips rewriting one half when the other half would be drift-kept, avoiding a self-inconsistent lock (npm 6 EINTEGRITY).Re-vendor treats that mirror shape as already wired and does not snapshot the patched node as
original;carry_forward_wiringkeeps the true pre-vendor state.Adds focused unit tests in
npm_lock.rsand an e2e case exercising rollback, remove, revert, and re-vendor afternpm install.Reviewed by Cursor Bugbot for commit f6f0ee6. Configure here.
Generated by Claude Code