Fix vendored revert keeping artifact for removed lock entry (#665) - #689
Mikola Lysenko (mikolalysenko) wants to merge 4 commits into
Conversation
Assisted-by: Claude Code:claude-opus-5-5
After `npm uninstall`, `yarn remove`, `pnpm remove` or `bun remove` of a vendored package, the revert treated the vanished lock entry as drift. It kept the artifact and ledger entry forever, so `rollback` and `remove` exited 1 on every run, `scan --prune` kept the entry, and the printed remedies looped. A vanished entry now warns `vendor_lock_entry_removed` instead of `vendor_lock_entry_drifted`. The artifact and entry are dropped once no wired file still mentions the uuid dir, and are kept, as before, while one does. Re-resolved entries are still drift-kept. Fixes #665 Assisted-by: Claude Code:claude-opus-5-5
a64e33a to
8f841c0
Compare
The two scan --prune e2e tests encoded the old behavior that an uninstalled vendored dependency is drift-kept forever. With #665 the prune now reverts it in one run, so the tests assert that instead and check the unwired warning before the prune. Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
|
[burn-down agent] Labeled Ready for review at
Generated by Claude Code |
|
Codex review of
Two independent reproductions confirm this:
The new deletion path needs to distinguish absent files from unreadable ones and recognize semantically equivalent references before claiming absence. I’m correcting the shared check and adding regressions. Ready for review is being held off until the correction, native install controls, and fresh CI/review are clear. |
The #665 gate deleted a vendored artifact once one readable lockfile lacked the literal `.socket/vendor/npm/<uuid>/` path. That missed a lock whose resolution uses JSON-escaped slashes, and it ignored an alternate lockfile (npm-shrinkwrap.json) that exists but cannot be read, so the next fresh install failed with ENOENT. Deletion now needs every existing wired file to be read and to not mention the uuid in any spelling: only NotFound counts as absent, the match is on the uuid case-insensitively, JSON is parsed so \u escapes are decoded, and text with an escape the scan cannot see fails closed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BY1gbCU7vLkvqCmxRY9FF4
|
Both Codex P1 reproductions are confirmed and fixed in 43a6967. Pushed now so a parallel correction isn't needed. If you already have one, please rebase it onto this or drop it.
Regressions in
The unreadable case uses invalid UTF-8 rather than file permissions so it also fails as root. The full Generated by Claude Code |
|
BugBot review 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 43a6967. Configure here.
LLM Description written by Claude Code:claude-opus-5-5
Fixes #665
Summary
After
yarn remove,npm uninstall,pnpm removeorbun removeof a vendored package,rollbackandremoveexited 1 forever andscan --prunekept the entry.vendor --revertreported success without cleaning anything up, and the remedies the CLI printed looped. With this change the firstrollback/remove/vendor --revert/scan --prunedrops the unreferenced artifact and ledger entry, leaves the user's lock byte-identical, and says so with a warning. Later runs are clean exit-0 no-ops.Root cause
The npm-family vendored reverts handled a recorded lock entry that no longer exists the same way as one a third party re-resolved: they emitted
vendor_lock_entry_drifted. The affected reverts are:revert_recorded_blockrevert_one_recordRevertOutcome::drift_skipped()keys on that code, so each backend returned early withkeep_artifactbefore anything checked whether the lock still resolved through the artifact. The issue covers yarn classic; the npm comment on #665 confirms npm. Bun and pnpm share the same code path.Fix
vendor_lock_entry_removed(vendor::LOCK_ENTRY_REMOVED_CODE, plusRevertOutcome::lock_entry_removed()). It does not count as drift.npm_flavor::keep_artifact_while_lock_references_it, runs in every npm-family revert before the artifact is deleted..socket/vendor/npm/<uuid>/, the gate keeps the artifact exactly as before. The npm, yarn classic and berry reverts check their lockfiles; the pnpm revert also checkspackage.json/pnpm-workspace.yaml. Examples are an entry re-hoisted or re-keyed to a key the wiring never recorded, or an unreadable lock.NotFoundcounts as absent; an unreadable lock keeps the artifact) and must not mention the uuid in any spelling (case-insensitive, JSON parsed so\u/\/escapes are decoded; an undecodable escape fails closed).vendor --checkreportingvendor_check_okfor the removed package (one row of the issue's table) is unchanged. With the fix, the first rollback or prune removes the entry, so there is nothing left for--checkto report.Test evidence
New regression tests fail on
mainand pass with the fix (red → green):yarn remove)yarn_classic_lock::tests::revert_after_yarn_remove_drops_the_unreferenced_artifactnpm uninstall)npm_lock::tests::revert_after_dependency_removed_drops_the_unreferenced_artifactrollbacktwice +vendor --revertin_process_vendor::rollback_after_dependency_removed_cleans_up_and_convergesbun remove)bun_lock::tests::vanished_entry_drops_the_unreferenced_artifactvanished_rekeyed_packages_block_*,vanished_importer_dep_entry_*,snapshot_ref_*, legacyassert_removedmatrixscan --prunescan_vendor_e2e::scan_prune_reverts_unused_vendored_entry,scan_vendor_prune_reconciles_unwired_entry_on_an_empty_crawlGuard tests check that the artifact is still kept while the lock references it:
npm_lock::revert_keeps_artifact_when_a_vanished_entry_moved_to_an_unrecorded_keyyarn_classic_lock::revert_keeps_artifact_when_a_vanished_block_was_rekeyedbun_lock::vanished_entry_keeps_the_artifact_while_the_lock_references_itroot depcase, still keptExisting tests that encoded "vanished = drift" were updated to the new contract. Nothing was skipped or ignored.
Commands run locally:
cargo clippy --workspace --all-features -- -D warnings: clean.--all-targetsreports only pre-existing hits in files this PR doesn't touch.cargo test --workspace --all-features --no-fail-fast: 214 binaries ok. 12 tests fail only because the sandbox runs as root, so theirchmod 0o555/ unremovable-file write-failure setups can't fail a write. They are in repair, redirect, vlt-heal, copy_tree, pypi and covgap_commands_vendor, outside this diff, and CI runs them as non-root.cargo test -p socket-patch-cli --test e2e_vendor_npm_build --test e2e_vendor_yarn_classic_dev_flow -- --include-ignored: ok.cargo fmtis not applied tree-wide becausemainitself is not rustfmt-clean. The new code is formatted and only touched hunks are included.🤖 Generated with Claude Code
https://claude.ai/code/session_01BY1gbCU7vLkvqCmxRY9FF4
Note
Medium Risk
Changes vendored revert/GC semantics across all npm-family lock backends; incorrect uuid-unreference proof could delete artifacts still needed for installs, though the new gate is explicitly fail-closed on unreadable locks.
Overview
Fixes #665: npm-family vendoring no longer treats a removed lock entry like third-party drift, so
rollback,remove,vendor --revert, andscan --prunecan actually clean up afternpm uninstall/yarn remove/pnpm remove/bun remove.Vanished wiring now emits
vendor_lock_entry_removed(notvendor_lock_entry_drifted), withRevertOutcome::lock_entry_removed()so drift-based artifact retention does not apply. Before deleting.socket/vendor/npm/<uuid>/,keep_artifact_while_lock_references_itrequires proof the uuid is absent from every readable wired lock (and related pnpm surfaces)—re-hoisted, re-keyed, escaped, or unreadable locks still keep the artifact fail-closed.CLI_CONTRACT.md documents the npm-family rule; integration and unit tests expect one-shot cleanup and convergent exit 0 on repeat runs.
Reviewed by Cursor Bugbot for commit 43a6967. Configure here.
Generated by Claude Code