From 2cadffbede732b2aa12f9f29183d7fb51c5595c5 Mon Sep 17 00:00:00 2001 From: Mikola Lysenko Date: Thu, 23 Jul 2026 18:24:10 -0400 Subject: [PATCH] fix(redirect): fail closed on non-npm: berry descriptors + yarn layering regression suite MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit rewrite_yarn_berry matched lock blocks by descriptor NAME only, so a hosted redirect of a package that yarn berry builtin-patches (fsevents, resolve, typescript) spliced an `npm:` resolution + checksum under the `patch:`-protocol key — a corrupted key/resolution pairing in exactly the 2026-07 strapi incident's error family, emitted with no warning. Now non-`npm:` descriptor ranges (patch:/workspace:/portal:/link:) are skipped byte-identically with a redirect_yarn_berry_unsupported_protocol warning, mirroring the vendor backend's fail-closed gate (vendor/yarn_berry_lock.rs). New vendor::yarn_layering_tests (in-crate #[cfg(test)] module, since the core-dedups cleanup privatized the vendor wiring internals it plumbs; inventory_npm_lock widened to pub(crate) for the same reason) pins the incident's flows on a strapi-shaped yarn v1 fixture (multi-version ansi-regex + fsevents + resolve) and a yarn-4 berry fixture with real builtin patch: entries — 9 tests, each RED-verified: - vendored wiring is byte-surgical: untouched sibling/builtin-target blocks byte-identical, zero `patch:` strings introduced, file still parseable by the crate's own inventory - integrity chain: #sha1 fragment, sha512 SRI, and ledger sha256/size all recomputed from the on-disk vendored tarball - hosted redirect layered over vendored wiring records the vendored block as `original` (reversible), is byte-surgical, and re-runs are no-ops - vendor revert after a hosted overlay: drift-skipped and lossy (deletes the blob dir, lockfile stays hosted) — pinned as current behavior - berry builtin patch: entries survive vendored wiring AND hosted redirect byte-identically; redirecting the builtin-patched package itself now skips its patch: entry with the new warning Co-Authored-By: Claude Fable 5 --- .../src/patch/redirect/mod.rs | 20 + .../src/patch/vendor/lock_inventory.rs | 4 +- .../socket-patch-core/src/patch/vendor/mod.rs | 2 + .../src/patch/vendor/yarn_layering_tests.rs | 1030 +++++++++++++++++ 4 files changed, 1055 insertions(+), 1 deletion(-) create mode 100644 crates/socket-patch-core/src/patch/vendor/yarn_layering_tests.rs diff --git a/crates/socket-patch-core/src/patch/redirect/mod.rs b/crates/socket-patch-core/src/patch/redirect/mod.rs index 24ff536..55580a2 100644 --- a/crates/socket-patch-core/src/patch/redirect/mod.rs +++ b/crates/socket-patch-core/src/patch/redirect/mod.rs @@ -987,6 +987,26 @@ fn rewrite_yarn_berry( if !version_re.is_match(block) { continue; } + // Descriptor ranges carry a protocol; only an `npm:` range names + // a registry tarball this rewriter can own. A `patch:` range + // (yarn's OWN builtin compat patches — the 2026-07 strapi + // incident family), `workspace:`, `portal:`, or `link:` block + // must survive byte-identically: splicing an npm resolution + // under such a key corrupts the key/resolution protocol pairing. + // Mirrors the vendor backend's fail-closed gate + // (vendor/yarn_berry_lock.rs). + if !parsed.iter().all(|p| p.unwrap().1.starts_with("npm:")) { + result.warnings.push(RewriteWarning { + code: "redirect_yarn_berry_unsupported_protocol".into(), + detail: format!( + "lock entry `{raw_key}` resolves {fname}@{} through a protocol \ + the hosted redirect cannot own (workspace:/patch:/portal:/link:); \ + leaving it byte-identical", + dep.version + ), + }); + continue; + } // Rewrite the resolution wholesale from name+version — handles a // pre-existing `::__archiveUrl=` (custom-registry lock) for free. let resolution = format!( diff --git a/crates/socket-patch-core/src/patch/vendor/lock_inventory.rs b/crates/socket-patch-core/src/patch/vendor/lock_inventory.rs index 3d5aaa2..be49830 100644 --- a/crates/socket-patch-core/src/patch/vendor/lock_inventory.rs +++ b/crates/socket-patch-core/src/patch/vendor/lock_inventory.rs @@ -98,7 +98,9 @@ impl LockfileEntry { /// Inventory the project's npm-family lockfile. Routes by /// [`detect_npm_lock_flavor`] (PnP markers, bun.lockb, unsupported lock /// versions, and a missing lockfile all yield `None`). -async fn inventory_npm_lock(project_root: &Path) -> Option<(NpmLockFlavor, Vec)> { +pub(crate) async fn inventory_npm_lock( + project_root: &Path, +) -> Option<(NpmLockFlavor, Vec)> { // Rush monorepos have no root package.json/lock pair; their single // pnpm source-of-truth lives under common/config/rush/. The flavor // probe (root-relative) can't see it, so fall back explicitly when the diff --git a/crates/socket-patch-core/src/patch/vendor/mod.rs b/crates/socket-patch-core/src/patch/vendor/mod.rs index 2ffc5a4..b11cc10 100644 --- a/crates/socket-patch-core/src/patch/vendor/mod.rs +++ b/crates/socket-patch-core/src/patch/vendor/mod.rs @@ -75,6 +75,8 @@ mod toml_surgery; pub(crate) mod verify; pub(crate) mod yarn_berry_lock; mod yarn_classic_lock; +#[cfg(test)] +mod yarn_layering_tests; pub use path::{ecosystem_dir_for_purl, parse_vendor_path}; pub use state::{load_state, lookup_entry, save_state, VendorEntry, VendorState, VENDOR_STATE_REL}; diff --git a/crates/socket-patch-core/src/patch/vendor/yarn_layering_tests.rs b/crates/socket-patch-core/src/patch/vendor/yarn_layering_tests.rs new file mode 100644 index 0000000..8f19a98 --- /dev/null +++ b/crates/socket-patch-core/src/patch/vendor/yarn_layering_tests.rs @@ -0,0 +1,1030 @@ +//! Regression guards distilled from the 2026-07 strapi incident: a yarn v1 +//! monorepo was `scan --mode vendored`-wired, then `scan --mode hosted` was +//! layered ON TOP of the vendored lock, and the subsequent `npx yarn@2` +//! install crashed inside yarn's own `patch:` builtin compat entries +//! (`#builtin` / `#builtin` — a yarn-2.4.3 + +//! Node 23+ `util.isDate` removal, NOT a socket edit). These tests freeze +//! the guarantees that made the incident diagnosable: +//! +//! 1. vendored wiring is byte-surgical: only the targeted block changes, +//! sibling versions and the packages yarn builtin-patches (fsevents, +//! resolve) stay byte-identical, no `patch:` protocol string is ever +//! introduced, and the crate's own lockfile parser still reads the file; +//! 2. the rewritten block's `#` fragment and `integrity` SRI match the +//! actual vendored blob bytes (the chain the forensics verified 48/48); +//! 3. hosted-over-vendored layering records the vendored blocks as its +//! `original`s (data-level reversibility) — and `vendor --revert` after +//! that overlay is CURRENTLY drift-skipped and lossy (blob deleted, lock +//! left hosted, exit success): pinned here so a future hosted `--revert` +//! must contend with these semantics deliberately; +//! 4. yarn berry locks containing builtin `patch:` resolution entries pass +//! through both the vendor backend and the hosted redirect rewriter with +//! those entries byte-identical — even when the redirected package IS the +//! builtin-patched one (only its plain `npm:` entry is rewritten; the +//! `patch:` block is skipped with a warning) — and vendoring a +//! builtin-patched package itself refuses fail-closed instead of +//! corrupting the entry. +//! +//! Hermetic: every test builds its own tempdir project, patches from a local +//! `.socket/blobs/` store (`PatchSources::blobs_only`), and never touches +//! the network or process environment (no `#[serial]` needed). + +use std::collections::{BTreeMap, HashMap}; +use std::path::{Path, PathBuf}; + +use base64::Engine as _; +use sha1::Digest as _; + +use crate::hash::git_sha256::compute_git_sha256_from_bytes; +use crate::manifest::schema::{PatchFileInfo, PatchRecord}; +use crate::patch::apply::PatchSources; +use crate::patch::redirect::{rewrite_registry_redirect, DepOverride, Integrity}; +use crate::patch::vendor::lock_inventory::{inventory_npm_lock, LockIntegrity}; +use crate::patch::vendor::npm_flavor::NpmLockFlavor; +use crate::patch::vendor::yarn_berry_lock::{revert_yarn_berry, vendor_yarn_berry}; +use crate::patch::vendor::yarn_classic_lock::{revert_yarn_classic, vendor_yarn_classic}; +use crate::patch::vendor::{RevertOutcome, VendorEntry, VendorOutcome}; +use crate::utils::uri::encode_uri_component; + +/// Canonical-grammar patch uuid (the vendor path layer validates the shape +/// fail-closed, so fixtures must use the real grammar). +const UUID: &str = "9f6b2c4e-1d3a-4f6b-8c2d-7e5a9b1c3d5f"; +const TOKEN: &str = "11111111-1111-1111-1111-111111111111"; +const ORIG_INDEX: &[u8] = b"module.exports = () => 'orig';\n"; +const PATCHED_INDEX: &[u8] = b"module.exports = () => 'patched';\n"; + +// ───────────────────────── classic (yarn v1) fixture ───────────────────────── + +/// Strapi-shaped v1 lock: three versions of the target name (`ansi-regex` — +/// only `4.1.0` is patched), plus the exact packages yarn berry +/// builtin-patches at install time (`fsevents`, `resolve`) at the incident's +/// versions. Every non-target block must survive vendoring byte-identically. +const CLASSIC_BEFORE: &str = r#"# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +ansi-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" + integrity sha512-wFUFA5bg5dviipbQQ32yOQhl6gcJaJXiHE7dvR8VYPG97+J/GNC5FKGepKdEDUFeXRzDxPF1X/Btc8L+v7oqIQ== + +ansi-regex@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" + integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== + +ansi-regex@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" + integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== + +fsevents@^1.2.7: + version "1.2.13" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.13.tgz#f325cb0455592428bcf11b383370ef70e3bfcc38" + integrity sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw== + dependencies: + bindings "^1.5.0" + nan "^2.12.1" + +fsevents@~2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + +resolve@1.1.7: + version "1.1.7" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" + integrity sha1-IDEU2CrSxe2eiBEEs5kwWtaJlls= + +resolve@^1.10.0, resolve@^1.20.0: + version "1.20.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" + integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A== +"#; + +const TARGET_KEY: &str = "ansi-regex@^4.1.0"; + +/// The pre-vendor target block, verbatim (appears exactly once in +/// `CLASSIC_BEFORE`); the expected-text builders splice replacements over it. +const TARGET_BLOCK: &str = "ansi-regex@^4.1.0:\n version \"4.1.0\"\n resolved \"https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997\"\n integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg=="; + +/// The exact hosted URL shape `patch.socket.dev` serves (mirrors the strapi +/// lock's 62 rewritten blocks). +const HOSTED_URL: &str = "https://patch.socket.dev/patch/npm/ansi-regex/4.1.0/11111111-1111-1111-1111-111111111111/9f6b2c4e-1d3a-4f6b-8c2d-7e5a9b1c3d5f/ansi-regex-4.1.0.tgz"; +/// Hosted artifact hashes differ from the local blob's (the server repacks); +/// values are synthetic but shape-correct. +const HOSTED_SHA1: &str = "d758ad8ed758ad8ed758ad8ed758ad8ed758ad8e"; +const HOSTED_SRI: &str = "sha512-HOSTEDhostedHOSTEDhostedHOSTEDhosted9876543210ZYXWVUTSRQPONMLKJIHGFEDCBAzyxwvutsrqponmlkjihgfedcba0123456789AB=="; + +fn vendored_target_block(sha1: &str, sri: &str) -> String { + format!( + "ansi-regex@^4.1.0:\n version \"4.1.0\"\n resolved \"file:./.socket/vendor/npm/{UUID}/ansi-regex-4.1.0.tgz#{sha1}\"\n integrity {sri}" + ) +} + +fn hosted_target_block() -> String { + format!( + "ansi-regex@^4.1.0:\n version \"4.1.0\"\n resolved \"{HOSTED_URL}#{HOSTED_SHA1}\"\n integrity {HOSTED_SRI}" + ) +} + +fn hosted_override() -> DepOverride { + DepOverride { + ecosystem: "npm".to_string(), + name: "ansi-regex".to_string(), + namespace: None, + version: "4.1.0".to_string(), + token: TOKEN.to_string(), + patch_uuid: UUID.to_string(), + artifact_url: HOSTED_URL.to_string(), + berry_zip_url: None, + registry_override: None, + integrity: Integrity { + sha512: Some(HOSTED_SRI.to_string()), + sha1: Some(HOSTED_SHA1.to_string()), + ..Integrity::default() + }, + } +} + +fn record_for(after_hash: &str) -> PatchRecord { + let mut files = HashMap::new(); + files.insert( + "package/index.js".to_string(), + PatchFileInfo { + before_hash: compute_git_sha256_from_bytes(ORIG_INDEX), + after_hash: after_hash.to_string(), + }, + ); + PatchRecord { + uuid: UUID.to_string(), + exported_at: "2026-07-01T00:00:00Z".to_string(), + files, + vulnerabilities: HashMap::new(), + description: "incident regression patch".to_string(), + license: "MIT".to_string(), + tier: "free".to_string(), + } +} + +struct ClassicFx { + tmp: tempfile::TempDir, + record: PatchRecord, +} + +impl ClassicFx { + fn root(&self) -> &Path { + self.tmp.path() + } + + fn lock_path(&self) -> PathBuf { + self.root().join("yarn.lock") + } + + fn lock_text(&self) -> String { + std::fs::read_to_string(self.lock_path()).unwrap() + } + + fn tgz_path(&self) -> PathBuf { + self.root() + .join(format!(".socket/vendor/npm/{UUID}/ansi-regex-4.1.0.tgz")) + } + + /// (sha1 hex, sha512 SRI, sha256 hex) of the ACTUAL vendored blob bytes + /// on disk — the ground truth the rewritten lock block must pin. + fn blob_hashes(&self) -> (String, String, String) { + let tgz = std::fs::read(self.tgz_path()).unwrap(); + let sha1 = hex::encode(sha1::Sha1::digest(&tgz)); + let sri = format!( + "sha512-{}", + base64::engine::general_purpose::STANDARD.encode(sha2::Sha512::digest(&tgz)) + ); + let sha256 = hex::encode(sha2::Sha256::digest(&tgz)); + (sha1, sri, sha256) + } + + async fn vendor(&self) -> VendorOutcome { + let blobs = self.root().join(".socket/blobs"); + let sources = PatchSources::blobs_only(&blobs); + vendor_yarn_classic( + "pkg:npm/ansi-regex@4.1.0", + &self.root().join("node_modules/ansi-regex"), + self.root(), + &self.record, + &sources, + "2026-07-23T00:00:00Z", + false, + false, + None, + ) + .await + } +} + +/// Self-contained classic project: installed target package, local patch +/// blob, and the given yarn.lock bytes. +fn classic_fx(lock_text: &str) -> ClassicFx { + let tmp = tempfile::tempdir().unwrap(); + let root = tmp.path(); + + let installed = root.join("node_modules/ansi-regex"); + std::fs::create_dir_all(&installed).unwrap(); + std::fs::write( + installed.join("package.json"), + br#"{"name":"ansi-regex","version":"4.1.0"}"#, + ) + .unwrap(); + std::fs::write(installed.join("index.js"), ORIG_INDEX).unwrap(); + + let blobs = root.join(".socket/blobs"); + std::fs::create_dir_all(&blobs).unwrap(); + let after_hash = compute_git_sha256_from_bytes(PATCHED_INDEX); + std::fs::write(blobs.join(&after_hash), PATCHED_INDEX).unwrap(); + + std::fs::write(root.join("yarn.lock"), lock_text.as_bytes()).unwrap(); + + ClassicFx { + tmp, + record: record_for(&after_hash), + } +} + +fn expect_done(outcome: VendorOutcome) -> Option { + match outcome { + VendorOutcome::Done { result, entry, .. } => { + assert!(result.success, "vendor failed: {:?}", result.error); + entry + } + VendorOutcome::Refused { code, detail } => { + panic!("expected Done, got Refused {code}: {detail}") + } + } +} + +fn wiring_new_text(entry: &VendorEntry) -> String { + entry.wiring[0] + .new + .as_ref() + .unwrap() + .as_array() + .unwrap() + .iter() + .map(|v| v.as_str().unwrap()) + .collect::>() + .join("\n") +} + +// ───────────────────────────── classic tests ───────────────────────────── + +/// Incident guard 1: vendoring one package out of a strapi-shaped lock is +/// byte-surgical. Untouched blocks (same-name siblings at other versions, +/// fsevents, resolve) stay byte-identical, no `patch:` protocol string is +/// introduced anywhere, and the crate's own yarn-classic parser still reads +/// every surviving registry entry. +/// +/// RED-verified: tampering one byte inside the expected fsevents block fails +/// the full-text equality; pointing the fixture target at `5.0.0` fails the +/// wiring-count assert (sibling gating). +#[tokio::test] +async fn classic_vendor_rewrites_only_target_block_and_stays_parseable() { + assert_eq!( + CLASSIC_BEFORE.matches(TARGET_BLOCK).count(), + 1, + "fixture sanity: target block appears exactly once" + ); + assert!( + !CLASSIC_BEFORE.contains("patch:"), + "fixture sanity: classic input carries no patch: protocol" + ); + + let fx = classic_fx(CLASSIC_BEFORE); + let entry = expect_done(fx.vendor().await).expect("first run wires a ledger entry"); + + // Exactly one block rewritten: the 4.1.0 target — 3.0.0/5.0.0 siblings + // (the strapi lock's exact multi-version shape) are not candidates. + assert_eq!(entry.wiring.len(), 1, "one wiring record for one block"); + assert_eq!(entry.wiring[0].key.as_deref(), Some(TARGET_KEY)); + + // Full-file byte oracle: the output IS the input with only the target + // block spliced — every other byte (fsevents, resolve, siblings, + // comments, blank lines) identical. + let (sha1, sri, _) = fx.blob_hashes(); + let expected = CLASSIC_BEFORE.replace(TARGET_BLOCK, &vendored_target_block(&sha1, &sri)); + let text = fx.lock_text(); + assert_eq!(text, expected, "vendored rewrite must be byte-surgical"); + + // The incident's error family: no rewrite may introduce a `patch:` + // protocol string the file did not already contain. + assert_eq!( + text.matches("patch:").count(), + 0, + "vendored classic lock must not contain a patch: protocol entry" + ); + + // The rewritten file still parses with the crate's own parser: flavor is + // still yarn-classic and every untouched registry entry survives with + // name/version/resolved/integrity intact (the vendored block is excluded + // from the registry inventory by design). + let (flavor, entries) = inventory_npm_lock(fx.root()) + .await + .expect("rewritten lock must still be inventoriable"); + assert_eq!(flavor, NpmLockFlavor::YarnClassic); + let mut got: Vec<(String, String)> = entries + .iter() + .map(|e| (e.name.clone(), e.version.clone())) + .collect(); + got.sort(); + assert_eq!( + got, + vec![ + ("ansi-regex".to_string(), "3.0.0".to_string()), + ("ansi-regex".to_string(), "5.0.0".to_string()), + ("fsevents".to_string(), "1.2.13".to_string()), + ("fsevents".to_string(), "2.3.2".to_string()), + ("resolve".to_string(), "1.1.7".to_string()), + ("resolve".to_string(), "1.20.0".to_string()), + ], + "every untouched entry parses; the vendored one is excluded by design" + ); + let fsev = entries + .iter() + .find(|e| e.name == "fsevents" && e.version == "2.3.2") + .unwrap(); + assert_eq!( + fsev.resolved.as_deref(), + Some("https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz"), + "untouched blocks keep their registry URL (fragment stripped by the parser)" + ); + assert!( + entries + .iter() + .all(|e| matches!(e.integrity, LockIntegrity::Sri(_))), + "every untouched entry keeps a parseable SRI integrity" + ); +} + +/// Incident guard 2 (the chain the forensics verified 48/48 on the strapi +/// tree): the rewritten block's `resolved` keeps the `#` shape +/// where the sha1 fragment and the `integrity` sha512 SRI are recomputed +/// from the ACTUAL vendored blob bytes, and the ledger's artifact.sha256 +/// matches the same bytes. +/// +/// RED-verified: asserting against a flipped sha1 nibble fails all three +/// legs independently. +#[tokio::test] +async fn classic_rewritten_block_hashes_match_vendored_blob_bytes() { + let fx = classic_fx(CLASSIC_BEFORE); + let entry = expect_done(fx.vendor().await).unwrap(); + + let tgz = std::fs::read(fx.tgz_path()).unwrap(); + let (sha1, sri, sha256) = fx.blob_hashes(); + + // resolved "#" — both spellings load-bearing + // (bare path = registry-relative 404; missing fragment = no sha1 check). + let text = fx.lock_text(); + let resolved_line = + format!(" resolved \"file:./.socket/vendor/npm/{UUID}/ansi-regex-4.1.0.tgz#{sha1}\""); + assert!( + text.contains(&resolved_line), + "resolved must pin the blob's sha1 fragment:\n{text}" + ); + assert!( + text.contains(&format!(" integrity {sri}")), + "integrity must be the sha512 SRI of the blob bytes:\n{text}" + ); + + // Ledger chain: artifact facts describe the same bytes. + assert_eq!(entry.artifact.sha256, sha256, "state.json sha256 == blob"); + assert_eq!(entry.artifact.size, Some(tgz.len() as u64)); + assert_eq!( + entry.artifact.path, + format!(".socket/vendor/npm/{UUID}/ansi-regex-4.1.0.tgz") + ); +} + +/// Incident guard 3, forward direction (exactly what the strapi user did): +/// a hosted redirect layered over vendored wiring records the VENDORED +/// block as its `original` (redirect edit.original == vendor wiring.new — +/// the cross-ledger invariant the forensics checked 48/48), produces hosted +/// URLs, leaves every other block byte-identical, introduces no `patch:` +/// string, is idempotent, and its recorded originals are sufficient data to +/// restore the vendored lock byte-exactly. +/// +/// RED-verified: corrupting the expected hosted block text fails the +/// full-text equality; swapping edit.original/new in the splice-back fails +/// the reversibility assert. +#[tokio::test] +async fn classic_hosted_redirect_layers_over_vendored_wiring() { + let fx = classic_fx(CLASSIC_BEFORE); + let entry = expect_done(fx.vendor().await).unwrap(); + let vendored_text = fx.lock_text(); + let (sha1, sri, _) = fx.blob_hashes(); + + let mut files = BTreeMap::new(); + files.insert("yarn.lock".to_string(), vendored_text.clone()); + let result = rewrite_registry_redirect(&files, &[hosted_override()]); + + // Exactly one edit: the yarn-classic entry for the target. + let edits: Vec<_> = result + .edits + .iter() + .filter(|e| e.kind == "redirect_yarn_classic_entry") + .collect(); + assert_eq!(edits.len(), 1, "one hosted edit: {:?}", result.edits); + assert_eq!(result.edits.len(), 1, "no other rewriter produced edits"); + let edit = edits[0]; + assert_eq!(edit.path, "yarn.lock"); + assert_eq!(edit.action, "rewritten"); + assert_eq!(edit.key.as_deref(), Some("ansi-regex@4.1.0")); + + // Cross-ledger invariant: the redirect's `original` is the vendored + // block, verbatim — byte-equal to the vendor wiring's `new`. + let edit_original = edit.original.as_ref().unwrap().as_str().unwrap(); + assert_eq!( + edit_original, + vendored_target_block(&sha1, &sri), + "redirect must record the vendored block as its original" + ); + assert_eq!( + edit_original, + wiring_new_text(&entry), + "redirect edit.original == vendor wiring.new (restore chain intact)" + ); + + // Hosted output: only the target block changed, to the hosted URL + + // hosted hashes; zero vendored file: pointers remain; no patch: string. + let hosted_text = result.files.get("yarn.lock").expect("yarn.lock rewritten"); + let expected_hosted = CLASSIC_BEFORE.replace(TARGET_BLOCK, &hosted_target_block()); + assert_eq!( + hosted_text, &expected_hosted, + "hosted rewrite over vendored wiring must also be byte-surgical" + ); + assert!(!hosted_text.contains("file:./.socket")); + assert_eq!(hosted_text.matches("patch:").count(), 0); + let edit_new = edit.new.as_ref().unwrap().as_str().unwrap(); + assert_eq!(edit_new, hosted_target_block()); + + // Data-level reversibility: splicing each edit's original back over its + // new restores the vendored lock byte-exactly (a future hosted --revert + // has everything it needs in the ledger). + assert_ne!(hosted_text, &vendored_text); + assert_eq!( + hosted_text.replace(edit_new, edit_original), + vendored_text, + "recorded originals must round-trip the hosted overlay" + ); + + // Idempotency: re-running the redirect over the hosted lock is a no-op + // (no new edits, no changed files) — the strapi re-run scenario. + let mut files2 = BTreeMap::new(); + files2.insert("yarn.lock".to_string(), hosted_text.clone()); + let again = rewrite_registry_redirect(&files2, &[hosted_override()]); + assert!( + again.files.is_empty(), + "second hosted run must change nothing: {:?}", + again.files.keys() + ); + assert!(again.edits.is_empty(), "{:?}", again.edits); +} + +/// Incident guard 3, reverse direction — PINS CURRENT (lossy) BEHAVIOR: +/// `vendor --revert` after a hosted overlay finds every block re-resolved +/// (the hosted URL fails the `.socket/vendor/npm/` ownership gate), +/// warns `vendor_lock_entry_drifted`, leaves the lock byte-identical at the +/// hosted URLs — yet still DELETES the blob dir and reports success. If a +/// hosted --revert ships later and restores its recorded originals (the +/// vendored `file:` blocks), the lock would point at blobs this path already +/// deleted. A deliberate behavior change here should update this test. +/// +/// RED-verified: asserting the lock was restored (registry URLs) fails; +/// asserting the blob survives fails. +#[tokio::test] +async fn classic_vendor_revert_after_hosted_overlay_is_drift_skipped_and_lossy() { + let fx = classic_fx(CLASSIC_BEFORE); + let entry = expect_done(fx.vendor().await).unwrap(); + let vendored_text = fx.lock_text(); + + // Layer the hosted redirect over the vendored lock, on disk. + let mut files = BTreeMap::new(); + files.insert("yarn.lock".to_string(), vendored_text); + let result = rewrite_registry_redirect(&files, &[hosted_override()]); + let hosted_text = result.files.get("yarn.lock").unwrap().clone(); + std::fs::write(fx.lock_path(), hosted_text.as_bytes()).unwrap(); + assert!(fx.tgz_path().exists(), "blob present before revert"); + + let outcome: RevertOutcome = revert_yarn_classic(&entry, fx.root(), false).await; + + // CURRENT semantics, all four legs deliberate: + // 1. warning-only success (exit 0 at the CLI layer); + assert!(outcome.success, "{:?}", outcome.error); + // 2. per-block drift warning naming the key; + assert!( + outcome + .warnings + .iter() + .any(|w| w.code == "vendor_lock_entry_drifted" + && w.detail.contains(TARGET_KEY) + && w.detail.contains("re-resolved")), + "hosted URL must be treated as third-party drift: {:?}", + outcome.warnings + ); + // 3. the lock is left byte-identical at the hosted URLs (never + // corrupted, never restored); + assert_eq!( + fx.lock_text(), + hosted_text, + "drift-skip must leave the hosted lock untouched" + ); + // 4. the blob dir is deleted anyway — the lossy half. + assert!( + !fx.root() + .join(format!(".socket/vendor/npm/{UUID}")) + .exists(), + "current behavior deletes the artifact dir even when every block drifted" + ); +} + +/// Incident guard 3, mode flip-flop: re-vendoring over a hosted lock rewires +/// the block back to `file:` but records the HOSTED block as the wiring +/// `original` (`block_points_into_vendor` is false for https URLs), so a +/// subsequent revert restores the hosted block exactly — never a fabricated +/// registry URL. Pins the provenance-supersession the forensics traced. +/// +/// RED-verified: asserting original == the registry block fails; asserting +/// revert lands on registry URLs fails. +#[tokio::test] +async fn classic_revendor_over_hosted_records_hosted_block_as_original() { + let hosted_before = CLASSIC_BEFORE.replace(TARGET_BLOCK, &hosted_target_block()); + let fx = classic_fx(&hosted_before); + + let entry = expect_done(fx.vendor().await).expect("re-vendor wires a new entry"); + let (sha1, sri, _) = fx.blob_hashes(); + + // The lock is rewired to file: — same bytes as vendoring a registry lock. + assert_eq!( + fx.lock_text(), + CLASSIC_BEFORE.replace(TARGET_BLOCK, &vendored_target_block(&sha1, &sri)), + "re-vendor over hosted must produce the standard vendored block" + ); + + // The wiring original is the HOSTED block (one provenance layer deep), + // not the long-gone registry block. + assert_eq!(entry.wiring.len(), 1); + let original: Vec = entry.wiring[0] + .original + .as_ref() + .expect("https block is not ours — it must be recorded as original") + .as_array() + .unwrap() + .iter() + .map(|v| v.as_str().unwrap().to_string()) + .collect(); + assert_eq!( + original.join("\n"), + hosted_target_block(), + "the hosted block supersedes the registry block as `original`" + ); + + // Revert restores the hosted block byte-exactly (never fabricates a + // registry URL) and removes the artifact. + let outcome = revert_yarn_classic(&entry, fx.root(), false).await; + assert!(outcome.success, "{:?}", outcome.error); + assert!(outcome.warnings.is_empty(), "{:?}", outcome.warnings); + assert_eq!( + fx.lock_text(), + hosted_before, + "revert must land on the hosted lock, not registry URLs" + ); + assert!(!fx + .root() + .join(format!(".socket/vendor/npm/{UUID}")) + .exists()); +} + +// ───────────────────────── berry (yarn 2+/4) fixtures ───────────────────────── + +/// Berry lock with the incident's exact coexistence shape: yarn's OWN +/// builtin `patch:` compat entries for fsevents and resolve (`yarn@2.4.3` +/// spells the fragment `#builtin<...>`, yarn 4 `#optional!builtin<...>` — +/// one of each below) alongside a plain npm: entry socket patches. +const BERRY_BEFORE_LOCK: &str = r#"# This file is generated by running "yarn install" inside your project. +# Manual changes might be lost - proceed with caution! + +__metadata: + version: 8 + cacheKey: 10c0 + +"berry-app@workspace:.": + version: 0.0.0-use.local + resolution: "berry-app@workspace:." + dependencies: + fsevents: "npm:2.3.2" + left-pad: "npm:1.3.0" + resolve: "npm:1.20.0" + languageName: unknown + linkType: soft + +"fsevents@npm:2.3.2": + version: 2.3.2 + resolution: "fsevents@npm:2.3.2" + dependencies: + node-gyp: "npm:latest" + checksum: 10c0/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + conditions: os=darwin + languageName: node + linkType: hard + +"fsevents@patch:fsevents@npm%3A2.3.2#optional!builtin": + version: 2.3.2 + resolution: "fsevents@patch:fsevents@npm%3A2.3.2#optional!builtin::version=2.3.2&hash=df0bf1" + dependencies: + node-gyp: "npm:latest" + checksum: 10c0/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + conditions: os=darwin + languageName: node + linkType: hard + +"left-pad@npm:1.3.0": + version: 1.3.0 + resolution: "left-pad@npm:1.3.0" + checksum: 10c0/3fb59c76e281a2f5c810ad71dbbb8eba8b10c6cf94733dc7f27b8c516a5376cacea53543e76f6ae477d866c8954b27f1e15ca349424c2542474eb5bb1d2b6955 + languageName: node + linkType: hard + +"resolve@npm:1.20.0": + version: 1.20.0 + resolution: "resolve@npm:1.20.0" + dependencies: + is-core-module: "npm:^2.2.0" + path-parse: "npm:^1.0.6" + checksum: 10c0/cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc + languageName: node + linkType: hard + +"resolve@patch:resolve@npm%3A1.20.0#builtin": + version: 1.20.0 + resolution: "resolve@patch:resolve@npm%3A1.20.0#builtin::version=1.20.0&hash=c3c19d" + dependencies: + is-core-module: "npm:^2.2.0" + path-parse: "npm:^1.0.6" + checksum: 10c0/dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd + languageName: node + linkType: hard +"#; + +/// serde_json-pretty shape (2-space, trailing newline) so the berry +/// backend's revert round-trips byte-exactly. +const BERRY_BEFORE_PKG: &str = r#"{ + "name": "berry-app", + "version": "1.0.0", + "packageManager": "yarn@4.12.0", + "dependencies": { + "fsevents": "2.3.2", + "left-pad": "1.3.0", + "resolve": "1.20.0" + } +} +"#; + +const BERRY_YARNRC: &str = + "nodeLinker: node-modules\nenableGlobalCache: true\nenableTelemetry: false\n"; + +/// The two builtin patch: entries, verbatim — they must survive every socket +/// operation byte-identically. +const FSEVENTS_PATCH_ENTRY: &str = "\"fsevents@patch:fsevents@npm%3A2.3.2#optional!builtin\":\n version: 2.3.2\n resolution: \"fsevents@patch:fsevents@npm%3A2.3.2#optional!builtin::version=2.3.2&hash=df0bf1\"\n dependencies:\n node-gyp: \"npm:latest\"\n checksum: 10c0/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\n conditions: os=darwin\n languageName: node\n linkType: hard"; +const RESOLVE_PATCH_ENTRY: &str = "\"resolve@patch:resolve@npm%3A1.20.0#builtin\":\n version: 1.20.0\n resolution: \"resolve@patch:resolve@npm%3A1.20.0#builtin::version=1.20.0&hash=c3c19d\"\n dependencies:\n is-core-module: \"npm:^2.2.0\"\n path-parse: \"npm:^1.0.6\"\n checksum: 10c0/dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd\n languageName: node\n linkType: hard"; + +/// `patch:` occurrences in the pristine berry lock: 2 per builtin entry +/// (key + resolution) × 2 entries. +const BERRY_PATCH_COUNT: usize = 4; + +struct BerryFx { + tmp: tempfile::TempDir, + record: PatchRecord, +} + +impl BerryFx { + fn root(&self) -> &Path { + self.tmp.path() + } + + fn lock_text(&self) -> String { + std::fs::read_to_string(self.root().join("yarn.lock")).unwrap() + } + + fn pkg_text(&self) -> String { + std::fs::read_to_string(self.root().join("package.json")).unwrap() + } + + async fn vendor(&self, name: &str, version: &str) -> VendorOutcome { + let blobs = self.root().join(".socket/blobs"); + let sources = PatchSources::blobs_only(&blobs); + let purl = format!("pkg:npm/{name}@{version}"); + vendor_yarn_berry( + &purl, + &self.root().join("node_modules").join(name), + self.root(), + &self.record, + &sources, + "2026-07-23T00:00:00Z", + false, + false, + None, + ) + .await + } +} + +/// Berry project with `installed_name` staged under node_modules/ (the +/// package a test will try to vendor). +fn berry_fx(installed_name: &str, installed_version: &str) -> BerryFx { + let tmp = tempfile::tempdir().unwrap(); + let root = tmp.path(); + + let installed = root.join("node_modules").join(installed_name); + std::fs::create_dir_all(&installed).unwrap(); + std::fs::write( + installed.join("package.json"), + format!(r#"{{"name":"{installed_name}","version":"{installed_version}"}}"#), + ) + .unwrap(); + std::fs::write(installed.join("index.js"), ORIG_INDEX).unwrap(); + + let blobs = root.join(".socket/blobs"); + std::fs::create_dir_all(&blobs).unwrap(); + let after_hash = compute_git_sha256_from_bytes(PATCHED_INDEX); + std::fs::write(blobs.join(&after_hash), PATCHED_INDEX).unwrap(); + + std::fs::write(root.join("package.json"), BERRY_BEFORE_PKG).unwrap(); + std::fs::write(root.join("yarn.lock"), BERRY_BEFORE_LOCK).unwrap(); + std::fs::write(root.join(".yarnrc.yml"), BERRY_YARNRC).unwrap(); + + BerryFx { + tmp, + record: record_for(&after_hash), + } +} + +// ───────────────────────────── berry tests ───────────────────────────── + +/// Incident guard 4a: vendoring an unrelated package in a berry lock leaves +/// yarn's builtin `patch:` compat entries byte-identical (both the yarn-2 +/// `#builtin<...>` and yarn-4 `#optional!builtin<...>` spellings), adds no +/// new `patch:` strings, and the whole edit reverts byte-exactly. +/// +/// RED-verified: expecting BERRY_PATCH_COUNT+1 occurrences fails; tampering +/// a byte in FSEVENTS_PATCH_ENTRY's expectation fails the verbatim assert. +#[tokio::test] +async fn berry_vendor_leaves_builtin_patch_entries_byte_identical() { + assert_eq!( + BERRY_BEFORE_LOCK.matches("patch:").count(), + BERRY_PATCH_COUNT, + "fixture sanity" + ); + + let fx = berry_fx("left-pad", "1.3.0"); + let entry = expect_done(fx.vendor("left-pad", "1.3.0").await).expect("ledger entry"); + + let text = fx.lock_text(); + // The two builtin entries survive verbatim, and the rewrite introduced + // no new patch: protocol strings (the incident's failing entry family). + assert!( + text.contains(FSEVENTS_PATCH_ENTRY), + "fsevents builtin patch entry must survive byte-identically:\n{text}" + ); + assert!( + text.contains(RESOLVE_PATCH_ENTRY), + "resolve builtin patch entry must survive byte-identically:\n{text}" + ); + assert_eq!( + text.matches("patch:").count(), + BERRY_PATCH_COUNT, + "vendoring must neither add nor remove patch: strings" + ); + + // The target entry was rewired to the workspace-bound file: locator. + assert!( + text.contains(&format!( + "\"left-pad@file:./.socket/vendor/npm/{UUID}/left-pad-1.3.0.tgz::locator=berry-app%40workspace%3A.\":" + )), + "left-pad rewired: {text}" + ); + assert!( + !text.contains("\"left-pad@npm:1.3.0\":"), + "the old registry entry key is replaced" + ); + + // The crate's own berry parser still reads the file; the untouched npm: + // entries survive, patch:/file:/workspace resolutions are skipped by + // design. + let (flavor, entries) = inventory_npm_lock(fx.root()).await.expect("parseable"); + assert_eq!(flavor, NpmLockFlavor::YarnBerry); + let mut got: Vec<(String, String)> = entries + .iter() + .map(|e| (e.name.clone(), e.version.clone())) + .collect(); + got.sort(); + assert_eq!( + got, + vec![ + ("fsevents".to_string(), "2.3.2".to_string()), + ("resolve".to_string(), "1.20.0".to_string()), + ] + ); + + // Byte-exact round-trip: revert restores lock AND manifest, builtin + // entries included. + let outcome = revert_yarn_berry(&entry, fx.root(), false).await; + assert!(outcome.success, "{:?}", outcome.error); + assert!(outcome.warnings.is_empty(), "{:?}", outcome.warnings); + assert_eq!(fx.lock_text(), BERRY_BEFORE_LOCK, "lock round-trips"); + assert_eq!(fx.pkg_text(), BERRY_BEFORE_PKG, "package.json round-trips"); +} + +/// Incident guard 4b: vendoring a package that yarn ITSELF builtin-patches +/// (fsevents — its lock resolves the name through a `patch:` protocol entry +/// vendor cannot own) refuses fail-closed BEFORE any write, instead of +/// corrupting the builtin entry. +/// +/// RED-verified: dropping the patch: block from the fixture flips the +/// outcome to Done, failing the Refused assert. +#[tokio::test] +async fn berry_vendoring_a_builtin_patched_package_refuses_fail_closed() { + let fx = berry_fx("fsevents", "2.3.2"); + let outcome = fx.vendor("fsevents", "2.3.2").await; + + match outcome { + VendorOutcome::Refused { code, detail } => { + assert_eq!(code, "vendor_override_conflict", "{detail}"); + assert!( + detail.contains("patch:"), + "the refusal names the un-ownable protocol family: {detail}" + ); + } + VendorOutcome::Done { result, .. } => panic!( + "vendoring a builtin-patched package must refuse (got Done, success={})", + result.success + ), + } + + // Refuse-early contract: nothing written. + assert_eq!(fx.lock_text(), BERRY_BEFORE_LOCK); + assert_eq!(fx.pkg_text(), BERRY_BEFORE_PKG); + assert!(!fx.root().join(".socket/vendor").exists()); +} + +/// Incident guard 4c: the hosted redirect rewriter on a berry lock rewrites +/// ONLY the targeted npm: entry (gaining yarn's own `::__archiveUrl=` +/// binding) and leaves the builtin patch: entries byte-identical. +/// +/// RED-verified: asserting BERRY_PATCH_COUNT+1 fails; asserting the fsevents +/// entry gained an __archiveUrl fails. +#[tokio::test] +async fn berry_hosted_redirect_leaves_builtin_patch_entries_untouched() { + let hosted_url = format!( + "https://patch.socket.dev/patch/npm/left-pad/1.3.0/{TOKEN}/{UUID}/left-pad-1.3.0.tgz" + ); + let dep = DepOverride { + ecosystem: "npm".to_string(), + name: "left-pad".to_string(), + namespace: None, + version: "1.3.0".to_string(), + token: TOKEN.to_string(), + patch_uuid: UUID.to_string(), + artifact_url: hosted_url.clone(), + berry_zip_url: None, + registry_override: None, + integrity: Integrity { + yarn_berry10c0: Some( + "10c0/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee".to_string(), + ), + ..Integrity::default() + }, + }; + + let mut files = BTreeMap::new(); + files.insert("yarn.lock".to_string(), BERRY_BEFORE_LOCK.to_string()); + files.insert(".yarnrc.yml".to_string(), BERRY_YARNRC.to_string()); + let result = rewrite_registry_redirect(&files, &[dep]); + + let text = result.files.get("yarn.lock").expect("yarn.lock rewritten"); + let berry_edits: Vec<_> = result + .edits + .iter() + .filter(|e| e.kind == "redirect_yarn_berry_entry") + .collect(); + assert_eq!(berry_edits.len(), 1, "{:?}", result.edits); + assert_eq!(berry_edits[0].key.as_deref(), Some("left-pad@1.3.0")); + + // The target gained yarn's own archive binding… + assert!( + text.contains(&format!( + " resolution: \"left-pad@npm:1.3.0::__archiveUrl={}\"", + encode_uri_component(&hosted_url) + )), + "target entry redirected: {text}" + ); + // …and the builtin patch: entries are byte-identical, count unchanged. + assert!(text.contains(FSEVENTS_PATCH_ENTRY), "{text}"); + assert!(text.contains(RESOLVE_PATCH_ENTRY), "{text}"); + assert_eq!( + text.matches("patch:").count(), + BERRY_PATCH_COUNT, + "redirect must not introduce or remove patch: strings" + ); + assert!( + !text.contains("fsevents@npm:2.3.2::__archiveUrl"), + "untargeted packages must not be redirected" + ); + // left-pad has no builtin patch: entry, so the protocol gate (and every + // other berry gate) must stay silent. (Non-berry rewriters may still + // warn, e.g. the npm one about the absent package-lock.json.) + assert!( + !result + .warnings + .iter() + .any(|w| w.code.starts_with("redirect_yarn_berry")), + "{:?}", + result.warnings + ); +} + +/// Incident guard 4d: redirecting a package yarn ITSELF builtin-patches +/// (resolve — the fixture holds both the plain `resolve@npm:1.20.0` entry +/// and the builtin `resolve@patch:...#builtin` entry at the +/// same version) rewrites ONLY the npm: entry and leaves the builtin +/// `patch:` block byte-identical, warning about the block it refused to +/// touch. Without the protocol gate the rewriter spliced an +/// `npm:...::__archiveUrl=` resolution under the still-`patch:` key — a +/// corrupted key/resolution protocol mismatch in the incident's exact error +/// family, emitted as a silent second edit. +/// +/// RED-verified: with the protocol gate removed from `rewrite_yarn_berry`, +/// TWO `resolve@1.20.0` edits are emitted and the RESOLVE_PATCH_ENTRY +/// verbatim assert fails (resolution rewritten under the patch: key). +#[tokio::test] +async fn berry_hosted_redirect_of_builtin_patched_package_skips_patch_entry() { + let hosted_url = format!( + "https://patch.socket.dev/patch/npm/resolve/1.20.0/{TOKEN}/{UUID}/resolve-1.20.0.tgz" + ); + let dep = DepOverride { + ecosystem: "npm".to_string(), + name: "resolve".to_string(), + namespace: None, + version: "1.20.0".to_string(), + token: TOKEN.to_string(), + patch_uuid: UUID.to_string(), + artifact_url: hosted_url.clone(), + berry_zip_url: None, + registry_override: None, + integrity: Integrity { + yarn_berry10c0: Some(format!("10c0/{}", "f".repeat(128))), + ..Integrity::default() + }, + }; + + let mut files = BTreeMap::new(); + files.insert("yarn.lock".to_string(), BERRY_BEFORE_LOCK.to_string()); + files.insert(".yarnrc.yml".to_string(), BERRY_YARNRC.to_string()); + let result = rewrite_registry_redirect(&files, &[dep]); + + let text = result.files.get("yarn.lock").expect("yarn.lock rewritten"); + + // Exactly ONE edit — the plain npm: entry. (The corruption shape was + // two edits both keyed resolve@1.20.0, the second under the patch: key.) + let berry_edits: Vec<_> = result + .edits + .iter() + .filter(|e| e.kind == "redirect_yarn_berry_entry") + .collect(); + assert_eq!(berry_edits.len(), 1, "{:?}", result.edits); + assert_eq!(berry_edits[0].key.as_deref(), Some("resolve@1.20.0")); + + // The plain npm: entry gained yarn's archive binding… + assert!( + text.contains(&format!( + " resolution: \"resolve@npm:1.20.0::__archiveUrl={}\"", + encode_uri_component(&hosted_url) + )), + "plain npm: entry redirected: {text}" + ); + // …the builtin patch: entries survive byte-identically (key AND + // resolution — the corruption kept the key but rewrote the resolution), + // with the patch: count unchanged… + assert!(text.contains(RESOLVE_PATCH_ENTRY), "{text}"); + assert!(text.contains(FSEVENTS_PATCH_ENTRY), "{text}"); + assert_eq!( + text.matches("patch:").count(), + BERRY_PATCH_COUNT, + "redirect must not introduce or remove patch: strings" + ); + // …and the skip is loud, naming the un-ownable entry. + assert!( + result.warnings.iter().any(|w| { + w.code == "redirect_yarn_berry_unsupported_protocol" + && w.detail.contains("builtin") + }), + "warning must name the skipped builtin patch: entry: {:?}", + result.warnings + ); +}