diff --git a/crates/socket-patch-cli/src/commands/vendor.rs b/crates/socket-patch-cli/src/commands/vendor.rs index e7403890..f9d40028 100644 --- a/crates/socket-patch-cli/src/commands/vendor.rs +++ b/crates/socket-patch-cli/src/commands/vendor.rs @@ -40,7 +40,7 @@ use crate::commands::apply::{representative_file, result_to_event, variant_match use crate::commands::fetch_stage::{stage_vendor_sources_in_memory, MemStageOutcome}; use crate::commands::lock_cli::acquire_or_emit; use crate::commands::vex::{generate_vex_from_manifest_path, VexEmbedArgs}; -use crate::ecosystem_dispatch::{find_packages_for_purls, partition_purls}; +use crate::ecosystem_dispatch::{find_packages_for_rollback, partition_purls}; use crate::json_envelope::{ Command, Envelope, EnvelopeError, PatchAction, PatchEvent, RunWarning, Status, VexSummary, }; @@ -681,7 +681,18 @@ pub(crate) async fn vendor_records( global: common.global, global_prefix: common.global_prefix.clone(), }; - let mut all_packages = find_packages_for_purls( + // Resolve installed packages with the qualified-purl-aware resolver, NOT + // `find_packages_for_purls`: the manifest keys release-variant ecosystems + // (gem `?platform=`, pypi `?artifact_id=`, maven `?classifier=&ext=`) by + // *qualified* purls, but the crawler only knows the *base* purl. + // `find_packages_for_purls` keys the result map by the base purl, so the + // `missing`/`contains_key` check below would miss every installed + // qualified-purl package and falsely classify it "not installed" — + // triggering a spurious `vendor_fetched_missing`, a redundant per-run + // registry download, and (for gem) a HashMap-order platform coin-flip. + // The rollback variant fans each base path back out to every qualified + // manifest purl (same invariant as `find_manifest_package_paths`). + let mut all_packages = find_packages_for_rollback( &vendorable_partition, &crawler_options, common.silent || common.json, diff --git a/crates/socket-patch-cli/src/ecosystem_dispatch.rs b/crates/socket-patch-cli/src/ecosystem_dispatch.rs index d2b44ea4..e218227d 100644 --- a/crates/socket-patch-cli/src/ecosystem_dispatch.rs +++ b/crates/socket-patch-cli/src/ecosystem_dispatch.rs @@ -964,6 +964,53 @@ mod tests { assert_eq!(out.get(&qualified), Some(&pkg_dir)); } + #[tokio::test] + async fn find_packages_for_rollback_resolves_installed_qualified_gem() { + // Regression for the vendor lookup path (vendor.rs): every real + // production gem/pypi patch PURL is QUALIFIED (`?platform=` / + // `?artifact_id=`), but the crawler only knows the BASE PURL. + // `vendor` must resolve installed packages via the qualified-aware + // rollback resolver so its `all_packages.contains_key(qualified)` + // check recognizes the installed gem. Using `find_packages_for_purls` + // (base-keyed) misses the qualified key, falsely classifying the + // installed gem "not installed" — the bug that produced spurious + // `vendor_fetched_missing` events and the gem platform coin-flip. + let tmp = tempfile::tempdir().unwrap(); + // A platform gem installs into a `-` dir (with an + // optional `-` suffix); lay down the plain-platform case. + let gem_dir = tmp.path().join("activestorage-7.0.2.2"); + std::fs::create_dir_all(gem_dir.join("lib")).unwrap(); + + // `global_prefix` makes the gem crawler treat `tmp` as the gems root + // directly (same shortcut the ruby crawler's own tests use). + let options = CrawlerOptions { + cwd: tmp.path().to_path_buf(), + global: false, + global_prefix: Some(tmp.path().to_path_buf()), + }; + + let qualified = "pkg:gem/activestorage@7.0.2.2?platform=ruby".to_string(); + let partitioned = partition_purls(std::slice::from_ref(&qualified), None); + + // The vendor lookup path: the qualified manifest PURL is resolved to + // the installed dir under its EXACT qualified key. + let rollback = find_packages_for_rollback(&partitioned, &options, true).await; + assert_eq!( + rollback.get(&qualified), + Some(&gem_dir), + "installed qualified gem must resolve under its qualified key" + ); + + // The old resolver keyed by the BASE PURL only, so a `contains_key` + // on the qualified PURL missed — the exact false "not installed". + let base_keyed = find_packages_for_purls(&partitioned, &options, true).await; + assert!( + !base_keyed.contains_key(&qualified), + "find_packages_for_purls must NOT be used by vendor: it keys by \ + the base PURL, so the qualified lookup falsely misses" + ); + } + #[tokio::test] async fn dispatch_find_empty_partition_yields_empty_map() { let tmp = tempfile::tempdir().unwrap();