Fix uv vendored revert half-reverting after a relock (#806, #821) - #822
Conversation
Assisted-by: Claude Code:claude-opus-5-5
After vendoring, an ordinary uv relock (`uv add --dev x`, `uv add y`) re-serializes the lock arrays that hold our element: the dev group's requires-dev line and `[manifest] overrides`. Revert matched those arrays by their exact recorded text, so it saw drift and kept uv.lock wired, but still reverted pyproject.toml. The pair then failed `uv sync --locked` while `vendor --revert` reported success. Revert now finds our unchanged element inside the live array under the same key and restores or removes just that element, rendering the array the way uv writes it. A pair gate also writes neither file when any record is genuinely drift-kept, so pyproject.toml and uv.lock always stay consistent. Fixes #806, #821. Assisted-by: Claude Code:claude-opus-5-5
Vendor six, run the uv command that re-serializes the lock array around our element (`uv add --dev zipp` for a dev group, `uv add idna` beside user overrides), then revert. Both files must be unwired with no drift warning, and `uv lock --check` must pass. Refs #806, #821. Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
A [manifest] overrides record holds the bare array, and the old convergence shortcut searched the whole lock for it. When the root requires-dist happened to match the user's overrides array, revert treated our element as already gone, left it in uv.lock and deleted the artifact it points at. Every whole-array record now reverts through its own key: an untouched array is restored verbatim, otherwise just our element is. Refs #806. Assisted-by: Claude Code:claude-opus-5-5
Revert treated any miss locating a whole-array record as convergence, including a key spelled differently or an unbalanced array. A lock that still routed through the vendored wheel could then lose the wheel. Only a key or section that is provably absent now counts as converged. Anything unreadable is drift, which keeps both files and the artifact. Refs #806, #821. Assisted-by: Claude Code:claude-opus-5-5
|
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 77686c4. Configure here.
|
[agent] One check is red on head 77686c4: PDM patch compatibility → I don't think this PR caused it:
No fix exists to port. Once this run finishes I'll re-run the failed job once. If it fails again, I'll treat it as real and dig into the case artifacts. Generated by Claude Code |
|
[agent] Labeled Ready for review.
Slack announcement not sent this run (no Slack send tool available), so the next run will retry. Generated by Claude Code |
|
[agent] Stacked follow-up: #841 fixes #840. That's a different cause in the same Generated by Claude Code |
LLM Description written by Claude Code:claude-opus-5-5
Fixes #806
Fixes #821
Summary
Before this change, a vendored uv project broke after an ordinary relock. Running
uv add --dev zippwith six in a dev group (#821), oruv add idnabeside a user-authoredoverride-dependencies(#806), and thenvendor --revert,remove,rollbackor a vendored→hosted takeover:uv sync --lockedfail with "The lockfile atuv.lockneeds to be updated";vendor --revertand the takeover exit 0 withstatus: "success", and the takeover warned "left in place" after it had already written pyproject.toml.Now:
Root cause (shared by both issues)
revert_uv(crates/socket-patch-core/src/vendor/pypi_uv.rs) had two defects:Whole-array text matching. The
uv_lock_requires_devrecord (Vendored uv package in a dependency group: afteruv add --dev/uv remove --dev,vendor --revert,remove,rollbackand the hosted takeover revert pyproject.toml but keep uv.lock's vendored requires-dev entry, souv sync --lockedfails (exit 0, "success") #821) and the Rewrittenuv_lock_manifest_overridesrecord (Vendored uv with a user-authoredoverride-dependencies: after any relock (uv add,uv lock --upgrade-package),vendor --revert/removerevert pyproject.toml but keep the vendored[manifest] overridesentry in uv.lock, souv sync --lockedfails (vendor --revert exits 0) #806) capture the entire<key> = [ … ]array, sibling elements included. Any relock that touches a sibling makes uv re-serialize the array:uv add --dev xrewrites the group line;uv add ysorts[manifest] overridesand splits it one element per line.Our element stays byte-identical, but the recorded text no longer matches, so revert called it drift. The convergence shortcut also searched the whole lock for the recorded array, so a byte-identical array under another key could fake convergence. That was found in review: see the resolved thread.
No pair gate. A drift-kept uv.lock record only added a warning. The pyproject.toml records were still reverted and both files written. That contradicts docs/testing/uv-compatibility.md ("conflicting changes preserve both files … rather than restoring only one side").
Change
revert_array_elements). It handlesuv_lock_requires_dev,uv_lock_manifest_constraintsand the Rewrittenuv_lock_manifest_overrides, none of which is text-matched across the whole lock any more. It finds the array under the record's own key, in the root unit's[package.metadata.requires-dev]or in[manifest]:revert_uvwrites neither file. Thevendor_lock_entry_driftedwarning still keeps the artifact and ledger entry, so a re-run finishes once the drift is undone. With this, the hosted takeover'sredirect_vendored_revert_failedwarning ("left in place") is accurate:revert_keeps_wiringinscan/hosted.rskeys on the same drift signal.remove,rollbackand the takeover all go throughrevert_pypi→revert_uv, so they get the same fix.docs/testing/uv-compatibility.mdnow describes the relock behaviour.Behaviour change to existing tests
Six existing unit tests asserted the half-revert that these issues report. #821 points at one of them,
revert_warns_and_skips_on_drifted_lock_fragment("The pyproject side (undrifted) was still reverted"). They now assert the pair gate: neither file is written on drift. One test, renamed torevert_removes_our_element_from_a_reshaped_manifest_overrides_array, covered a user reshaping the overrides array around our unchanged element. That is no longer drift: our element is removed and the user's reshaping is kept. No test was skipped or weakened. Every drift case still warnsvendor_lock_entry_driftedand keeps the artifact.Test evidence (red → green)
pypi_uv::tests::revert_survives_uv_reserializing_the_dev_group_lineuv add --dev)pypi_uv::tests::revert_survives_a_sibling_leaving_the_dev_groupuv remove --dev)pypi_uv::tests::revert_survives_uv_reserializing_manifest_overridespypi_uv::tests::revert_removes_our_override_from_a_sorted_multi_line_arraypypi_uv::tests::revert_converges_on_the_overrides_key_not_a_lookalike_arraypypi_uv::tests::revert_treats_an_unreadable_array_key_as_driftpypi_uv::tests::revert_writes_neither_file_when_a_lock_record_is_drift_kepte2e_vendor_pypi_build::uv_vendor_revert_after_dev_group_relock(real uv: vendor →uv add --dev zipp→vendor --revert→uv lock --check)uv.lock fragment … changed since vendoring; left untouched)e2e_vendor_pypi_build::uv_vendor_revert_after_manifest_overrides_relock(real uv: useroverride-dependencies+ transitive six →uv add idna==3.7→vendor --revert→uv lock --check)The e2e results were run locally against uv 0.8.17 with
SOCKET_PATCH_UV_E2E_REQUIRED=1, so skips count as failures. The red runs used main'spypi_uv.rswith the new tests. CI runs this suite on its uv legs (0.2.37 through 0.12.17, Linux and macOS); releases withoutuv lock --checkreportn/a, as the existing capstones do.Local gates (head 77686c4)
cargo clippy --workspace --all-features -- -D warnings: clean.rustfmt --checkon both touched Rust files: clean (both were clean on main).cargo test -p socket-patch-core --lib vendor::pypi_uv: 98/98 pass.cargo test -p socket-patch-cli --test e2e_vendor_pypi_build -- --include-ignored: 25/25 pass.cargo test --workspace --all-features --lib --bins(on c35bf96): every test passes except 4 write-permission tests in files this PR doesn't touch (copy_tree,vlt_heal,pypi_poetry,pypi_requirements). They expect a read-only path to refuse a write, which can't happen as root, and this sandbox runs as root. The integration suites are left to CI; a full local workspace build of every test binary didn't fit in the sandbox disk.Per-issue checklist
uv add --dev/uv remove --dev,vendor --revert,remove,rollbackand the hosted takeover revert pyproject.toml but keep uv.lock's vendored requires-dev entry, souv sync --lockedfails (exit 0, "success") #821vendor --revertafteruv add --dev/uv remove --dev→ the two dev-group unit tests and the real-uv e2euv add --dev/uv remove --dev,vendor --revert,remove,rollbackand the hosted takeover revert pyproject.toml but keep uv.lock's vendored requires-dev entry, souv sync --lockedfails (exit 0, "success") #821remove/rollback/ hosted takeover → the samerevert_uvpath; the pair gate makes the takeover's "left in place" trueoverride-dependencies: after any relock (uv add,uv lock --upgrade-package),vendor --revert/removerevert pyproject.toml but keep the vendored[manifest] overridesentry in uv.lock, souv sync --lockedfails (vendor --revert exits 0) #806vendor --revert/removeafter a relock re-serializes[manifest] overrides→ the overrides unit tests (including the lookalike-array case) and the real-uv e2erevert_writes_neither_file_when_a_lock_record_is_drift_kept,revert_treats_an_unreadable_array_key_as_drift, plus the five updated drift testsFollow-ups (out of scope)
add_manifest_overridestill appends our element in socket-patch's own spelling rather than uv's sorted multi-line form. With key-anchored revert this no longer causes drift, so the wiring format is left as is.🤖 Generated with Claude Code
https://claude.ai/code/session_016MCKtjJk6MF8zQWSFYWg7N
Generated by Claude Code