From e403edbc4d4b731c7ee725b90bb2cc6db66d82e6 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 12:22:43 +0000 Subject: [PATCH 1/4] Start fix for #1105 Assisted-by: Claude Code:claude-opus-5-5 From b2bad323e160c08f3dd352abb5ff85fb0e7b60dc Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 12:40:15 +0000 Subject: [PATCH 2/4] Vendor over a hosted pin at an older patch After a hosted scan with patch A and a `uv sync` or `poetry install`, the venv holds A's patched bytes. When a newer patch B for the same release arrived, `scan --mode vendored` / `vendor` dropped the package as "no installed package found on disk" and exited 1, leaving the project hosted on A, while the dry run previewed success. The installed copy is recognised as a superseded install only through the vendor ledger. A live hosted pin at another patch uuid now counts too (for a sole release variant, since the pin names none), so the takeover restores the upstream entry and vendors B from the pristine artifact. Fixes #1105 Assisted-by: Claude Code:claude-opus-5-5 --- .../socket-patch-cli/src/commands/vendor.rs | 43 ++++++++++++-- .../tests/mode_migration_pypi.rs | 57 +++++++++++++++++++ 2 files changed, 95 insertions(+), 5 deletions(-) diff --git a/crates/socket-patch-cli/src/commands/vendor.rs b/crates/socket-patch-cli/src/commands/vendor.rs index 6dedfaf7e..95dae4266 100644 --- a/crates/socket-patch-cli/src/commands/vendor.rs +++ b/crates/socket-patch-cli/src/commands/vendor.rs @@ -2537,15 +2537,21 @@ impl StagedSource { /// neither the pristine release nor this patch's output. The re-vendor /// then takes the pristine artifact from the lock / registry / service, as /// a lock-only checkout does, instead of reporting it not installed. +/// +/// A live hosted pin at another uuid (`hosted_uuid`) is the same story +/// after a hosted scan (#1105): `uv sync` / `poetry install` from the +/// hosted lock installs that patch's bytes. The pin names no release +/// variant, so it only answers for a sole candidate. fn superseded_install( ledger: &VendorState, candidate: &str, record: &PatchRecord, sole_candidate: bool, + hosted_uuid: Option<&str>, ) -> bool { lookup_entry_kv(&ledger.entries, candidate).is_some_and(|(key, entry)| { entry.uuid != record.uuid && (sole_candidate || key == candidate) - }) + }) || (sole_candidate && hosted_uuid.is_some_and(|uuid| uuid != record.uuid)) } #[allow(clippy::too_many_arguments)] @@ -2605,8 +2611,15 @@ async fn plan_service_downloads( if !variant_matches_installed(Some( &verify_file_patch(dir, file, info).await.status, )) { - if !superseded_install(ledger, candidate, record, candidates.len() == 1) - { + if !superseded_install( + ledger, + candidate, + record, + candidates.len() == 1, + // A hosted-pinned candidate is left out + // below (`takeover_blocked`) either way. + None, + ) { continue; } source_path = &uninstalled; @@ -3093,8 +3106,13 @@ pub(crate) async fn vendor_records_reusing( .await .status, )) { - if !superseded_install(&state, candidate, record, candidates.len() == 1) - { + if !superseded_install( + &state, + candidate, + record, + candidates.len() == 1, + hosted_pin_of(candidate).map(|pin| pin.uuid.as_str()), + ) { continue; } candidate_source = PackageSource::Installed(&uninstalled); @@ -4909,6 +4927,21 @@ mod plan_gate_tests { } } + /// #1105: a live hosted pin at an older uuid marks the installed copy + /// superseded (a hosted sync installed that patch's bytes), so the + /// takeover vendors the newer patch instead of reporting it not + /// installed. The pin names no variant: only a sole candidate counts. + #[test] + fn hosted_pin_at_another_uuid_is_a_superseded_install() { + let ledger = VendorState::default(); + let purl = "pkg:pypi/six@1.16.0"; + let b = record(UUID_B); + assert!(superseded_install(&ledger, purl, &b, true, Some(UUID_A))); + assert!(!superseded_install(&ledger, purl, &b, true, Some(UUID_B))); + assert!(!superseded_install(&ledger, purl, &b, false, Some(UUID_A))); + assert!(!superseded_install(&ledger, purl, &b, true, None)); + } + /// The download plan runs every record through its backend's own gate: /// a package the backend refuses before its first service call is /// never planned, wherever it sits in the loop order. Here the refused diff --git a/crates/socket-patch-cli/tests/mode_migration_pypi.rs b/crates/socket-patch-cli/tests/mode_migration_pypi.rs index 5494b8126..38b2dce32 100644 --- a/crates/socket-patch-cli/tests/mode_migration_pypi.rs +++ b/crates/socket-patch-cli/tests/mode_migration_pypi.rs @@ -839,6 +839,63 @@ async fn poetry_revendors_to_a_superseding_patch_over_a_patched_venv() { ); } +/// #1105: a project hosted at patch A whose venv was then synced from the +/// hosted lock (`uv sync` / `poetry install`), so six holds A's patched +/// bytes, moves to vendored at the superseding patch B. The installed copy +/// matches neither the pristine release nor B's output; it used to be +/// dropped as `package_not_installed` (exit 1, the project left hosted on +/// A) while `scan --mode vendored --dry-run` previewed success. The +/// takeover must restore the upstream entry and vendor B. +#[tokio::test] +async fn hosted_to_vendored_takes_a_superseding_patch_over_a_hosted_venv() { + const UUID_B: &str = "5c3e1a2b-7d4f-4e6a-9b8c-1d2e3f4a5b6d"; + let lanes: [(&str, StageFn); 2] = [("uv", stage_uv_with_sibling), ("poetry", stage_poetry)]; + for (lane, stage) in lanes { + let (_tmp, root) = project(); + let files = stage(&root); + let server = MockServer::start().await; + let hosted_url = mount_hosted_api(&server, true).await; + let (code, env) = hosted_scan(&root, &server); + assert_eq!(code, 0, "{lane}: hosted scan at A: {env:#}"); + assert!( + files.iter().any(|f| std::fs::read_to_string(root.join(f)) + .unwrap() + .contains(&hosted_url)), + "{lane}: hosted mode pins A: {env:#}" + ); + + let venv = root.join("../hosted-venv"); + venv_installed_from_patch_a(&venv); + stage_manifest_with(&root, UUID_B, PATCHED_B); + prebuilt_common::mount_project(&server, &root).await; + mount_pypi_json(&server).await; + let uri = server.uri(); + let pypi = format!("{uri}/pypi"); + let extra = [ + ("VIRTUAL_ENV", venv.to_str().unwrap()), + ("SOCKET_PYPI_JSON_API", pypi.as_str()), + ]; + let args = ["vendor", "--patch-server-url", &uri, "--vendor-url", &uri]; + + let (code, env) = run_cli(&root, &args, &extra); + assert_eq!(code, 0, "{lane}: takeover onto B: {env:#}"); + assert!( + !env.to_string().contains("package_not_installed"), + "{lane}: the installed copy is not reported missing: {env:#}" + ); + let wired: String = files + .iter() + .map(|f| std::fs::read_to_string(root.join(f)).unwrap()) + .collect(); + assert!( + wired.contains(&format!(".socket/vendor/pypi/{UUID_B}/")) + && !wired.contains(&hosted_url) + && !wired.contains(UUID), + "{lane}: wired to vendored B, hosted A gone:\n{wired}" + ); + } +} + const UV_LOCK: &str = r#"version = 1 revision = 2 requires-python = ">=3.9" From a99bceaa1f84d804742bdd345a88cd0f59a9b33c Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 12:44:25 +0000 Subject: [PATCH 3/4] Cover the requirements.txt lane of #1105 The issue also reproduces in the uv pip requirements.txt lane, which goes through the same check, so the regression test runs it too. Assisted-by: Claude Code:claude-opus-5-5 --- .../tests/mode_migration_pypi.rs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/crates/socket-patch-cli/tests/mode_migration_pypi.rs b/crates/socket-patch-cli/tests/mode_migration_pypi.rs index 38b2dce32..026c89302 100644 --- a/crates/socket-patch-cli/tests/mode_migration_pypi.rs +++ b/crates/socket-patch-cli/tests/mode_migration_pypi.rs @@ -840,16 +840,21 @@ async fn poetry_revendors_to_a_superseding_patch_over_a_patched_venv() { } /// #1105: a project hosted at patch A whose venv was then synced from the -/// hosted lock (`uv sync` / `poetry install`), so six holds A's patched -/// bytes, moves to vendored at the superseding patch B. The installed copy -/// matches neither the pristine release nor B's output; it used to be -/// dropped as `package_not_installed` (exit 1, the project left hosted on -/// A) while `scan --mode vendored --dry-run` previewed success. The -/// takeover must restore the upstream entry and vendor B. +/// hosted lock (`uv sync`, `poetry install`, `uv pip install -r`), so six +/// holds A's patched bytes, moves to vendored at the superseding patch B. +/// The installed copy matches neither the pristine release nor B's +/// output; it used to be dropped as `package_not_installed` (exit 1, the +/// project left hosted on A) while `scan --mode vendored --dry-run` +/// previewed success. The takeover must restore the upstream entry and +/// vendor B. #[tokio::test] async fn hosted_to_vendored_takes_a_superseding_patch_over_a_hosted_venv() { const UUID_B: &str = "5c3e1a2b-7d4f-4e6a-9b8c-1d2e3f4a5b6d"; - let lanes: [(&str, StageFn); 2] = [("uv", stage_uv_with_sibling), ("poetry", stage_poetry)]; + let lanes: [(&str, StageFn); 3] = [ + ("uv", stage_uv_with_sibling), + ("poetry", stage_poetry), + ("requirements", stage_requirements), + ]; for (lane, stage) in lanes { let (_tmp, root) = project(); let files = stage(&root); From ae32e918f4327516d5b18ec0b224d3e50342279e Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 12:49:29 +0000 Subject: [PATCH 4/4] Count any hosted pin as a superseded PyPI install A venv synced from an older hosted patch can sit under a lock a later hosted scan already moved to the patch being vendored, and a package can carry hosted pins at two uuids in different files. Treat any live hosted pin as the superseded install, keep the rule to PyPI so gems still refuse a platform gem from their installed dir, and give the download planner the same hosted check as the vendor loop. Assisted-by: Claude Code:claude-opus-5-5 --- .../socket-patch-cli/src/commands/vendor.rs | 49 +++++++++++-------- .../tests/mode_migration_pypi.rs | 1 - 2 files changed, 29 insertions(+), 21 deletions(-) diff --git a/crates/socket-patch-cli/src/commands/vendor.rs b/crates/socket-patch-cli/src/commands/vendor.rs index 95dae4266..8cc2b1a63 100644 --- a/crates/socket-patch-cli/src/commands/vendor.rs +++ b/crates/socket-patch-cli/src/commands/vendor.rs @@ -2538,20 +2538,24 @@ impl StagedSource { /// then takes the pristine artifact from the lock / registry / service, as /// a lock-only checkout does, instead of reporting it not installed. /// -/// A live hosted pin at another uuid (`hosted_uuid`) is the same story -/// after a hosted scan (#1105): `uv sync` / `poetry install` from the -/// hosted lock installs that patch's bytes. The pin names no release -/// variant, so it only answers for a sole candidate. +/// A PyPI install under a live hosted pin (`hosted_pinned`) is the same +/// story after a hosted scan (#1105): `uv sync` / `poetry install` from +/// the hosted lock installs a hosted patch's bytes, whichever generation +/// the pin names now. The pin names no release variant, so it only answers +/// for a sole candidate; gems keep their installed dir, which the gem +/// backend reads to refuse a platform gem. fn superseded_install( ledger: &VendorState, candidate: &str, record: &PatchRecord, sole_candidate: bool, - hosted_uuid: Option<&str>, + hosted_pinned: bool, ) -> bool { lookup_entry_kv(&ledger.entries, candidate).is_some_and(|(key, entry)| { entry.uuid != record.uuid && (sole_candidate || key == candidate) - }) || (sole_candidate && hosted_uuid.is_some_and(|uuid| uuid != record.uuid)) + }) || (sole_candidate + && hosted_pinned + && matches!(Ecosystem::from_purl(candidate), Some(Ecosystem::Pypi))) } #[allow(clippy::too_many_arguments)] @@ -2616,9 +2620,7 @@ async fn plan_service_downloads( candidate, record, candidates.len() == 1, - // A hosted-pinned candidate is left out - // below (`takeover_blocked`) either way. - None, + takeover_blocked(candidate), ) { continue; } @@ -3111,7 +3113,7 @@ pub(crate) async fn vendor_records_reusing( candidate, record, candidates.len() == 1, - hosted_pin_of(candidate).map(|pin| pin.uuid.as_str()), + hosted_pin_of(candidate).is_some(), ) { continue; } @@ -4927,19 +4929,26 @@ mod plan_gate_tests { } } - /// #1105: a live hosted pin at an older uuid marks the installed copy - /// superseded (a hosted sync installed that patch's bytes), so the - /// takeover vendors the newer patch instead of reporting it not - /// installed. The pin names no variant: only a sole candidate counts. + /// #1105: a live hosted pin marks a PyPI install superseded (a hosted + /// sync installed a hosted patch's bytes), so the takeover vendors the + /// record instead of reporting it not installed. The pin names no + /// variant, so only a sole candidate counts, and gems keep their + /// installed dir for the platform-gem refusal. #[test] - fn hosted_pin_at_another_uuid_is_a_superseded_install() { + fn hosted_pin_marks_a_pypi_install_superseded() { let ledger = VendorState::default(); - let purl = "pkg:pypi/six@1.16.0"; + let pypi = "pkg:pypi/six@1.16.0"; let b = record(UUID_B); - assert!(superseded_install(&ledger, purl, &b, true, Some(UUID_A))); - assert!(!superseded_install(&ledger, purl, &b, true, Some(UUID_B))); - assert!(!superseded_install(&ledger, purl, &b, false, Some(UUID_A))); - assert!(!superseded_install(&ledger, purl, &b, true, None)); + assert!(superseded_install(&ledger, pypi, &b, true, true)); + assert!(!superseded_install(&ledger, pypi, &b, false, true)); + assert!(!superseded_install(&ledger, pypi, &b, true, false)); + assert!(!superseded_install( + &ledger, + "pkg:gem/nokogiri@1.16.0", + &b, + true, + true + )); } /// The download plan runs every record through its backend's own gate: diff --git a/crates/socket-patch-cli/tests/mode_migration_pypi.rs b/crates/socket-patch-cli/tests/mode_migration_pypi.rs index 026c89302..0533424c5 100644 --- a/crates/socket-patch-cli/tests/mode_migration_pypi.rs +++ b/crates/socket-patch-cli/tests/mode_migration_pypi.rs @@ -849,7 +849,6 @@ async fn poetry_revendors_to_a_superseding_patch_over_a_patched_venv() { /// vendor B. #[tokio::test] async fn hosted_to_vendored_takes_a_superseding_patch_over_a_hosted_venv() { - const UUID_B: &str = "5c3e1a2b-7d4f-4e6a-9b8c-1d2e3f4a5b6d"; let lanes: [(&str, StageFn); 3] = [ ("uv", stage_uv_with_sibling), ("poetry", stage_poetry),