Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
344 changes: 344 additions & 0 deletions crates/socket-patch-cli/tests/in_process_alternate_installers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,350 @@ async fn pnpm_install_then_apply_patches_file() {
assert_patched(&real, &patched, &before_hash, &after_hash);
}

// ---------------------------------------------------------------------------
// pnpm isolated linker: transitive-only dependency in the virtual store
// ---------------------------------------------------------------------------

/// Under pnpm's isolated linker a *transitive-only* dependency has no
/// importer-root entry at all: `node_modules/<dep>` does not exist, and the
/// only physical install lives at `node_modules/.pnpm/<x>/node_modules/<dep>`
/// — runtime-loaded, yet invisible to any walk that skips the hidden `.pnpm`
/// virtual store (apply reported `package_not_installed` on pnpm 7–12).
/// mkdirp@0.5.5 depends on minimist, giving a real pnpm install with exactly
/// that shape. Apply must resolve minimist inside the store and patch the
/// canonical file — while a sibling project sharing the same store stays
/// pristine: the file is hardlink-imported, so only a CoW break (not an
/// in-place write) keeps the store and every other consumer untouched.
#[tokio::test]
#[serial]
async fn pnpm_transitive_only_dep_apply_patches_virtual_store() {
if !has("pnpm") {
println!("SKIP: pnpm not on PATH");
return;
}

let tmp = tempfile::tempdir().expect("create tempdir");
let store = tmp.path().join("store");

let stage_project = |name: &str| -> std::path::PathBuf {
let proj = tmp.path().join(name);
std::fs::create_dir_all(&proj).expect("create project dir");
std::fs::write(
proj.join("package.json"),
format!(
r#"{{ "name": "{name}", "version": "0.0.0", "dependencies": {{ "mkdirp": "0.5.5" }} }}"#
),
)
.expect("write package.json");
proj
};
let proj_a = stage_project("pnpm-iso-a");
let proj_b = stage_project("pnpm-iso-b");

for proj in [&proj_a, &proj_b] {
// Both projects share one store; hardlink import (instead of the
// APFS-clone default) makes each project file share the store
// file's inode — the layout the CoW assertions below are about.
// CLI flags, not project `.npmrc`: pnpm 11 no longer reads these
// settings from `.npmrc` (silently — config get returns undefined).
let out = pm_command("pnpm", &["npm_config_"])
.args([
"install",
"--silent",
"--no-frozen-lockfile",
"--store-dir",
store.to_str().unwrap(),
"--config.package-import-method=hardlink",
])
.current_dir(proj)
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.output()
.expect("pnpm install");
if !out.status.success() {
println!(
"SKIP: pnpm install failed: {}",
String::from_utf8_lossy(&out.stderr)
);
return;
}
}

// Premise: minimist is transitive-only — no importer-root entry (not
// even a symlink). If pnpm ever hoisted it, this test would silently
// stop exercising the virtual-store path and must say so.
assert!(
std::fs::symlink_metadata(proj_a.join("node_modules/minimist")).is_err(),
"pnpm test premise broken: minimist appeared at the importer root; \
the transitive-only virtual-store path is not being exercised"
);

// The lock-resolved minimist lives next to the real mkdirp inside its
// own store entry; canonicalize resolves that (possibly symlinked)
// sibling to its physical store home.
let locate = |proj: &Path| -> std::path::PathBuf {
let mkdirp_real =
std::fs::canonicalize(proj.join("node_modules/mkdirp")).expect("canonicalize mkdirp");
std::fs::canonicalize(mkdirp_real.parent().unwrap().join("minimist"))
.expect("minimist must be installed beside mkdirp in its store entry")
};
let minimist_a = locate(&proj_a);
assert!(
minimist_a.components().any(|c| c.as_os_str() == ".pnpm"),
"premise: minimist's canonical home must be inside the virtual store: {minimist_a:?}"
);
let meta: serde_json::Value = serde_json::from_slice(
&std::fs::read(minimist_a.join("package.json")).expect("read minimist package.json"),
)
.expect("parse minimist package.json");
let version = meta["version"].as_str().expect("version field").to_string();

let target = minimist_a.join("index.js");
let original = std::fs::read(&target).expect("read minimist index.js");
let before_hash = git_sha256(&original);
let mut patched = original.clone();
patched.extend_from_slice(b"\n// SOCKET-PATCH-PNPM-TRANSITIVE-MARKER\n");
let after_hash = git_sha256(&patched);

// The sibling project's canonical copy of the same file.
let target_b = locate(&proj_b).join("index.js");
assert_eq!(
std::fs::read(&target_b).expect("read sibling copy"),
original,
"both projects must start from identical store-imported bytes"
);
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
assert_eq!(
std::fs::metadata(&target)
.expect("stat project A copy")
.ino(),
std::fs::metadata(&target_b)
.expect("stat project B copy")
.ino(),
"hardlink-import premise: both projects' copies must share the store inode"
);
}

let socket = proj_a.join(".socket");
write_manifest(
&socket,
&format!("pkg:npm/minimist@{version}"),
&before_hash,
&after_hash,
);
let blobs = socket.join("blobs");
std::fs::create_dir_all(&blobs).expect("create blobs dir");
std::fs::write(blobs.join(&after_hash), &patched).expect("write patch blob");

let code = apply_run(default_apply(&proj_a)).await;
assert_eq!(
code, 0,
"apply must resolve the transitive-only dep inside .pnpm and succeed"
);
assert_patched(&target, &patched, &before_hash, &after_hash);

// CoW safety: the sibling project sharing the store is untouched, and
// the patched file no longer shares the store inode.
let after_b = std::fs::read(&target_b).expect("read sibling copy");
assert_eq!(
git_sha256(&after_b),
before_hash,
"sibling project sharing the store must keep the original bytes"
);
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
assert_ne!(
std::fs::metadata(&target)
.expect("stat project A copy")
.ino(),
std::fs::metadata(&target_b)
.expect("stat project B copy")
.ino(),
"apply must break the hardlink (CoW) instead of writing through the store inode"
);
}
}

// ---------------------------------------------------------------------------
// pnpm 4 (legacy NESTED virtual store): transitive-only dependency
// ---------------------------------------------------------------------------

/// REAL `corepack pnpm@4.14.4` install (pnpm 4 runs fine on modern Node).
/// Its layoutVersion-3 virtual store is nested by registry host —
/// `.pnpm/registry.npmjs.org/<name>/<version>/node_modules/<name>` — not
/// the flat `.pnpm/<name>@<version>` shape of pnpm 6+, and the flat-entry
/// walk was blind to it: apply exited 0 claiming success while the
/// transitive dep's file was never written (empirically confirmed on a
/// captured pnpm 4.14.4 tree). mkdirp@0.5.5 depends on minimist, giving a
/// real install with exactly that shape. Two projects share one store via
/// hardlink import, so the CoW asserts prove apply broke the link instead
/// of writing through the store inode.
#[tokio::test]
#[serial]
async fn pnpm4_nested_store_transitive_only_dep_apply_patches_file() {
if !has_corepack_pm("pnpm@4.14.4") {
println!("SKIP: corepack pnpm@4.14.4 unavailable");
return;
}

let tmp = tempfile::tempdir().expect("create tempdir");
let store = tmp.path().join("store");

let stage_project = |name: &str| -> std::path::PathBuf {
let proj = tmp.path().join(name);
std::fs::create_dir_all(&proj).expect("create project dir");
std::fs::write(
proj.join("package.json"),
format!(
r#"{{ "name": "{name}", "version": "0.0.0", "dependencies": {{ "mkdirp": "0.5.5" }} }}"#
),
)
.expect("write package.json");
proj
};
let proj_a = stage_project("pnpm4-nested-a");
let proj_b = stage_project("pnpm4-nested-b");

for proj in [&proj_a, &proj_b] {
// `--store-dir` + `--package-import-method hardlink` are the pnpm 4
// spellings (verified against `pnpm@4.14.4 install --help`); both
// projects share the store so the file is hardlink-imported and the
// CoW assertions below have teeth.
let out = pm_command("corepack", &["npm_config_"])
.args([
"pnpm@4.14.4",
"install",
"--store-dir",
store.to_str().unwrap(),
"--package-import-method",
"hardlink",
])
.current_dir(proj)
.env("COREPACK_ENABLE_DOWNLOAD_PROMPT", "0")
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.output()
.expect("corepack pnpm@4.14.4 install");
if !out.status.success() {
println!(
"SKIP: pnpm@4.14.4 install failed: {}",
String::from_utf8_lossy(&out.stderr)
);
return;
}
}

// Layout premises — if any of these drift the test would silently stop
// exercising the nested-store path and must say so instead:
// the store is nested by registry host (layoutVersion 3), …
assert!(
proj_a
.join("node_modules/.pnpm/registry.npmjs.org")
.is_dir(),
"pnpm 4 premise broken: no nested `.pnpm/registry.npmjs.org` host dir"
);
// … and minimist is transitive-only: no importer-root entry at all.
assert!(
std::fs::symlink_metadata(proj_a.join("node_modules/minimist")).is_err(),
"pnpm 4 premise broken: minimist appeared at the importer root; \
the transitive-only nested-store path is not being exercised"
);

// The lock-resolved minimist lives next to the real mkdirp inside its
// own store entry; canonicalize resolves that sibling symlink to its
// physical nested-store home.
let locate = |proj: &Path| -> std::path::PathBuf {
let mkdirp_real =
std::fs::canonicalize(proj.join("node_modules/mkdirp")).expect("canonicalize mkdirp");
std::fs::canonicalize(mkdirp_real.parent().unwrap().join("minimist"))
.expect("minimist must be installed beside mkdirp in its store entry")
};
let minimist_a = locate(&proj_a);
assert!(
minimist_a
.components()
.any(|c| c.as_os_str() == "registry.npmjs.org"),
"premise: minimist's canonical home must be inside the NESTED store: {minimist_a:?}"
);
let meta: serde_json::Value = serde_json::from_slice(
&std::fs::read(minimist_a.join("package.json")).expect("read minimist package.json"),
)
.expect("parse minimist package.json");
let version = meta["version"].as_str().expect("version field").to_string();

let target = minimist_a.join("index.js");
let original = std::fs::read(&target).expect("read minimist index.js");
let before_hash = git_sha256(&original);
let mut patched = original.clone();
patched.extend_from_slice(b"\n// SOCKET-PATCH-PNPM4-NESTED-MARKER\n");
let after_hash = git_sha256(&patched);

// The sibling project's canonical copy of the same file.
let target_b = locate(&proj_b).join("index.js");
assert_eq!(
std::fs::read(&target_b).expect("read sibling copy"),
original,
"both projects must start from identical store-imported bytes"
);
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
assert_eq!(
std::fs::metadata(&target)
.expect("stat project A copy")
.ino(),
std::fs::metadata(&target_b)
.expect("stat project B copy")
.ino(),
"hardlink-import premise: both projects' copies must share the store inode"
);
}

let socket = proj_a.join(".socket");
write_manifest(
&socket,
&format!("pkg:npm/minimist@{version}"),
&before_hash,
&after_hash,
);
let blobs = socket.join("blobs");
std::fs::create_dir_all(&blobs).expect("create blobs dir");
std::fs::write(blobs.join(&after_hash), &patched).expect("write patch blob");

let code = apply_run(default_apply(&proj_a)).await;
assert_eq!(
code, 0,
"apply must resolve the transitive-only dep inside the nested \
`.pnpm/registry.npmjs.org` store and succeed"
);
assert_patched(&target, &patched, &before_hash, &after_hash);

// CoW safety: the sibling project sharing the store is untouched, and
// the patched file no longer shares the store inode.
let after_b = std::fs::read(&target_b).expect("read sibling copy");
assert_eq!(
git_sha256(&after_b),
before_hash,
"sibling project sharing the store must keep the original bytes"
);
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
assert_ne!(
std::fs::metadata(&target)
.expect("stat project A copy")
.ino(),
std::fs::metadata(&target_b)
.expect("stat project B copy")
.ino(),
"apply must break the hardlink (CoW) instead of writing through the store inode"
);
}
}

// ---------------------------------------------------------------------------
// Monorepo workspace (npm workspaces)
// ---------------------------------------------------------------------------
Expand Down
Loading
Loading