Skip to content

Commit 31458e6

Browse files
Test cargo takeover over unrewritable deps (#1020) (#1314)
* WIP: Fix cargo vendored to hosted takeover ordering (#1020) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * 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) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent 478ceb4 commit 31458e6

1 file changed

Lines changed: 131 additions & 0 deletions

File tree

‎crates/socket-patch-cli/tests/mode_migration_cargo.rs‎

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1320,3 +1320,134 @@ async fn vendor_over_unrestorable_hosted_pin_is_refused() {
13201320
assert_eq!(read(&proj, "Cargo.lock"), pristine_lock);
13211321
assert!(!vendor_ledger_claims(&proj, &purl));
13221322
}
1323+
1324+
// ── #1020: a takeover the hosted rewriter refuses keeps the vendored patch ──
1325+
// Vendored mode accepts every Cargo.toml spelling of the dependency (it never
1326+
// edits the dependency line); the hosted cargo rewriter refuses some
1327+
// (`redirect_cargo_toml_dep_unrewritable`). A vendored → hosted takeover over
1328+
// such a spelling must leave the purl VENDORED, byte for byte (wiring, ledger
1329+
// entry and committed copy), and the dry run must preview exactly that, not
1330+
// `redirect_would_revert_vendored` + `redirected: 1`.
1331+
#[tokio::test(flavor = "multi_thread")]
1332+
async fn takeover_over_unrewritable_spelling_keeps_vendored() {
1333+
for (tag, spelling) in [
1334+
("dotted", format!("{DEP}.version = \"1.0\"\n")),
1335+
("literal", format!("{DEP} = '1.0'\n")),
1336+
(
1337+
"crates-io",
1338+
format!("{DEP} = {{ version = \"1.0\", registry = \"crates-io\" }}\n"),
1339+
),
1340+
] {
1341+
let tmp = tempfile::tempdir().unwrap();
1342+
let Some((proj, cargo_home, version, crate_dir)) = stage_fixture(tmp.path()) else {
1343+
return;
1344+
};
1345+
let toml = read(&proj, "Cargo.toml").replace(&format!("{DEP} = \"1.0\"\n"), &spelling);
1346+
assert!(toml.contains(&spelling), "{tag}: {toml}");
1347+
std::fs::write(proj.join("Cargo.toml"), toml).unwrap();
1348+
std::fs::write(
1349+
proj.join("src/main.rs"),
1350+
format!(
1351+
"fn main() {{ println!(\"M:{{}}\", {}::socket_patched()); }}\n",
1352+
DEP.replace('-', "_")
1353+
),
1354+
)
1355+
.unwrap();
1356+
let purl = format!("pkg:cargo/{DEP}@{version}");
1357+
let orig = std::fs::read(crate_dir.join("src/lib.rs")).unwrap();
1358+
let patched: Vec<u8> = [orig.as_slice(), PATCH_SUFFIX.as_bytes()].concat();
1359+
stage_patch(&proj, &purl, &orig, &patched);
1360+
1361+
let (code, stdout, stderr) = run_socket(
1362+
&proj,
1363+
&[
1364+
"vendor",
1365+
"--json",
1366+
"--offline",
1367+
"--cwd",
1368+
proj.to_str().unwrap(),
1369+
],
1370+
&cargo_home,
1371+
);
1372+
assert_eq!(code, 0, "{tag}: vendor failed: {stdout}\n{stderr}");
1373+
assert!(vendor_ledger_claims(&proj, &purl), "{tag}: vendored");
1374+
assert_build_ok(
1375+
&format!("{tag}: vendored cargo build --locked --offline"),
1376+
&cargo(
1377+
&proj,
1378+
&["build", "--locked", "--offline", "-q"],
1379+
&cargo_home,
1380+
),
1381+
);
1382+
1383+
let server = MockServer::start().await;
1384+
let crate_bytes =
1385+
build_patched_crate(&tmp.path().join("stage"), &crate_dir, &version, &patched);
1386+
mount_hosted_mocks(&server, &purl, &version, &crate_bytes, &orig, &patched).await;
1387+
let uri = server.uri();
1388+
let hosted_args = [
1389+
"scan",
1390+
"--mode",
1391+
"hosted",
1392+
"--json",
1393+
"--yes",
1394+
"--cwd",
1395+
proj.to_str().unwrap(),
1396+
"--api-url",
1397+
&uri,
1398+
"--org",
1399+
ORG,
1400+
"--api-token",
1401+
"fake",
1402+
];
1403+
let before = project_snapshot(&proj);
1404+
let dry: Vec<&str> = hosted_args.iter().copied().chain(["--dry-run"]).collect();
1405+
let (dry_code, dry_out, stderr) = run_socket(&proj, &dry, &cargo_home);
1406+
let dry_env: serde_json::Value = serde_json::from_str(&dry_out)
1407+
.unwrap_or_else(|e| panic!("{tag}: dry-run json ({e}): {dry_out}\n{stderr}"));
1408+
assert_eq!(dry_env["redirect"]["redirected"], 0, "{tag}: {dry_out}");
1409+
assert!(
1410+
!dry_out.contains("redirect_would_revert_vendored"),
1411+
"{tag}: the dry run must not preview a takeover the wet run refuses: {dry_out}"
1412+
);
1413+
assert!(
1414+
dry_out.contains("redirect_takeover_kept_vendored")
1415+
&& dry_out.contains("redirect_cargo_toml_dep_unrewritable"),
1416+
"{tag}: the dry run previews the refusal: {dry_out}"
1417+
);
1418+
assert!(project_snapshot(&proj) == before, "{tag}: dry run wrote");
1419+
1420+
let (code, stdout, stderr) = run_socket(&proj, &hosted_args, &cargo_home);
1421+
let env: serde_json::Value = serde_json::from_str(&stdout)
1422+
.unwrap_or_else(|e| panic!("{tag}: wet json ({e}): {stdout}\n{stderr}"));
1423+
assert_eq!(code, dry_code, "{tag}: dry run and wet run agree: {stdout}");
1424+
assert_eq!(env["status"], dry_env["status"], "{tag}: {stdout}");
1425+
assert_eq!(env["redirect"]["redirected"], 0, "{tag}: {stdout}");
1426+
assert!(
1427+
!stdout.contains("redirect_takeover_unpatched")
1428+
&& !stdout.contains("redirect_takeover_reverted_vendored"),
1429+
"{tag}: the vendored patch must not be reverted: {stdout}"
1430+
);
1431+
assert!(
1432+
stdout.contains("redirect_takeover_kept_vendored")
1433+
&& stdout.contains("redirect_cargo_toml_dep_unrewritable"),
1434+
"{tag}: the refusal is reported: {stdout}"
1435+
);
1436+
let after = project_snapshot(&proj);
1437+
let changed: Vec<&String> = before
1438+
.keys()
1439+
.chain(after.keys())
1440+
.filter(|k| before.get(*k) != after.get(*k))
1441+
.collect();
1442+
assert!(
1443+
changed.is_empty(),
1444+
"{tag}: a refused takeover leaves the vendored project byte for byte: {changed:?}\n{stdout}"
1445+
);
1446+
// A fresh checkout still builds the PATCHED crate.
1447+
let (fresh, home) = fresh_checkout(&proj, tmp.path(), tag);
1448+
assert_build_ok(
1449+
&format!("{tag}: fresh cargo build --locked"),
1450+
&cargo(&fresh, &["build", "--locked", "-q"], &home),
1451+
);
1452+
}
1453+
}

0 commit comments

Comments
 (0)