From d9b1dd9ab99ba915f7fabf013c30ce6b2df43774 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 12:23:53 +0000 Subject: [PATCH 1/4] Start fix for #1252 Assisted-by: Claude Code:claude-opus-5-5 From a940212003ddf4576590c2bf5d7da4772f9124f6 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 12:26:56 +0000 Subject: [PATCH 2/4] Test revert keeps wheels a requirements.lock uses A vendored PyPI revert deletes the vendored wheel even when a requirements.lock export (Rye's name, still written by uv export -o) installs from it. Add unit and e2e regression tests for root and subdirectory requirements.lock exports; they fail until the reference probe reads those files. Refs #1252 Assisted-by: Claude Code:claude-opus-5-5 --- .../tests/e2e_vendor_pypi_build.rs | 15 +++++++ crates/socket-patch-core/src/vendor/pypi.rs | 39 +++++++++++++++++-- 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/crates/socket-patch-cli/tests/e2e_vendor_pypi_build.rs b/crates/socket-patch-cli/tests/e2e_vendor_pypi_build.rs index ab8daa70b..d8230f0c7 100644 --- a/crates/socket-patch-cli/tests/e2e_vendor_pypi_build.rs +++ b/crates/socket-patch-cli/tests/e2e_vendor_pypi_build.rs @@ -858,6 +858,21 @@ fn uv_vendor_revert_keeps_wheel_while_subdir_pylock_references_it() { uv_vendor_revert_keeps_wheel_while_export_at("uv-export-subdir-pylock", "deploy/pylock.toml"); } +/// #1252: the same keep for an export under a name that is neither +/// `*.txt` nor a Python lock name (Rye's `requirements.lock`, which +/// `uv export -o` keeps writing after a Rye -> uv move), at the root and +/// in a subdirectory. Before the fix the probe skipped the file by its +/// extension, so the revert deleted the wheel it installs. +#[test] +#[serial_test::serial] +fn uv_vendor_revert_keeps_wheel_while_lock_named_export_references_it() { + uv_vendor_revert_keeps_wheel_while_export_at("uv-export-requirements-lock", "requirements.lock"); + uv_vendor_revert_keeps_wheel_while_export_at( + "uv-export-subdir-requirements-lock", + "deploy/requirements.lock", + ); +} + fn uv_vendor_revert_keeps_wheel_while_export_at(tag: &str, out: &str) { let Some((uv, python)) = capstone_uv(tag) else { return; diff --git a/crates/socket-patch-core/src/vendor/pypi.rs b/crates/socket-patch-core/src/vendor/pypi.rs index dbccf0a58..f6cddf2ee 100644 --- a/crates/socket-patch-core/src/vendor/pypi.rs +++ b/crates/socket-patch-core/src/vendor/pypi.rs @@ -6099,8 +6099,7 @@ wheels = [ /// entry; once the file stops naming the wheel the next revert cleans up. #[tokio::test] async fn requirements_revert_keeps_artifact_for_subdir_requirements_file() { - use crate::vendor::pypi_requirements::wire_requirements; - for (file, content) in [ + assert_revert_keeps_artifact_for(&[ ( "requirements/lock.txt", "six @ file:///proj/.socket/vendor/pypi/{UUID}/six-1.16.0-py2.py3-none-any.whl\n", @@ -6113,7 +6112,41 @@ wheels = [ "deploy/requirements/prod.txt", "./.socket/vendor/pypi/{UUID}/six-1.16.0-py2.py3-none-any.whl\n", ), - ] { + ]) + .await; + } + + /// #1252: `uv export -o` and `uv pip compile -o` write any file name, + /// and Rye's `requirements.lock` / `requirements-dev.lock` keep being + /// written after a Rye -> uv move. Such an export at the root or in a + /// subdirectory installs from the wheel exactly like a `*.txt` one, so + /// the revert keeps the wheel until the export stops naming it. + #[tokio::test] + async fn requirements_revert_keeps_artifact_for_lock_named_export() { + assert_revert_keeps_artifact_for(&[ + ( + "requirements.lock", + "six @ file:///proj/.socket/vendor/pypi/{UUID}/six-1.16.0-py2.py3-none-any.whl\n", + ), + ( + "requirements-dev.lock", + "six @ file:///proj/.socket/vendor/pypi/{UUID}/six-1.16.0-py2.py3-none-any.whl\n", + ), + ( + "deploy/requirements.lock", + "./../.socket/vendor/pypi/{UUID}/six-1.16.0-py2.py3-none-any.whl\n", + ), + ]) + .await; + } + + /// Wires the root `requirements.txt`, writes each `(file, content)` + /// export naming the wheel, and checks that the dry run and the wet + /// revert keep the wheel for it, and that the revert reclaims the + /// artifact once the export stops naming it. + async fn assert_revert_keeps_artifact_for(cases: &[(&str, &str)]) { + use crate::vendor::pypi_requirements::wire_requirements; + for &(file, content) in cases { let tmp = tempfile::tempdir().unwrap(); let root = tmp.path(); tokio::fs::write(root.join("requirements.txt"), "six==1.16.0\n") From 2d3eeb8c03de1e02e3d7a89d59856413704ef444 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 12:30:20 +0000 Subject: [PATCH 3/4] Keep vendored wheels a requirements.lock uses Reverting a vendored PyPI package (vendor --revert, remove, rollback or the hosted takeover) deleted the vendored wheel while a requirements.lock export still installed from it, so the next pip install -r requirements.lock failed. The in-use check only read *.txt files and fixed Python lock names. It now also reads any *.lock file at the root and in subdirectories, so the wheel is kept with a vendor_revert_residual_reference warning until the export stops naming it. Fixes #1252 Assisted-by: Claude Code:claude-opus-5-5 --- crates/socket-patch-core/src/vendor/pypi.rs | 25 +++++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/crates/socket-patch-core/src/vendor/pypi.rs b/crates/socket-patch-core/src/vendor/pypi.rs index f6cddf2ee..73822f574 100644 --- a/crates/socket-patch-core/src/vendor/pypi.rs +++ b/crates/socket-patch-core/src/vendor/pypi.rs @@ -2005,9 +2005,10 @@ async fn guard_unwired_pypi_revert( /// named project files, the root `requirements.txt` plus every `-r` include /// the planner may have written a pin into, every Python lock the root /// directory LISTS (`uv.lock`, `pylock*.toml`, `*.py.lock` with its paired -/// script), and every other root-level `*.txt` (a `uv export -o` target, or -/// a `requirements-dev.txt` the user moved a vendor line into), plus every -/// `*.txt` or Python lock in a project subdirectory +/// script), and every other root-level `*.txt` or `*.lock` (a `uv export -o` +/// target such as Rye's `requirements.lock`, #1252, or a +/// `requirements-dev.txt` the user moved a vendor line into), plus every +/// `*.txt`, `*.lock` or Python lock in a project subdirectory /// ([`subdir_probe_names`]: a `requirements/dev.txt` the root never /// includes, #1167, or a `deploy/pylock.toml` export, #1213). `skip` /// names files left out of the probe (a dry run's not-yet-restored wiring). @@ -2067,7 +2068,7 @@ async fn pypi_reference_clause(project_root: &Path, uuid: &str, skip: &[&str]) - continue; }; let is_lock = crate::utils::python_lock::is_python_lock_name(&name); - if !is_lock && !name.ends_with(".txt") { + if !is_lock && !is_export_name(&name) { continue; } // lstat only: a regular file or ANY symlink is probed (the read @@ -2120,6 +2121,16 @@ async fn pypi_reference_clause(project_root: &Path, uuid: &str, skip: &[&str]) - None } +/// Whether a file that is not a Python lock name may still be a +/// requirements-format export [`pypi_reference_clause`] must read: any +/// `*.txt` (`pip freeze >`, `uv export -o requirements.txt`) and any +/// `*.lock` (#1252: Rye's `requirements.lock` / `requirements-dev.lock`, +/// which `uv export -o` and `uv pip compile -o` keep writing after a +/// Rye -> uv move; both accept any output name). +fn is_export_name(name: &str) -> bool { + name.ends_with(".txt") || name.ends_with(".lock") +} + /// Directory names [`subdir_probe_names`] never descends into: VCS metadata, /// socket-patch's own state, and tool or cache trees whose `*.txt` files /// are package payloads, not requirements files anyone installs from. @@ -2139,8 +2150,8 @@ const PROBE_SKIPPED_DIRS: &[&str] = &[ "site-packages", ]; -/// Every `*.txt` and every Python lock (with a script lock's paired -/// script) below the project root's subdirectories, as `/`-joined +/// Every `*.txt`, every `*.lock` and every Python lock (with a script +/// lock's paired script) below the project root's subdirectories, as `/`-joined /// root-relative names in a stable order (#1167, #1213): `pip install -r /// requirements/dev.txt` installs from a file the root `-r` tree never /// reaches, `pip freeze > requirements/lock.txt` or `uv export -o @@ -2193,7 +2204,7 @@ fn subdir_probe_names(project_root: &Path) -> Vec { } else if !rel.is_empty() && (ft.is_file() || ft.is_symlink()) { // Root-level files are the caller's own listing. let is_lock = crate::utils::python_lock::is_python_lock_name(&name); - if !is_lock && !name.ends_with(".txt") { + if !is_lock && !is_export_name(&name) { continue; } if is_lock { From 415ce51e35c9fea28e92e3e5637a7ed324405e09 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 12:30:57 +0000 Subject: [PATCH 4/4] Wrap long e2e test call for rustfmt Assisted-by: Claude Code:claude-opus-5-5 --- crates/socket-patch-cli/tests/e2e_vendor_pypi_build.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/socket-patch-cli/tests/e2e_vendor_pypi_build.rs b/crates/socket-patch-cli/tests/e2e_vendor_pypi_build.rs index d8230f0c7..8416f8c2d 100644 --- a/crates/socket-patch-cli/tests/e2e_vendor_pypi_build.rs +++ b/crates/socket-patch-cli/tests/e2e_vendor_pypi_build.rs @@ -866,7 +866,10 @@ fn uv_vendor_revert_keeps_wheel_while_subdir_pylock_references_it() { #[test] #[serial_test::serial] fn uv_vendor_revert_keeps_wheel_while_lock_named_export_references_it() { - uv_vendor_revert_keeps_wheel_while_export_at("uv-export-requirements-lock", "requirements.lock"); + uv_vendor_revert_keeps_wheel_while_export_at( + "uv-export-requirements-lock", + "requirements.lock", + ); uv_vendor_revert_keeps_wheel_while_export_at( "uv-export-subdir-requirements-lock", "deploy/requirements.lock",