From 51aae35284efd5c890929f16fa26668fcfc84e89 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 00:22:32 +0000 Subject: [PATCH 1/3] Start fix for #783 Assisted-by: Claude Code:claude-opus-5-5 From 5bb4e96d4e07bc7e0c4e1486f3d3758be17933de Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 00:30:05 +0000 Subject: [PATCH 2/3] Refuse vendoring beside parent-scoped resolutions A yarn berry project that pins a package for one workspace with a parent-scoped resolutions entry (for example "pkg-a/left-pad") used to get a vendored "success" that broke every `yarn install --immutable` with YN0028, and a plain install silently dropped the patch. The vendored conflict gate now names each resolutions selector the same way hosted mode does, so parent-scoped and glob selectors that override the patched package refuse with vendor_override_conflict and nothing is written. The bare exact-version takeover is unchanged. Fixes #783 Assisted-by: Claude Code:claude-opus-5-5 --- .../tests/e2e_vendor_yarn_berry_build.rs | 87 +++++++++++++++++++ .../src/vendor/yarn_berry_lock.rs | 48 ++++++++-- 2 files changed, 130 insertions(+), 5 deletions(-) diff --git a/crates/socket-patch-cli/tests/e2e_vendor_yarn_berry_build.rs b/crates/socket-patch-cli/tests/e2e_vendor_yarn_berry_build.rs index f56f2e289..741b63d8d 100644 --- a/crates/socket-patch-cli/tests/e2e_vendor_yarn_berry_build.rs +++ b/crates/socket-patch-cli/tests/e2e_vendor_yarn_berry_build.rs @@ -1056,3 +1056,90 @@ fn run_revert_after_lock_change(change: LockChange) { "the reverted project installs the registry bytes" ); } + +/// #783: a user's parent-scoped `resolutions` selector for the patched +/// package (`pkg-a/left-pad`) is an override vendoring must not stack its +/// own bare pin beside. Vendoring used to report success and leave a lock +/// every `yarn install --immutable` rejected (YN0028). It now refuses with +/// `vendor_override_conflict` and writes nothing, so the project still +/// installs immutably. +#[test] +fn issue_783_parent_scoped_resolution_refuses_vendoring() { + let pm = yarn_berry(); + if !has_corepack_pm(pm) { + skip!("SKIP e2e_vendor_yarn_berry_build (#783): `corepack {pm}` unavailable"); + return; + } + for selector in ["pkg-a/left-pad", "pkg-a/left-pad@^1.3.0"] { + let tmp = tempfile::tempdir().unwrap(); + let proj = tmp.path().join("proj"); + let global = tmp.path().join("yarn-global"); + let env = [("YARN_GLOBAL_FOLDER", global.to_str().unwrap())]; + std::fs::create_dir_all(proj.join("packages/pkg-a")).unwrap(); + std::fs::write( + proj.join("package.json"), + format!( + r#"{{"name":"root","private":true,"workspaces":["packages/*"],"resolutions":{{"{selector}":"{DEP_VERSION}"}}}}"# + ), + ) + .unwrap(); + std::fs::write( + proj.join("packages/pkg-a/package.json"), + format!(r#"{{"name":"pkg-a","version":"1.0.0","dependencies":{{"{DEP}":"^{DEP_VERSION}"}}}}"#), + ) + .unwrap(); + std::fs::write( + proj.join(".yarnrc.yml"), + "nodeLinker: node-modules\nenableGlobalCache: false\n", + ) + .unwrap(); + let out = corepack(&proj, pm, &["install"], &env); + if !out.status.success() { + skip!( + "SKIP e2e_vendor_yarn_berry_build (#783 {selector}): fixture install failed:\n{}", + yarn_berry_common::yarn_output(&out) + ); + return; + } + let pkg_before = std::fs::read(proj.join("package.json")).unwrap(); + let lock_before = std::fs::read(proj.join("yarn.lock")).unwrap(); + + let orig = std::fs::read(proj.join("node_modules").join(DEP).join("index.js")).unwrap(); + let patched: Vec = [MARKER.as_bytes(), orig.as_slice()].concat(); + stage_patch( + &proj, + &format!("pkg:npm/{DEP}@{DEP_VERSION}"), + "package/index.js", + &orig, + &patched, + ); + let cwd = proj.to_str().unwrap(); + let (code, stdout, stderr) = + run_socket(&proj, &["vendor", "--json", "--offline", "--cwd", cwd]); + assert_eq!( + code, 1, + "{selector}: vendor must refuse.\nstdout:\n{stdout}\nstderr:\n{stderr}" + ); + assert!( + stdout.contains("vendor_override_conflict"), + "{selector}: refusal code.\nstdout:\n{stdout}" + ); + assert_eq!( + std::fs::read(proj.join("package.json")).unwrap(), + pkg_before, + "{selector}: package.json untouched" + ); + assert_eq!( + std::fs::read(proj.join("yarn.lock")).unwrap(), + lock_before, + "{selector}: yarn.lock untouched" + ); + + let out = corepack(&proj, pm, &["install", "--immutable"], &env); + assert!( + out.status.success(), + "{selector}: the project must still install immutably:\n{}", + yarn_berry_common::yarn_output(&out) + ); + } +} diff --git a/crates/socket-patch-core/src/vendor/yarn_berry_lock.rs b/crates/socket-patch-core/src/vendor/yarn_berry_lock.rs index 271afdee2..aa31870c6 100644 --- a/crates/socket-patch-core/src/vendor/yarn_berry_lock.rs +++ b/crates/socket-patch-core/src/vendor/yarn_berry_lock.rs @@ -589,7 +589,8 @@ fn root_workspace_gate(blocks: &[LockBlock]) -> Result, name: &str, @@ -604,10 +605,10 @@ fn resolutions_gate( ))); }; for (selector, value) in res_obj { - let sel_name = split_pattern(selector) - .map(|(n, _)| n) - .unwrap_or(selector.as_str()); - if sel_name != name { + // The package the selector overrides, through any parent scope + // (`pkg-a/left-pad`, `**/left-pad`): yarn applies those before + // our bare pin, so they conflict just like a bare entry (#783). + if crate::formats::yarn::patterns::resolution_selector_target(selector) != Some(name) { continue; } // Our own (possibly stale-uuid) entry is fine to overwrite; a @@ -2302,6 +2303,43 @@ __metadata: assert_eq!(tokio::fs::read(fx.pkg_path()).await.unwrap(), fx.pkg_bytes); } + /// #783: a parent-scoped or glob selector that overrides the target + /// (`pkg-a/left-pad`, `pkg-a/left-pad@^1.3.0`, `/left-pad`, + /// `@scope/parent/left-pad`, `**/left-pad`) is a user-authored override + /// too. Yarn applies it before our bare pin, so vendoring beside it + /// leaves a lock `--immutable` rejects: refuse and write nothing. A + /// selector naming the target only as a PARENT overrides another + /// package and still vendors. + #[tokio::test] + async fn parent_scoped_resolutions_selector_is_refused() { + for selector in [ + "pkg-a/left-pad", + "pkg-a/left-pad@^1.3.0", + "vendor-spike/left-pad", + "@scope/parent/left-pad", + "**/left-pad", + ] { + let pkg = B3_BEFORE_PKG.replace( + " }\n}", + &format!(" }},\n \"resolutions\": {{\n \"{selector}\": \"1.3.0\"\n }}\n}}"), + ); + assert_ne!(pkg, B3_BEFORE_PKG, "the fixture edit must hit"); + let fx = fixture_with(&pkg, B3_BEFORE_LOCK).await; + let detail = expect_refused(fx.vendor(false).await, "vendor_override_conflict"); + assert!(detail.contains(selector), "{selector}: {detail}"); + fx.assert_untouched().await; + } + + let pkg = B3_BEFORE_PKG.replace( + " }\n}", + " },\n \"resolutions\": {\n \"left-pad/is-odd\": \"1.0.0\"\n }\n}", + ); + let fx = fixture_with(&pkg, B3_BEFORE_LOCK).await; + let (result, entry, _) = expect_done(fx.vendor(false).await); + assert!(result.success, "{:?}", result.error); + assert!(entry.is_some()); + } + /// A user-authored BARE-name pin to the exact version being vendored is /// taken over: the value moves to our spec, the wiring records the pin /// as `original`, and revert restores it (table kept). Range-keyed From c5524a1719856dc6ef01f55572d7364f31ccbacc Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 00:55:37 +0000 Subject: [PATCH 3/3] Refuse parent-scoped vendor-path resolutions too Vendoring only writes the bare package key, so a parent-scoped resolutions selector that happens to point into .socket/vendor is not ours to skip: it now refuses like any other parent-scoped override. The refusal message now says that only a bare exact-version pin is taken over. The #783 e2e keeps checking the remaining selector when one fixture install is skipped. Assisted-by: Claude Code:claude-opus-5-5 --- .../tests/e2e_vendor_yarn_berry_build.rs | 2 +- .../src/vendor/yarn_berry_lock.rs | 30 ++++++++++++++----- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/crates/socket-patch-cli/tests/e2e_vendor_yarn_berry_build.rs b/crates/socket-patch-cli/tests/e2e_vendor_yarn_berry_build.rs index 741b63d8d..14fa7489c 100644 --- a/crates/socket-patch-cli/tests/e2e_vendor_yarn_berry_build.rs +++ b/crates/socket-patch-cli/tests/e2e_vendor_yarn_berry_build.rs @@ -1099,7 +1099,7 @@ fn issue_783_parent_scoped_resolution_refuses_vendoring() { "SKIP e2e_vendor_yarn_berry_build (#783 {selector}): fixture install failed:\n{}", yarn_berry_common::yarn_output(&out) ); - return; + continue; } let pkg_before = std::fs::read(proj.join("package.json")).unwrap(); let lock_before = std::fs::read(proj.join("yarn.lock")).unwrap(); diff --git a/crates/socket-patch-core/src/vendor/yarn_berry_lock.rs b/crates/socket-patch-core/src/vendor/yarn_berry_lock.rs index aa31870c6..20a6f0333 100644 --- a/crates/socket-patch-core/src/vendor/yarn_berry_lock.rs +++ b/crates/socket-patch-core/src/vendor/yarn_berry_lock.rs @@ -612,10 +612,13 @@ fn resolutions_gate( continue; } // Our own (possibly stale-uuid) entry is fine to overwrite; a - // user-authored override is never clobbered silently. - let ours = value - .as_str() - .is_some_and(|v| parse_vendor_path(v).is_some_and(|p| p.eco == "npm")); + // user-authored override is never clobbered silently. Vendoring + // only ever writes the bare `name` key, so a parent-scoped + // selector pointing into .socket/vendor is not ours. + let ours = selector == name + && value + .as_str() + .is_some_and(|v| parse_vendor_path(v).is_some_and(|p| p.eco == "npm")); if ours { continue; } @@ -627,9 +630,9 @@ fn resolutions_gate( "vendor_override_conflict", format!( "{PACKAGE_JSON} already has a resolutions entry for `{selector}` \ - ({value}); vendor will not overwrite a user-authored override (an \ - exact-version pin `\"{name}\": \"{version}\"` is taken over \ - automatically)" + ({value}); vendor will not overwrite a user-authored override (only \ + a bare exact-version pin `\"{name}\": \"{version}\"` is taken over \ + automatically; parent-scoped selectors always refuse)" ), ))); } @@ -2330,6 +2333,19 @@ __metadata: fx.assert_untouched().await; } + // Vendoring only writes the bare key, so a parent-scoped selector + // pointing into .socket/vendor (hand-copied, or another uuid) is + // not ours to overwrite: it still refuses. + let pkg = B3_BEFORE_PKG.replace( + " }\n}", + &format!( + " }},\n \"resolutions\": {{\n \"pkg-a/left-pad\": \"file:./.socket/vendor/npm/{UUID}/left-pad-1.3.0.tgz\"\n }}\n}}" + ), + ); + let fx = fixture_with(&pkg, B3_BEFORE_LOCK).await; + expect_refused(fx.vendor(false).await, "vendor_override_conflict"); + fx.assert_untouched().await; + let pkg = B3_BEFORE_PKG.replace( " }\n}", " },\n \"resolutions\": {\n \"left-pad/is-odd\": \"1.0.0\"\n }\n}",