From 38867658d2876d993dd5bd0e965000f70c33beee Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 22:30:06 +0000 Subject: [PATCH 1/2] Start fix for #370 Assisted-by: Claude Code:claude-opus-5-5 From 933280e90eae10128ade442192cb8c45b915b49e Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 22:35:26 +0000 Subject: [PATCH 2/2] Read yarnrc compressionLevel past YAML comments A .yarnrc.yml line like `compressionLevel: 0 # keep yarn default` is the yarn default (cacheKey 10c0), but socket-patch read the comment as part of the value and refused the project in both vendored and hosted mode. The reader now parses the value as a YAML scalar: a quoted value ends at its closing quote and a plain value ends before a whitespace-separated `#`. Anything not positively `0` still refuses. Fixes #370 Assisted-by: Claude Code:claude-opus-5-5 --- .../tests/e2e_vendor_yarn_berry_build.rs | 22 +++++-- .../src/patch/redirect/mod.rs | 9 +++ .../src/vendor/yarn_berry_lock.rs | 64 ++++++++++++++++++- 3 files changed, 89 insertions(+), 6 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 71045ba0c..c33839278 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 @@ -265,7 +265,20 @@ fn copy_dir_recursive(src: &Path, dst: &Path) { #[tokio::test(flavor = "multi_thread")] async fn yarn_berry_vendor_fresh_checkout_immutable_check_cache_and_revert() { - run_berry_capstone(VendorDriver::VendorCli).await; + run_berry_capstone(VendorDriver::VendorCli, "").await; +} + +/// #370: `compressionLevel: 0` with a trailing YAML comment is the default +/// to yarn (cacheKey `10c0`), so vendor must not refuse it as a +/// checksum-changing level. The whole capstone runs against that +/// `.yarnrc.yml`, fresh-checkout `--immutable` proof included. +#[tokio::test(flavor = "multi_thread")] +async fn yarn_berry_vendor_commented_default_compression_level() { + run_berry_capstone( + VendorDriver::VendorCli, + "compressionLevel: 0 # keep yarn default\n", + ) + .await; } /// get-driven twin (v3.6): `get --mode vendored` consumes the SAME @@ -277,10 +290,11 @@ async fn yarn_berry_vendor_fresh_checkout_immutable_check_cache_and_revert() { /// the `vendor` front door's contract (the capstone above). #[tokio::test(flavor = "multi_thread")] async fn berry_get_uuid_vendored_fresh_checkout_immutable() { - run_berry_capstone(VendorDriver::GetUuid).await; + run_berry_capstone(VendorDriver::GetUuid, "").await; } -async fn run_berry_capstone(driver: VendorDriver) { +/// `yarnrc_extra` is appended to the capstone's `.yarnrc.yml`. +async fn run_berry_capstone(driver: VendorDriver, yarnrc_extra: &str) { if !has_corepack_pm(yarn_berry()) { skip!( "SKIP e2e_vendor_yarn_berry_build ({driver:?}): `corepack {}` unavailable \ @@ -304,7 +318,7 @@ async fn run_berry_capstone(driver: VendorDriver) { // (the only checksum recipe vendor reproduces offline — spike B4). std::fs::write( proj.join(".yarnrc.yml"), - "nodeLinker: node-modules\nenableGlobalCache: false\n", + format!("nodeLinker: node-modules\nenableGlobalCache: false\n{yarnrc_extra}"), ) .unwrap(); diff --git a/crates/socket-patch-core/src/patch/redirect/mod.rs b/crates/socket-patch-core/src/patch/redirect/mod.rs index 3fdda3257..33aabee4c 100644 --- a/crates/socket-patch-core/src/patch/redirect/mod.rs +++ b/crates/socket-patch-core/src/patch/redirect/mod.rs @@ -14450,6 +14450,15 @@ packages: Err("redirect_yarn_berry_cache_unsupported".to_string()) ); assert_eq!(code(&crlf, Some("compressionLevel: 0\n")), Ok(())); + // #370: a trailing YAML comment is not part of the value. + assert_eq!( + code(&crlf, Some("compressionLevel: 0 # keep yarn default\n")), + Ok(()) + ); + assert_eq!( + code(&crlf, Some("compressionLevel: mixed # smaller cache\n")), + Err("redirect_yarn_berry_cache_unsupported".to_string()) + ); } /// The whole-file gates read the NORMALIZED lock: a CRLF lock at an 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 dcd692c95..c051e38cc 100644 --- a/crates/socket-patch-core/src/vendor/yarn_berry_lock.rs +++ b/crates/socket-patch-core/src/vendor/yarn_berry_lock.rs @@ -1348,11 +1348,25 @@ fn root_workspace_name(blocks: &[LockBlock]) -> Option { /// skipped the way yarn's YAML parser skips it — otherwise a knob on the /// first line of a BOM'd file would read as unset (the offline-reproducible /// default) while yarn applies it and every install fails YN0018. +/// +/// The value is read as a YAML scalar: a quoted value ends at its closing +/// quote, and a plain value ends before a whitespace-separated `#` comment +/// (`compressionLevel: 0 # keep yarn default` is `0`, #370). A `#` with no +/// whitespace before it stays part of a plain value, as in YAML. pub(crate) fn yarnrc_compression_level(rc: &str) -> Option<&str> { let rc = rc.strip_prefix('\u{feff}').unwrap_or(rc); rc.lines().find_map(|line| { - let rest = line.strip_prefix("compressionLevel:")?; - Some(rest.trim().trim_matches(['\'', '"'])) + let rest = line.strip_prefix("compressionLevel:")?.trim(); + if let Some(quote) = rest.chars().next().filter(|c| matches!(c, '\'' | '"')) { + if let Some(end) = rest[1..].find(quote) { + return Some(&rest[1..1 + end]); + } + } + let value = rest + .char_indices() + .find(|&(i, c)| c == '#' && rest[..i].ends_with([' ', '\t'])) + .map_or(rest, |(i, _)| &rest[..i]); + Some(value.trim_end().trim_matches(['\'', '"'])) }) } @@ -1858,6 +1872,21 @@ __metadata: assert!(result.success, "{:?}", result.error); } + /// #370: `compressionLevel: 0` with a trailing YAML comment is the + /// default yarn reads as `0`, so vendoring proceeds rather than refusing. + #[tokio::test] + async fn commented_default_compression_level_vendors() { + let fx = fixture().await; + tokio::fs::write( + fx.root().join(YARNRC), + "nodeLinker: node-modules\ncompressionLevel: 0 # keep yarn default\n", + ) + .await + .unwrap(); + let (result, _, _) = expect_done(fx.vendor(false).await); + assert!(result.success, "{:?}", result.error); + } + #[tokio::test] async fn user_resolutions_entry_is_refused_never_overwritten() { let pkg = B3_BEFORE_PKG.replace( @@ -3963,6 +3992,37 @@ __metadata: ); } + /// A trailing YAML comment is not part of the scalar (#370): yarn reads + /// `compressionLevel: 0 # keep yarn default` as `0`, quoted or not. + #[test] + fn yarnrc_compression_level_drops_a_trailing_comment() { + for (rc, level) in [ + ("compressionLevel: 0 # keep yarn default\n", "0"), + ("compressionLevel: 0\t# tab-separated\r\n", "0"), + ("compressionLevel: 0 #\n", "0"), + ("compressionLevel: \"0\" # quoted\n", "0"), + ("compressionLevel: '0'# quoted, no gap\n", "0"), + ("compressionLevel: mixed # not the default\n", "mixed"), + ("compressionLevel: 9 #\r\n", "9"), + ] { + assert_eq!(yarnrc_compression_level(rc), Some(level), "{rc:?}"); + } + } + + /// A `#` with no whitespace before it is part of a plain scalar in YAML, + /// so `0#x` is not the default and must still refuse (fail closed). + #[test] + fn yarnrc_compression_level_keeps_an_unseparated_hash() { + assert_eq!( + yarnrc_compression_level("compressionLevel: 0#x\n"), + Some("0#x") + ); + assert_eq!( + yarnrc_compression_level("compressionLevel: \"0 # in quotes\"\n"), + Some("0 # in quotes") + ); + } + /// yarn 4.0.x spells `10c0` checksums bare, 4.1+ prefixed: a written /// entry follows the lock (an `--immutable` install rejects a respelled /// checksum with YN0028). A lock with no checksum keeps the prefix.