From cd5895a528219da8db467949fdf00abbc640c5ec Mon Sep 17 00:00:00 2001 From: Mikola Lysenko Date: Fri, 14 Aug 2026 07:00:59 -0700 Subject: [PATCH 1/2] fix(hosted): never redirect bundled npm entries; warn on misses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bundled (`inBundle`, legacy `bundled`) package-lock entries are extracted from their parent's tarball at install time — npm ignores their resolved/integrity — yet `scan --mode hosted` rewrote them anyway. The hosted URL then appeared in the lockfile text, so the run counted the dep as redirected, persisted a ledger record, and `--vex` attested not_affected over bytes that never install. The npm rewriter now skips bundled entries with the vendored backend's loud stays-UNPATCHED warning (`redirect_npm_bundled_instance_skipped`), so they are never rewritten, never counted redirected, and never attested. The rewriter also now matches lock entries by the package they stand for — the entry's `name` field when present, mirroring the vendored `entry_name` — so an alias install (`npm i alias@npm:real`) of the patched package is redirected instead of silently dropped, and an entry that merely shares the key name (`npm i @npm:other`, the fork-substitution pattern) is never hijacked to the upstream patched artifact. Link (workspace) entries are skipped with a warning, and a granted dep matching no lock entry finally warns `redirect_npm_entry_not_found` — parity with the pnpm/berry/uv rewriters — instead of vanishing from the redirected count with an empty warnings array. Co-authored-by: Claude Fable 5 --- .../tests/in_process_redirect.rs | 78 ++++ .../src/patch/redirect/mod.rs | 436 +++++++++++++++++- 2 files changed, 493 insertions(+), 21 deletions(-) diff --git a/crates/socket-patch-cli/tests/in_process_redirect.rs b/crates/socket-patch-cli/tests/in_process_redirect.rs index d302aa48..55084f3c 100644 --- a/crates/socket-patch-cli/tests/in_process_redirect.rs +++ b/crates/socket-patch-cli/tests/in_process_redirect.rs @@ -1313,6 +1313,84 @@ fn warning_codes(env: &serde_json::Value) -> Vec { .unwrap_or_default() } +/// A patched dep whose ONLY lock instance is bundled (`inBundle: true`) +/// must not be redirected: npm extracts that copy from its parent's tarball +/// and ignores the entry's resolved/integrity, so a rewrite would confirm — +/// and VEX-attest — a patch whose bytes never install. The run must report +/// `redirected: 0`, leave the lockfile byte-identical, and carry the loud +/// stays-UNPATCHED warning. Subprocess so the `--json` envelope's +/// `redirected` count and `warnings[]` can be read back. +#[tokio::test] +#[serial] +async fn redirect_inbundle_only_dep_is_skipped_not_confirmed() { + let server = MockServer::start().await; + mock_discovery(&server).await; + mock_reference(&server).await; + + let tmp = tempfile::tempdir().unwrap(); + std::fs::write( + tmp.path().join("package.json"), + r#"{ "name": "consumer", "version": "0.0.0", "dependencies": { "parent": "2.0.0" } }"#, + ) + .unwrap(); + // Installed tree: the patched package exists only as parent's bundled + // nested copy — the crawler still discovers it there. + let parent = tmp.path().join("node_modules").join("parent"); + std::fs::create_dir_all(&parent).unwrap(); + std::fs::write( + parent.join("package.json"), + r#"{ "name": "parent", "version": "2.0.0", "bundleDependencies": ["in-proc-redirect"] }"#, + ) + .unwrap(); + let nested = parent.join("node_modules").join(NAME); + std::fs::create_dir_all(&nested).unwrap(); + std::fs::write( + nested.join("package.json"), + format!(r#"{{ "name": "{NAME}", "version": "{VERSION}" }}"#), + ) + .unwrap(); + let lock = format!( + r#"{{ + "name": "consumer", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": {{ + "": {{ "name": "consumer", "version": "0.0.0", "dependencies": {{ "parent": "2.0.0" }} }}, + "node_modules/parent": {{ + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/parent/-/parent-2.0.0.tgz", + "integrity": "sha512-PARENT==" + }}, + "node_modules/parent/node_modules/{NAME}": {{ + "version": "{VERSION}", + "inBundle": true, + "integrity": "sha512-UPSTREAMupstream==" + }} + }} +}} +"# + ); + std::fs::write(tmp.path().join("package-lock.json"), &lock).unwrap(); + + let env = run_redirect_subprocess(tmp.path(), &server.uri()); + assert_eq!( + env["redirect"]["redirected"], 0, + "a bundled-only dep must NOT be counted redirected: {env}" + ); + let codes = warning_codes(&env); + assert!( + codes.contains(&"redirect_npm_bundled_instance_skipped".to_string()), + "the stays-UNPATCHED warning must reach the envelope: {env}" + ); + let after = std::fs::read_to_string(tmp.path().join("package-lock.json")).unwrap(); + assert_eq!(after, lock, "the lockfile must be byte-untouched"); + assert!( + !after.contains(HOSTED_URL), + "the hosted URL must never appear (it would confirm + attest): {after}" + ); +} + /// The rewriters' own warnings must reach HUMAN mode too, not just the /// `--json` envelope: they carry the load-bearing "why nothing happened / /// what you must do" guidance (`redirect_npm_no_lockfile`, diff --git a/crates/socket-patch-core/src/patch/redirect/mod.rs b/crates/socket-patch-core/src/patch/redirect/mod.rs index 0ff40eb5..396ad6e1 100644 --- a/crates/socket-patch-core/src/patch/redirect/mod.rs +++ b/crates/socket-patch-core/src/patch/redirect/mod.rs @@ -200,30 +200,94 @@ fn rewrite_npm_lock( }); continue; }; - let suffix = format!("node_modules/{fname}"); + let mut matched_any = false; if let Some(packages) = lock.get_mut("packages").and_then(Value::as_object_mut) { for (key, entry) in packages.iter_mut() { - let matches_key = key == &suffix || key.ends_with(&format!("/{suffix}")); + // Only `node_modules/` keys are installable dependencies: + // "" is the project root and other bare keys are workspace + // members — SOURCE dirs a resolved/integrity insert would + // corrupt. + let Some((_, key_name)) = key.rsplit_once("node_modules/") else { + continue; + }; + // The package a lock entry stands for: the explicit `name` + // field when present (npm writes it for aliases — `npm i + // alias@npm:real` keys the entry by the ALIAS), else the + // key's trailing path. Mirrors `vendor::npm_lock`'s + // `entry_name`, so an alias install of the patched package + // redirects and an entry that merely SHARES the key name + // (`npm i @npm:other`) is never hijacked. + let entry_nm = entry + .get("name") + .and_then(Value::as_str) + .unwrap_or(key_name); let matches_ver = entry.get("version").and_then(Value::as_str) == Some(dep.version.as_str()); - if matches_key && matches_ver { - if let Some(edit) = rewrite_npm_entry( - entry, - dep, - &sha512, - lockfile, - "redirect_npm_lock_entry", - key, - ) { - result.edits.push(edit); - changed = true; - } + if entry_nm != fname || !matches_ver { + continue; + } + if entry.get("link").and_then(Value::as_bool) == Some(true) { + matched_any = true; + result.warnings.push(RewriteWarning { + code: "redirect_npm_link_entry_skipped".into(), + detail: format!( + "lock entry `{key}` is a link (npm workspaces/file: dir); skipped" + ), + }); + continue; + } + // npm reify extracts a bundled copy from its PARENT's tarball + // and ignores the entry's resolved/integrity, so a rewrite + // here would put the hosted URL in the lockfile (confirming + // and VEX-attesting the patch) while the unpatched bundled + // bytes keep installing. Mirrors the vendored backend's + // `vendor_bundled_instance_skipped` refusal. + if entry.get("inBundle").and_then(Value::as_bool) == Some(true) { + matched_any = true; + result.warnings.push(RewriteWarning { + code: "redirect_npm_bundled_instance_skipped".into(), + detail: format!( + "lock entry `{key}` is bundled inside its parent's tarball and \ + CANNOT be redirected — that copy stays UNPATCHED; vendor or \ + update the bundling parent to cover it" + ), + }); + continue; + } + matched_any = true; + if let Some(edit) = rewrite_npm_entry( + entry, + dep, + &sha512, + lockfile, + "redirect_npm_lock_entry", + key, + ) { + result.edits.push(edit); + changed = true; } } } // v2 legacy `dependencies` tree (keyed by name), recursive. if let Some(deps) = lock.get_mut("dependencies").and_then(Value::as_object_mut) { - changed = rewrite_npm_v2_deps(deps, &fname, dep, &sha512, lockfile, result) || changed; + changed = rewrite_npm_v2_deps( + deps, + &fname, + dep, + &sha512, + lockfile, + result, + &mut matched_any, + ) || changed; + } + // Parity with the pnpm/berry/uv rewriters: a granted dep the + // lockfile cannot pin must be SAID, not silently dropped from the + // redirected count. + if !matched_any { + result.warnings.push(RewriteWarning { + code: "redirect_npm_entry_not_found".into(), + detail: format!("no {lockfile} entry for {fname}@{}", dep.version), + }); } } if changed { @@ -270,21 +334,39 @@ fn rewrite_npm_v2_deps( sha512: &str, lockfile: &str, result: &mut RewriteResult, + matched_any: &mut bool, ) -> bool { let mut changed = false; for (name, entry) in deps.iter_mut() { if name == fname && entry.get("version").and_then(Value::as_str) == Some(dep.version.as_str()) { - if let Some(edit) = - rewrite_npm_entry(entry, dep, sha512, lockfile, "redirect_npm_lock_dep", name) - { - result.edits.push(edit); - changed = true; + // Legacy spelling of `inBundle`: same npm-ignores-the-rewrite + // fail-open as the `packages` guard above. + if entry.get("bundled").and_then(Value::as_bool) == Some(true) { + *matched_any = true; + result.warnings.push(RewriteWarning { + code: "redirect_npm_bundled_instance_skipped".into(), + detail: format!( + "legacy dependencies entry `{name}` is bundled inside its parent's \ + tarball and CANNOT be redirected — that copy stays UNPATCHED; vendor \ + or update the bundling parent to cover it" + ), + }); + } else { + *matched_any = true; + if let Some(edit) = + rewrite_npm_entry(entry, dep, sha512, lockfile, "redirect_npm_lock_dep", name) + { + result.edits.push(edit); + changed = true; + } } } if let Some(nested) = entry.get_mut("dependencies").and_then(Value::as_object_mut) { - changed = rewrite_npm_v2_deps(nested, fname, dep, sha512, lockfile, result) || changed; + changed = + rewrite_npm_v2_deps(nested, fname, dep, sha512, lockfile, result, matched_any) + || changed; } } changed @@ -3610,6 +3692,318 @@ mod tests { ); } + /// A bundled (`inBundle: true`) lock entry must NOT be rewritten: npm + /// reify extracts that copy from its parent's tarball and ignores the + /// entry's resolved/integrity, so a rewrite would put the hosted URL in + /// the lockfile — confirming, ledger-recording, and VEX-attesting a patch + /// whose bytes never install. It must be skipped with a loud + /// stays-UNPATCHED warning instead (mirroring the vendored backend). + #[test] + fn npm_inbundle_entry_is_skipped_with_loud_warning() { + let mut files = BTreeMap::new(); + files.insert( + "package-lock.json".to_string(), + r#"{ + "name": "app", + "lockfileVersion": 3, + "packages": { + "": { "name": "app", "version": "0.0.0" }, + "node_modules/parent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/parent/-/parent-2.0.0.tgz", + "integrity": "sha512-PARENT==" + }, + "node_modules/parent/node_modules/left-pad": { + "version": "1.3.0", + "inBundle": true, + "integrity": "sha512-UPSTREAM==" + } + } +} +"# + .to_string(), + ); + let overrides = vec![npm_override( + "left-pad", + "1.3.0", + "http://patch.test/lp.tgz", + "sha512-PATCHED==", + )]; + let r = rewrite_registry_redirect(&files, &overrides); + assert!( + r.files.is_empty() && r.edits.is_empty(), + "a bundled-only dep must change nothing: files={:?} edits={:?}", + r.files.keys(), + r.edits + ); + let bundled = r + .warnings + .iter() + .find(|w| w.code == "redirect_npm_bundled_instance_skipped") + .unwrap_or_else(|| panic!("bundled skip must warn: {:?}", r.warnings)); + assert!( + bundled.detail.contains("UNPATCHED") + && bundled + .detail + .contains("node_modules/parent/node_modules/left-pad"), + "the warning must say the copy stays unpatched and name the entry: {}", + bundled.detail + ); + assert!( + !warning_codes(&r).contains(&"redirect_npm_entry_not_found"), + "a bundled skip is a MATCH — not-found must stay quiet: {:?}", + r.warnings + ); + } + + /// When the patched dep has both a regular entry and a bundled nested + /// copy, the regular entry is redirected and the bundled copy is left + /// byte-untouched behind the stays-UNPATCHED warning (partial coverage + /// must be surfaced, not silently absorbed). + #[test] + fn npm_inbundle_skip_leaves_sibling_rewrite_intact() { + let mut files = BTreeMap::new(); + files.insert( + "package-lock.json".to_string(), + r#"{ + "name": "app", + "lockfileVersion": 3, + "packages": { + "": { "name": "app", "version": "0.0.0" }, + "node_modules/left-pad": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz", + "integrity": "sha512-UPSTREAM==" + }, + "node_modules/parent/node_modules/left-pad": { + "version": "1.3.0", + "inBundle": true, + "integrity": "sha512-UPSTREAM==" + } + } +} +"# + .to_string(), + ); + let overrides = vec![npm_override( + "left-pad", + "1.3.0", + "http://patch.test/lp.tgz", + "sha512-PATCHED==", + )]; + let r = rewrite_registry_redirect(&files, &overrides); + assert_eq!(r.edits.len(), 1, "only the regular entry: {:?}", r.edits); + assert_eq!( + r.edits[0].key.as_deref(), + Some("node_modules/left-pad"), + "the rewritten entry is the non-bundled one" + ); + let out = r.files.get("package-lock.json").expect("lock rewritten"); + let lock: Value = serde_json::from_str(out).unwrap(); + let bundled_entry = &lock["packages"]["node_modules/parent/node_modules/left-pad"]; + assert_eq!( + bundled_entry["integrity"], "sha512-UPSTREAM==", + "the bundled copy must keep its upstream pin: {out}" + ); + assert!( + bundled_entry.get("resolved").is_none(), + "no resolved may be inserted into the bundled entry: {out}" + ); + assert!( + warning_codes(&r).contains(&"redirect_npm_bundled_instance_skipped"), + "partial coverage must be surfaced: {:?}", + r.warnings + ); + } + + /// The v1/v2 legacy `dependencies` tree spells the bundled flag + /// `bundled: true` — same guard as `inBundle` in `packages`. + #[test] + fn npm_legacy_bundled_dependency_is_skipped() { + let mut files = BTreeMap::new(); + files.insert( + "package-lock.json".to_string(), + r#"{ + "name": "app", + "lockfileVersion": 1, + "dependencies": { + "parent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/parent/-/parent-2.0.0.tgz", + "integrity": "sha512-PARENT==", + "dependencies": { + "left-pad": { + "version": "1.3.0", + "bundled": true + } + } + } + } +} +"# + .to_string(), + ); + let overrides = vec![npm_override( + "left-pad", + "1.3.0", + "http://patch.test/lp.tgz", + "sha512-PATCHED==", + )]; + let r = rewrite_registry_redirect(&files, &overrides); + assert!( + r.files.is_empty() && r.edits.is_empty(), + "legacy bundled dep must change nothing: files={:?} edits={:?}", + r.files.keys(), + r.edits + ); + assert!( + warning_codes(&r).contains(&"redirect_npm_bundled_instance_skipped"), + "legacy bundled skip must warn: {:?}", + r.warnings + ); + } + + /// An alias install (`npm i my-alias@npm:left-pad@1.3.0`) keys the lock + /// entry by the ALIAS with the real package in `name`. Discovery is + /// alias-aware (the crawler reads the installed package.json name), so + /// the rewriter must be too — matching on the entry's `name`, mirroring + /// `vendor::npm_lock::entry_name`. + #[test] + fn npm_alias_entry_is_redirected() { + let mut files = BTreeMap::new(); + files.insert( + "package-lock.json".to_string(), + r#"{ + "name": "app", + "lockfileVersion": 3, + "packages": { + "": { "name": "app", "version": "0.0.0" }, + "node_modules/my-alias": { + "name": "left-pad", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz", + "integrity": "sha512-UPSTREAM==" + } + } +} +"# + .to_string(), + ); + let overrides = vec![npm_override( + "left-pad", + "1.3.0", + "http://patch.test/lp.tgz", + "sha512-PATCHED==", + )]; + let r = rewrite_registry_redirect(&files, &overrides); + assert_eq!(r.edits.len(), 1, "alias entry redirected: {:?}", r.warnings); + assert_eq!(r.edits[0].key.as_deref(), Some("node_modules/my-alias")); + let out = r.files.get("package-lock.json").expect("lock rewritten"); + let lock: Value = serde_json::from_str(out).unwrap(); + assert_eq!( + lock["packages"]["node_modules/my-alias"]["resolved"], + "http://patch.test/lp.tgz" + ); + assert_eq!( + lock["packages"]["node_modules/my-alias"]["integrity"], + "sha512-PATCHED==" + ); + assert!( + !warning_codes(&r).contains(&"redirect_npm_entry_not_found"), + "{:?}", + r.warnings + ); + } + + /// The reverse alias direction: `npm i left-pad@npm:other-pkg` keys an + /// entry `node_modules/left-pad` whose `name` is the OTHER package. A + /// deliberate fork substitution must never be hijacked back to the + /// patched upstream artifact just because the versions coincide. + #[test] + fn npm_alias_of_other_package_is_not_hijacked() { + let mut files = BTreeMap::new(); + files.insert( + "package-lock.json".to_string(), + r#"{ + "name": "app", + "lockfileVersion": 3, + "packages": { + "": { "name": "app", "version": "0.0.0" }, + "node_modules/left-pad": { + "name": "totally-other", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/totally-other/-/totally-other-1.3.0.tgz", + "integrity": "sha512-FORK==" + } + } +} +"# + .to_string(), + ); + let overrides = vec![npm_override( + "left-pad", + "1.3.0", + "http://patch.test/lp.tgz", + "sha512-PATCHED==", + )]; + let r = rewrite_registry_redirect(&files, &overrides); + assert!( + r.files.is_empty() && r.edits.is_empty(), + "the fork substitution must survive: files={:?} edits={:?}", + r.files.keys(), + r.edits + ); + assert!( + warning_codes(&r).contains(&"redirect_npm_entry_not_found"), + "nothing redirectable matched, which must be said: {:?}", + r.warnings + ); + } + + /// A granted npm override matching no lock entry (not installed, or the + /// lock drifted to another version) must warn — parity with + /// `redirect_pnpm_entry_not_found` / `redirect_yarn_berry_entry_not_found`. + /// Silence here made every npm redirect miss unreadable in CI. + #[test] + fn npm_entry_not_found_warns() { + let mut files = BTreeMap::new(); + files.insert( + "package-lock.json".to_string(), + r#"{ + "name": "app", + "lockfileVersion": 3, + "packages": { + "": { "name": "app", "version": "0.0.0" }, + "node_modules/left-pad": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/left-pad/-/left-pad-1.2.0.tgz", + "integrity": "sha512-OLD==" + } + } +} +"# + .to_string(), + ); + let overrides = vec![npm_override( + "left-pad", + "1.3.0", + "http://patch.test/lp.tgz", + "sha512-PATCHED==", + )]; + let r = rewrite_registry_redirect(&files, &overrides); + assert!(r.files.is_empty() && r.edits.is_empty()); + let nf = r + .warnings + .iter() + .find(|w| w.code == "redirect_npm_entry_not_found") + .unwrap_or_else(|| panic!("version drift must warn: {:?}", r.warnings)); + assert!( + nf.detail.contains("left-pad@1.3.0") && nf.detail.contains("package-lock.json"), + "the warning names the dep and the lockfile: {}", + nf.detail + ); + } + /// pnpm lockfileVersion 9 single-quotes `packages:` keys that begin with /// `@` (`'@scope/name@1.0.0':` — YAML forbids a plain scalar starting /// with `@`), so the rewriter must match the quoted form too. Without it, From 5f856c764ca0a27e72009ed39a1a791512978d7e Mon Sep 17 00:00:00 2001 From: Mikola Lysenko Date: Fri, 14 Aug 2026 20:10:14 -0400 Subject: [PATCH 2/2] test(redirect): message-carrying expect in new tests A bare unwrap panic names only the file and line, so a CI failure in these fixtures says "called Result::unwrap() on an Err value" and nothing about which setup step gave out. Reviewer feedback on #189: the new bundled-instance test and the two npm rewriter cases now say what they were doing when they failed. Scoped to the code this PR adds. A file-wide sweep would touch every test in in_process_redirect.rs and conflict with the several redirect branches in flight against it, so it belongs in its own mechanical PR. Co-authored-by: Claude Fable 5 --- .../tests/in_process_redirect.rs | 18 ++++++++++-------- .../src/patch/redirect/mod.rs | 6 ++++-- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/crates/socket-patch-cli/tests/in_process_redirect.rs b/crates/socket-patch-cli/tests/in_process_redirect.rs index 55084f3c..59d46359 100644 --- a/crates/socket-patch-cli/tests/in_process_redirect.rs +++ b/crates/socket-patch-cli/tests/in_process_redirect.rs @@ -1327,28 +1327,28 @@ async fn redirect_inbundle_only_dep_is_skipped_not_confirmed() { mock_discovery(&server).await; mock_reference(&server).await; - let tmp = tempfile::tempdir().unwrap(); + let tmp = tempfile::tempdir().expect("create the fixture tempdir"); std::fs::write( tmp.path().join("package.json"), r#"{ "name": "consumer", "version": "0.0.0", "dependencies": { "parent": "2.0.0" } }"#, ) - .unwrap(); + .expect("write the consumer package.json"); // Installed tree: the patched package exists only as parent's bundled // nested copy — the crawler still discovers it there. let parent = tmp.path().join("node_modules").join("parent"); - std::fs::create_dir_all(&parent).unwrap(); + std::fs::create_dir_all(&parent).expect("create node_modules/parent"); std::fs::write( parent.join("package.json"), r#"{ "name": "parent", "version": "2.0.0", "bundleDependencies": ["in-proc-redirect"] }"#, ) - .unwrap(); + .expect("write the bundling parent's package.json"); let nested = parent.join("node_modules").join(NAME); - std::fs::create_dir_all(&nested).unwrap(); + std::fs::create_dir_all(&nested).expect("create the bundled nested copy's dir"); std::fs::write( nested.join("package.json"), format!(r#"{{ "name": "{NAME}", "version": "{VERSION}" }}"#), ) - .unwrap(); + .expect("write the bundled nested copy's package.json"); let lock = format!( r#"{{ "name": "consumer", @@ -1371,7 +1371,8 @@ async fn redirect_inbundle_only_dep_is_skipped_not_confirmed() { }} "# ); - std::fs::write(tmp.path().join("package-lock.json"), &lock).unwrap(); + std::fs::write(tmp.path().join("package-lock.json"), &lock) + .expect("write the bundled-instance package-lock.json"); let env = run_redirect_subprocess(tmp.path(), &server.uri()); assert_eq!( @@ -1383,7 +1384,8 @@ async fn redirect_inbundle_only_dep_is_skipped_not_confirmed() { codes.contains(&"redirect_npm_bundled_instance_skipped".to_string()), "the stays-UNPATCHED warning must reach the envelope: {env}" ); - let after = std::fs::read_to_string(tmp.path().join("package-lock.json")).unwrap(); + let after = std::fs::read_to_string(tmp.path().join("package-lock.json")) + .expect("read back the package-lock.json the run must have left alone"); assert_eq!(after, lock, "the lockfile must be byte-untouched"); assert!( !after.contains(HOSTED_URL), diff --git a/crates/socket-patch-core/src/patch/redirect/mod.rs b/crates/socket-patch-core/src/patch/redirect/mod.rs index 396ad6e1..66b8dffb 100644 --- a/crates/socket-patch-core/src/patch/redirect/mod.rs +++ b/crates/socket-patch-core/src/patch/redirect/mod.rs @@ -3799,7 +3799,8 @@ mod tests { "the rewritten entry is the non-bundled one" ); let out = r.files.get("package-lock.json").expect("lock rewritten"); - let lock: Value = serde_json::from_str(out).unwrap(); + let lock: Value = + serde_json::from_str(out).expect("the rewritten lock must stay valid JSON"); let bundled_entry = &lock["packages"]["node_modules/parent/node_modules/left-pad"]; assert_eq!( bundled_entry["integrity"], "sha512-UPSTREAM==", @@ -3899,7 +3900,8 @@ mod tests { assert_eq!(r.edits.len(), 1, "alias entry redirected: {:?}", r.warnings); assert_eq!(r.edits[0].key.as_deref(), Some("node_modules/my-alias")); let out = r.files.get("package-lock.json").expect("lock rewritten"); - let lock: Value = serde_json::from_str(out).unwrap(); + let lock: Value = + serde_json::from_str(out).expect("the rewritten lock must stay valid JSON"); assert_eq!( lock["packages"]["node_modules/my-alias"]["resolved"], "http://patch.test/lp.tgz"