From 87b4a75a2463b1e24f9e62dddc064a8e71f203ba Mon Sep 17 00:00:00 2001 From: Mikola Lysenko Date: Fri, 9 Oct 2026 12:42:55 -0400 Subject: [PATCH 1/2] WIP: Fix cargo vendored to hosted takeover ordering (#1020) Co-Authored-By: Claude Opus 5.5 (1M context) From e18a8af264902eaedb8692e26cac6db144881e88 Mon Sep 17 00:00:00 2001 From: Mikola Lysenko Date: Fri, 9 Oct 2026 12:46:17 -0400 Subject: [PATCH 2/2] Test cargo takeover over unrewritable deps A vendored cargo crate whose Cargo.toml spelling the hosted rewriter refuses (dotted keys, a single-quoted literal, registry = "crates-io") must stay vendored when scan --mode hosted runs over it, and the dry run must preview that refusal instead of a takeover. The atomic takeover from #1039 already does this; pin it with a real-cargo e2e so the crate can never again land in neither mode (#1020). Co-Authored-By: Claude Opus 5.5 (1M context) --- .../tests/mode_migration_cargo.rs | 131 ++++++++++++++++++ 1 file changed, 131 insertions(+) diff --git a/crates/socket-patch-cli/tests/mode_migration_cargo.rs b/crates/socket-patch-cli/tests/mode_migration_cargo.rs index f3df21e24..46152b450 100644 --- a/crates/socket-patch-cli/tests/mode_migration_cargo.rs +++ b/crates/socket-patch-cli/tests/mode_migration_cargo.rs @@ -1320,3 +1320,134 @@ async fn vendor_over_unrestorable_hosted_pin_is_refused() { assert_eq!(read(&proj, "Cargo.lock"), pristine_lock); assert!(!vendor_ledger_claims(&proj, &purl)); } + +// ── #1020: a takeover the hosted rewriter refuses keeps the vendored patch ── +// Vendored mode accepts every Cargo.toml spelling of the dependency (it never +// edits the dependency line); the hosted cargo rewriter refuses some +// (`redirect_cargo_toml_dep_unrewritable`). A vendored → hosted takeover over +// such a spelling must leave the purl VENDORED, byte for byte (wiring, ledger +// entry and committed copy), and the dry run must preview exactly that, not +// `redirect_would_revert_vendored` + `redirected: 1`. +#[tokio::test(flavor = "multi_thread")] +async fn takeover_over_unrewritable_spelling_keeps_vendored() { + for (tag, spelling) in [ + ("dotted", format!("{DEP}.version = \"1.0\"\n")), + ("literal", format!("{DEP} = '1.0'\n")), + ( + "crates-io", + format!("{DEP} = {{ version = \"1.0\", registry = \"crates-io\" }}\n"), + ), + ] { + let tmp = tempfile::tempdir().unwrap(); + let Some((proj, cargo_home, version, crate_dir)) = stage_fixture(tmp.path()) else { + return; + }; + let toml = read(&proj, "Cargo.toml").replace(&format!("{DEP} = \"1.0\"\n"), &spelling); + assert!(toml.contains(&spelling), "{tag}: {toml}"); + std::fs::write(proj.join("Cargo.toml"), toml).unwrap(); + std::fs::write( + proj.join("src/main.rs"), + format!( + "fn main() {{ println!(\"M:{{}}\", {}::socket_patched()); }}\n", + DEP.replace('-', "_") + ), + ) + .unwrap(); + let purl = format!("pkg:cargo/{DEP}@{version}"); + let orig = std::fs::read(crate_dir.join("src/lib.rs")).unwrap(); + let patched: Vec = [orig.as_slice(), PATCH_SUFFIX.as_bytes()].concat(); + stage_patch(&proj, &purl, &orig, &patched); + + let (code, stdout, stderr) = run_socket( + &proj, + &[ + "vendor", + "--json", + "--offline", + "--cwd", + proj.to_str().unwrap(), + ], + &cargo_home, + ); + assert_eq!(code, 0, "{tag}: vendor failed: {stdout}\n{stderr}"); + assert!(vendor_ledger_claims(&proj, &purl), "{tag}: vendored"); + assert_build_ok( + &format!("{tag}: vendored cargo build --locked --offline"), + &cargo( + &proj, + &["build", "--locked", "--offline", "-q"], + &cargo_home, + ), + ); + + let server = MockServer::start().await; + let crate_bytes = + build_patched_crate(&tmp.path().join("stage"), &crate_dir, &version, &patched); + mount_hosted_mocks(&server, &purl, &version, &crate_bytes, &orig, &patched).await; + let uri = server.uri(); + let hosted_args = [ + "scan", + "--mode", + "hosted", + "--json", + "--yes", + "--cwd", + proj.to_str().unwrap(), + "--api-url", + &uri, + "--org", + ORG, + "--api-token", + "fake", + ]; + let before = project_snapshot(&proj); + let dry: Vec<&str> = hosted_args.iter().copied().chain(["--dry-run"]).collect(); + let (dry_code, dry_out, stderr) = run_socket(&proj, &dry, &cargo_home); + let dry_env: serde_json::Value = serde_json::from_str(&dry_out) + .unwrap_or_else(|e| panic!("{tag}: dry-run json ({e}): {dry_out}\n{stderr}")); + assert_eq!(dry_env["redirect"]["redirected"], 0, "{tag}: {dry_out}"); + assert!( + !dry_out.contains("redirect_would_revert_vendored"), + "{tag}: the dry run must not preview a takeover the wet run refuses: {dry_out}" + ); + assert!( + dry_out.contains("redirect_takeover_kept_vendored") + && dry_out.contains("redirect_cargo_toml_dep_unrewritable"), + "{tag}: the dry run previews the refusal: {dry_out}" + ); + assert!(project_snapshot(&proj) == before, "{tag}: dry run wrote"); + + let (code, stdout, stderr) = run_socket(&proj, &hosted_args, &cargo_home); + let env: serde_json::Value = serde_json::from_str(&stdout) + .unwrap_or_else(|e| panic!("{tag}: wet json ({e}): {stdout}\n{stderr}")); + assert_eq!(code, dry_code, "{tag}: dry run and wet run agree: {stdout}"); + assert_eq!(env["status"], dry_env["status"], "{tag}: {stdout}"); + assert_eq!(env["redirect"]["redirected"], 0, "{tag}: {stdout}"); + assert!( + !stdout.contains("redirect_takeover_unpatched") + && !stdout.contains("redirect_takeover_reverted_vendored"), + "{tag}: the vendored patch must not be reverted: {stdout}" + ); + assert!( + stdout.contains("redirect_takeover_kept_vendored") + && stdout.contains("redirect_cargo_toml_dep_unrewritable"), + "{tag}: the refusal is reported: {stdout}" + ); + let after = project_snapshot(&proj); + let changed: Vec<&String> = before + .keys() + .chain(after.keys()) + .filter(|k| before.get(*k) != after.get(*k)) + .collect(); + assert!( + changed.is_empty(), + "{tag}: a refused takeover leaves the vendored project byte for byte: {changed:?}\n{stdout}" + ); + // A fresh checkout still builds the PATCHED crate. + let (fresh, home) = fresh_checkout(&proj, tmp.path(), tag); + assert_build_ok( + &format!("{tag}: fresh cargo build --locked"), + &cargo(&fresh, &["build", "--locked", "-q"], &home), + ); + } +}