From 5e0418ed5cf53fc6a04dffc21f65358b36bdcdc7 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 08:54:47 +0000 Subject: [PATCH 1/2] Fix CRLF bin e2e failing under npm 11 on macOS npm_agent_keeps_crlf_shebang_of_bin_npm_did_not_link assumed npm never links a directories.bin-only package installed from a file: tarball. npm 10 doesn't, but npm 11 does: it links bin/cli.js and rewrites its CRLF shebang to LF. The e2e_redirect_npm_build legs use the runner image's npm, which is 11 on macos-latest, so the macOS nightly leg has been red on main since #1805 landed. The test now checks whether npm linked the bin and expects npm's rewritten bytes when it did, and the untouched CRLF bytes when it didn't. apply, apply --check and a byte-exact rollback are still asserted in both cases, and --no-bin-links must still leave the bin unlinked. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_011f9aeRC4w7jsAshzLQM6VL --- .../tests/e2e_redirect_npm_build.rs | 29 ++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/crates/socket-patch-cli/tests/e2e_redirect_npm_build.rs b/crates/socket-patch-cli/tests/e2e_redirect_npm_build.rs index 9529ae9f5..83196fe7c 100644 --- a/crates/socket-patch-cli/tests/e2e_redirect_npm_build.rs +++ b/crates/socket-patch-cli/tests/e2e_redirect_npm_build.rs @@ -1357,10 +1357,12 @@ async fn npm_agent_recovers_crlf_bin_left_by_raw_write() { /// Regression (#1804): npm rewrites the shebang only of bins it links. /// With `--no-bin-links`, or for a `directories.bin`-only package -/// installed from a `file:` tarball (npm doesn't expand that field from a -/// tarball's `package.json`), the installed bin keeps its CRLF shebang. +/// installed from a `file:` tarball (npm 10 doesn't expand that field from +/// a tarball's `package.json`), the installed bin keeps its CRLF shebang. /// `apply` writes the patch's recorded bytes, `apply --check` agrees, and -/// `rollback` restores what `npm install` wrote, byte for byte. +/// `rollback` restores what `npm install` wrote, byte for byte. npm 11 +/// does link the `directories.bin` file and rewrites its shebang; there +/// the same steps must round-trip npm's bytes instead. #[cfg(unix)] #[tokio::test(flavor = "multi_thread")] #[ignore = "wall-bound real-npm install; runs on the e2e CI matrix leg"] @@ -1385,10 +1387,23 @@ async fn npm_agent_keeps_crlf_shebang_of_bin_npm_did_not_link() { let tmp = tempfile::tempdir().unwrap(); let proj = crlf_bin_project_with(tmp.path(), manifest, rel, flags); let cli = proj.join("node_modules/crlfbin").join(rel); + let link = proj + .join("node_modules/.bin") + .join(Path::new(rel).file_name().unwrap()); + let linked = link.symlink_metadata().is_ok(); + assert!( + !(linked && flags.contains(&"--no-bin-links")), + "{rel}: npm linked a bin under --no-bin-links" + ); + let (before, after) = if linked { + (NPM_BIN_BEFORE, NPM_BIN_AFTER) + } else { + (CRLF_BIN_BEFORE, CRLF_BIN_AFTER) + }; assert_eq!( std::fs::read(&cli).unwrap(), - CRLF_BIN_BEFORE, - "{rel}: npm left the shebang alone" + before, + "{rel}: npm rewrites the shebang only if it linked the bin (linked: {linked})" ); let (code, stdout, stderr) = run_socket(&proj, &["apply", "--offline"]); @@ -1396,7 +1411,7 @@ async fn npm_agent_keeps_crlf_shebang_of_bin_npm_did_not_link() { code, 0, "{rel}: apply\nstdout:\n{stdout}\nstderr:\n{stderr}" ); - assert_eq!(std::fs::read(&cli).unwrap(), CRLF_BIN_AFTER, "{rel}: apply"); + assert_eq!(std::fs::read(&cli).unwrap(), after, "{rel}: apply"); let (code, stdout, stderr) = run_socket(&proj, &["apply", "--check", "--offline"]); assert_eq!( @@ -1411,7 +1426,7 @@ async fn npm_agent_keeps_crlf_shebang_of_bin_npm_did_not_link() { ); assert_eq!( std::fs::read(&cli).unwrap(), - CRLF_BIN_BEFORE, + before, "{rel}: rollback is byte-exact" ); } From 9d7d4cbfd7e9bf3ce49a71497051cc99b821c404 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 1 Nov 2026 00:58:43 +0000 Subject: [PATCH 2/2] Probe the CRLF bin e2e's link under npm's link name The --no-bin-links guard looked for .bin/cli.js, but npm names a bin-map link after its key (crlfbin), so the guard could never fire. Pass the link name npm would create for each case: the bin key for the bin map, the file name for directories.bin. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_011f9aeTApLyQQnpuXiK39EX --- .../socket-patch-cli/tests/e2e_redirect_npm_build.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/socket-patch-cli/tests/e2e_redirect_npm_build.rs b/crates/socket-patch-cli/tests/e2e_redirect_npm_build.rs index 83196fe7c..7a44c658d 100644 --- a/crates/socket-patch-cli/tests/e2e_redirect_npm_build.rs +++ b/crates/socket-patch-cli/tests/e2e_redirect_npm_build.rs @@ -1372,24 +1372,26 @@ async fn npm_agent_keeps_crlf_shebang_of_bin_npm_did_not_link() { npm_e2e_common::skip(SUITE, "npm not on PATH"); return; } - for (manifest, rel, flags) in [ + // The link name npm would create: the `bin` key for a `bin` map, the + // file name for a `directories.bin` entry. + for (manifest, rel, link_name, flags) in [ ( r#"{"name":"crlfbin","version":"1.0.0","bin":{"crlfbin":"cli.js"}}"#, "cli.js", + "crlfbin", &["--no-bin-links"][..], ), ( r#"{"name":"crlfbin","version":"1.0.0","directories":{"bin":"bin"}}"#, "bin/cli.js", + "cli.js", &[][..], ), ] { let tmp = tempfile::tempdir().unwrap(); let proj = crlf_bin_project_with(tmp.path(), manifest, rel, flags); let cli = proj.join("node_modules/crlfbin").join(rel); - let link = proj - .join("node_modules/.bin") - .join(Path::new(rel).file_name().unwrap()); + let link = proj.join("node_modules/.bin").join(link_name); let linked = link.symlink_metadata().is_ok(); assert!( !(linked && flags.contains(&"--no-bin-links")),