Skip to content

Commit f6baa30

Browse files
committed
Test apply/rollback refuse a workspace link
End-to-end check through the real binary: a node_modules link to an npm workspace member that shares a patched package's name@version is refused by apply, apply --dry-run and rollback, and the member's own source is never overwritten. Refs #626 Assisted-by: Claude Code:claude-opus-5-5
1 parent 331c478 commit f6baa30

1 file changed

Lines changed: 93 additions & 0 deletions

File tree

‎crates/socket-patch-cli/tests/apply/in_process_npm_multicopy.rs‎

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,3 +397,96 @@ fn apply_and_rollback_reach_both_transitive_only_vlt_store_copies() {
397397
assert_eq!(v["alreadyOriginal"], 1, "envelope={v}");
398398
assert_vlt_copies([&primary, &twin], false, "after rollback");
399399
}
400+
401+
/// #626: a `node_modules/<name>` link to first-party source (an npm
402+
/// workspace member, which a `file:` directory dependency lays out the
403+
/// same way) that shares a patched package's `name@version` is the user's
404+
/// own code, not an installed copy. Agent-mode apply (dry run included)
405+
/// and rollback refuse it with a diagnostic and never overwrite the fork,
406+
/// even though the default mismatch policy would otherwise replace it.
407+
#[cfg(unix)]
408+
#[test]
409+
fn apply_and_rollback_refuse_a_node_modules_link_to_first_party_source() {
410+
let tmp = tempfile::tempdir().unwrap();
411+
let root = tmp.path();
412+
let name = "dupvuln";
413+
let original = b"module.exports = function(){ return 'VULNERABLE'; };\n";
414+
let mut patched = original.to_vec();
415+
patched.extend_from_slice(b"// SOCKET-PATCHED-MULTICOPY\n");
416+
std::fs::write(
417+
root.join("package.json"),
418+
r#"{ "name": "ws-root", "version": "0.0.0", "private": true, "workspaces": ["packages/*"] }"#,
419+
)
420+
.unwrap();
421+
let fork = b"module.exports = 'first-party fork';\n";
422+
let fork_index = write_copy(&root.join("packages").join(name), name, "1.0.0", fork);
423+
std::fs::create_dir_all(root.join("node_modules")).unwrap();
424+
std::os::unix::fs::symlink(
425+
format!("../packages/{name}"),
426+
root.join("node_modules").join(name),
427+
)
428+
.unwrap();
429+
stage_manifest_and_blob(
430+
root,
431+
"pkg:npm/dupvuln@1.0.0",
432+
&git_sha256(original),
433+
&git_sha256(&patched),
434+
&patched,
435+
);
436+
std::fs::write(
437+
root.join(".socket")
438+
.join("blobs")
439+
.join(git_sha256(original)),
440+
original,
441+
)
442+
.unwrap();
443+
444+
let assert_refused = |code: i32, v: &serde_json::Value, stage: &str| {
445+
assert_ne!(code, 0, "{stage}: must fail closed; envelope={v}");
446+
assert!(
447+
v.to_string().contains("outside every node_modules tree"),
448+
"{stage}: the refusal must name the cause; envelope={v}"
449+
);
450+
assert_eq!(
451+
std::fs::read(&fork_index).unwrap(),
452+
fork,
453+
"{stage}: the first-party fork was overwritten"
454+
);
455+
};
456+
457+
let out = Command::new(binary())
458+
.args([
459+
"apply",
460+
"--json",
461+
"--offline",
462+
"--dry-run",
463+
"--ecosystems",
464+
"npm",
465+
"--cwd",
466+
])
467+
.arg(root)
468+
.output()
469+
.expect("run apply --dry-run");
470+
let stdout = String::from_utf8_lossy(&out.stdout).to_string();
471+
let v: serde_json::Value = serde_json::from_str(stdout.trim())
472+
.unwrap_or_else(|e| panic!("apply must emit JSON: {e}; stdout={stdout}"));
473+
assert_refused(out.status.code().unwrap_or(-1), &v, "apply --dry-run");
474+
475+
let (code, v) = run_apply(root);
476+
assert_refused(code, &v, "apply");
477+
478+
// A fork left patched by an apply from before the guard: rollback must
479+
// not write the upstream original over it either.
480+
std::fs::write(&fork_index, &patched).unwrap();
481+
let (code, v) = run_rollback(root);
482+
assert_ne!(code, 0, "rollback must fail closed; envelope={v}");
483+
assert!(
484+
v.to_string().contains("outside every node_modules tree"),
485+
"rollback: envelope={v}"
486+
);
487+
assert_eq!(
488+
std::fs::read(&fork_index).unwrap(),
489+
patched,
490+
"rollback wrote through the link"
491+
);
492+
}

0 commit comments

Comments
 (0)