Skip to content

Fix npm v2 revert half-reverting the lock (#1937) - #1938

Merged
Mikola Lysenko (mikolalysenko) merged 3 commits into
mainfrom
agent/fix-npm-v2-mirror-revert
Nov 2, 2026
Merged

Mikola Lysenko (mikolalysenko) merged 3 commits into
mainfrom
agent/fix-npm-v2-mirror-revert

Conversation

@mikolalysenko

@mikolalysenko Mikola Lysenko (mikolalysenko) commented Nov 1, 2026 •

Copy link
Copy Markdown
Collaborator

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 drops resolved from the legacy dependencies mirror node. rollback, remove and vendor --revert then 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 its resolved was the vendored one. npm never writes resolved for a file: resolution in the v2 mirror, so the node keeps only version and the patched integrity. #879 taught vex and vendor --check that this shape is still wired, but the revert engine still called it drift. Each record was also reverted on its own, so the packages entry 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-vendor original. A later revert would put the patched integrity back.

Changes

Tests (red → green)

Test Without fix With fix
vendor::npm_lock::tests::v2_revert_restores_a_resolved_less_mirror_node_with_our_integrity drift warning, artifact kept lock byte-identical to pre-vendor, artifact gone
vendor::npm_lock::tests::v2_revert_keeps_the_packages_twin_of_a_drifted_mirror_node packages entry reverted (half-revert) lock untouched
vendor::npm_lock::tests::v2_revert_keeps_the_packages_twin_of_a_re_resolved_mirror_node packages entry reverted packages stays wired
vendor::npm_lock::tests::v2_revendor_of_a_resolved_less_mirror_node_records_no_original original = patched node original: None
vendor::npm_lock::tests::v2_revert_does_not_blame_a_converged_packages_twin (new gate behavior) one drift warning, for the mirror only
e2e e2e_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) rollback exits 1 rollback_failed; re-vendor case leaves the mirror on the patched integrity all exit 0, both halves restored, artifact removed

Commands run locally:

  • cargo fmt --all -- --check: clean
  • cargo clippy --workspace --all-features -- -D warnings: clean
  • cargo test -p socket-patch-core --all-features --lib vendor::npm_lock: 122 passed
  • cargo 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 on main in 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 passed
  • cargo test --workspace --all-features could 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:

Review notes (/code-review high), not changed

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 strips resolved from the legacy dependencies mirror while leaving the vendored integrity.

Revert now treats a mirror node with no resolved but the recorded vendored integrity as still ours (same rule as vex / vendor --check, #879), so rollback, remove, and vendor --revert can restore both the packages entry and its mirror and remove the artifact. A TwinGate dry-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_wiring keeps the true pre-vendor state.

Adds focused unit tests in npm_lock.rs and an e2e case exercising rollback, remove, revert, and re-vendor after npm install.

Reviewed by Cursor Bugbot for commit f6f0ee6. Configure here.


Generated by Claude Code

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
@mikolalysenko
Mikola Lysenko (mikolalysenko) marked this pull request as ready for review November 1, 2026 15:05
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

BugBot review


Generated by Claude Code

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

bugbot run


Generated by Claude Code

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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.

@mikolalysenko Mikola Lysenko (mikolalysenko) added the Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review label Nov 1, 2026
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Ready for review — burn-down agent.

  • Head: f6f0ee669789151fd730556d451e2a336e0991e2
  • CI: all 82 check runs on this head succeeded or were skipped. None failed.
  • Bugbot reviewed f6f0ee66 and found no issues.
  • Mergeable with no conflicts. The PR doesn't touch CHANGELOG.md.
  • For the reviewer: the change is in vendor/npm_lock.rs, where reverting an npm v2 lockfile now restores both the packages and dependencies mirrors. The new e2e coverage is in e2e_vendor_npm_build.rs. Not announced in Slack yet: the send tool wasn't available in this run, so a later run will retry.

Generated by Claude Code

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Final review brief

What it does: After vendoring an npm lockfileVersion 2 project, a plain npm install (npm 7-12) drops resolved from the legacy dependencies mirror node. rollback, remove and vendor --revert then reverted the packages entry but kept the mirror, so the lock disagreed with itself. This PR counts a resolved-less mirror node that still has our integrity as ours (the same rule #879 gave vex), restores both halves together through a dry-pass "twin gate", and stops re-vendor from recording the patched node as the pre-vendor original.

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:

  • Read the full diff. CHANGELOG.md is untouched, there is no debug code, and the only refactor is the small record_packages_key helper.
  • cargo test -p socket-patch-core --all-features --lib vendor::npm_lock: all of the PR's tests pass.
  • Four of the five new unit tests exercise paths the fix changes. The fifth, v2_revert_does_not_blame_a_converged_packages_twin, guards the new gate.
  • CI: 31 checks passed, 20 skipped, 0 failed (ci-ok and clippy green). Cursor Bugbot passed. No open review threads.

Changes I made: none.

Open questions (not blocking, follow-up material):

  1. Patch upgrade after npm install. Re-vendoring with a new patch whose integrity differs (npm_lock.rs:1708-1709 compares against the new self.integrity) still records the old patched mirror node as original. A later revert then restores the patched integrity into the mirror without a warning. This behavior predates the PR, and the PR fixes the same-patch re-vendor remedy it prints, but the new-integrity case is worth a follow-up issue. A possible check: the mirror's twin pointed into the vendor dir before the rewrite, and the node's integrity equals that twin's pre-rewrite integrity.
  2. Workspace-nested twins. legacy_pointer_packages_key maps them to node_modules/ws/node_modules/x, not packages/ws/.... Those pairs fall back to the old per-record behavior. That is no regression, but it is not gated either (the PR description already notes this).

Auto-merge (squash) is armed, so approving sends this straight to the merge queue.


Generated by Claude Code

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

[agent] On the final review brief's open questions: both stay out of this PR, as follow-ups.

  1. Re-vendoring with a different patch after npm install. This is real. The mirror node carries the previous patch's integrity, which differs from the new one, so legacy_node_lost_our_resolved misses it, and the old patched node is recorded as original. It predates this PR and needs a separate signal: the twin packages entry pointed into .socket/vendor/ before the rewrite, and the node's integrity equals the twin's integrity before the rewrite. Fixing it here would widen a change that is already reviewed. Vendored npm rollback / remove / vendor --revert on a lockfileVersion 2 lock fail after any npm install, yet half-revert the lock: packages goes back to the registry while the legacy mirror keeps the patched integrity, and npm 6 then fails EINTEGRITY #1937's reported path is a same-patch re-vendor, and that is covered and tested here.
  2. Workspace-nested twins. This is the existing legacy_pointer_packages_key convention, shared with lock_inventory. Those pairs keep the old per-record behavior, so nothing regresses.

Generated by Claude Code

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

@mikolalysenko
Mikola Lysenko (mikolalysenko) added this pull request to the merge queue Nov 2, 2026
Merged via the queue into main with commit a722e27 Nov 2, 2026
82 checks passed
@mikolalysenko
Mikola Lysenko (mikolalysenko) deleted the agent/fix-npm-v2-mirror-revert branch November 2, 2026 16:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review

Projects

None yet

3 participants