From 8d8b6641909ef0a8b392b39b85360b4469f83803 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 08:24:04 +0000 Subject: [PATCH 1/4] Start fix for #1243 Assisted-by: Claude Code:claude-opus-5-5 From 537e25ceb7f814abe7f44db85e79f8cf29ec0253 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 08:42:19 +0000 Subject: [PATCH 2/4] Keep bun.lockb shared bundled pins manageable Bun 1.2+ keeps one bun.lockb record for a version that is installed both from the registry and bundled inside a parent's tarball. The hosted scan wires that record for the regular install, but discovery treated it like a bundled-only record and dropped its ref. So `list`, `remove` and `rollback` refused the pin as contested, and the hosted to vendored takeover failed with vendor_lock_entry_not_found. Discovery now classifies a shared record as the regular install and records its version as a bundled copy, so the ref is shadowed: still never attested in VEX (the bundled copy stays unpatched), but visible to every command that manages hosted pins, as the text bun.lock already is. Fixes #1243 Assisted-by: Claude Code:claude-opus-5-5 --- .../socket-patch-cli/tests/e2e_bun_lockb.rs | 145 ++++++++++++++++++ .../socket-patch-core/src/vex/discover/bun.rs | 47 +++++- 2 files changed, 190 insertions(+), 2 deletions(-) diff --git a/crates/socket-patch-cli/tests/e2e_bun_lockb.rs b/crates/socket-patch-cli/tests/e2e_bun_lockb.rs index 3e6d41a25..060824c61 100644 --- a/crates/socket-patch-cli/tests/e2e_bun_lockb.rs +++ b/crates/socket-patch-cli/tests/e2e_bun_lockb.rs @@ -351,6 +351,16 @@ impl Fixture { ) .unwrap(); } + if shape == "bundled" { + // REGRESSION (#1243): a local parent that bundles its own + // minimist@1.2.2 beside the root's registry minimist@1.2.2. + std::fs::write( + project.join("bparent-1.0.0.tgz"), + bundling_parent_tgz("minimist", "1.2.2"), + ) + .unwrap(); + package["dependencies"]["bparent"] = json!("file:./bparent-1.0.0.tgz"); + } if shape == "extensions" { package["dependencies"]["consumer"] = json!("workspace:*"); package["dependencies"]["git-number"] = json!("github:jonschlinkert/is-number#7.0.0"); @@ -619,6 +629,44 @@ impl Fixture { } } +/// A `bparent@1.0.0` tarball that declares `bundleDependencies: [name]` +/// and ships its own `name@version` under `node_modules/`. +fn bundling_parent_tgz(name: &str, version: &str) -> Vec { + let manifest = json!({"name":"bparent", "version":"1.0.0", + "dependencies":{name: version}, "bundleDependencies":[name]}); + let bundled = json!({"name":name, "version":version, "main":"index.js"}); + let files = [ + ("package/package.json".to_string(), manifest.to_string()), + ( + "package/index.js".to_string(), + format!("module.exports = require({name:?});\n"), + ), + ( + format!("package/node_modules/{name}/package.json"), + bundled.to_string(), + ), + ( + format!("package/node_modules/{name}/index.js"), + "module.exports = 'bundled';\n".to_string(), + ), + ]; + let mut builder = tar::Builder::new(flate2::write::GzEncoder::new( + Vec::new(), + flate2::Compression::default(), + )); + for (path, body) in &files { + let mut header = tar::Header::new_gnu(); + header.set_size(body.len() as u64); + header.set_mode(0o644); + header.set_mtime(0); + header.set_cksum(); + builder + .append_data(&mut header, path, body.as_bytes()) + .unwrap(); + } + builder.into_inner().unwrap().finish().unwrap() +} + fn make_tgz_from_installed(pkg_dir: &Path, replaced_index: &[u8]) -> Vec { let pkg_dir = pkg_dir .canonicalize() @@ -972,6 +1020,103 @@ async fn native_binary_hosted_vendored_takeover_roundtrip() { fixture.frozen("rolled-back", &fixture.original, "minimist"); } +/// REGRESSION (#1243): Bun 1.2+ keeps ONE `bun.lockb` record for a +/// version installed both from the registry and bundled inside a parent's +/// tarball. The hosted scan wires that record for the regular install +/// (warning that the bundled copy stays unpatched); the pin it wrote must +/// then be listed and unwound like any other: `list` succeeds, the online +/// hosted → vendored takeover restores the registry record and vendors over +/// it, and `vendor --revert` gives back the pre-hosted bytes exactly. +#[tokio::test(flavor = "multi_thread")] +#[serial_test::serial] +async fn native_binary_shared_bundled_record_hosted_pin_is_managed() { + let Some(fixture) = Fixture::new("bundled") else { + return; + }; + let server = MockServer::start().await; + mock_api(&server, &fixture, "minimist").await; + let project = &fixture.project; + let uri = server.uri(); + + let hosted = scan(project, &server, "hosted", &[]); + assert_eq!(hosted["redirect"]["redirected"], 1, "hosted scan: {hosted}"); + let text = hosted.to_string(); + let shared = + text.contains("redirect_bun_bundled_instance_skipped") && text.contains("also bundled"); + if !shared { + // Bun < 1.2 records no bundled flag on the regular record; that + // shape is the ordinary hosted pin the other tests cover. + eprintln!("SKIP #1243 leg: this Bun keeps no shared bundled record: {hosted}"); + return; + } + assert_ne!(fixture.lock(), fixture.original_lock); + + let (code, listed) = cli_code(project, &["list"]); + assert_eq!( + code, 0, + "list must accept the hosted pin it wrote: {listed}" + ); + assert!( + !listed.to_string().contains("hosted_wiring_contested"), + "{listed}" + ); + assert!( + listed.to_string().contains(UUID), + "list names the pin: {listed}" + ); + + // The npm registry's version document for minimist@1.2.2, served + // locally (see native_binary_hosted_vendored_takeover_roundtrip). + let integrity = "sha512-rIqbOrKb8GJmx/5bc2M0QchhUouMXSpd1RTclXsB41JdL+VtnojfaJR+h7F9k18/4kHUsBFgk80Uk+q569vjPA=="; + Mock::given(method("GET")) + .and(path("/minimist/1.2.2")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({"dist": { + "tarball": "https://registry.npmjs.org/minimist/-/minimist-1.2.2.tgz", + "integrity": integrity}}))) + .mount(&server) + .await; + fixture.stage(); + let taken_over = cli_env( + project, + &[ + "vendor", + "--patch-server-url", + &uri, + "--vendor-source", + "service", + ], + &[("SOCKET_NPM_REGISTRY", &uri)], + ); + assert_eq!( + taken_over["summary"]["applied"], 1, + "online vendor over the shared hosted pin: {taken_over}" + ); + assert!( + taken_over["events"].as_array().is_some_and(|events| events + .iter() + .any(|e| e["errorCode"] == "vendor_takeover_reverted_redirect")), + "the takeover is reported: {taken_over}" + ); + assert!( + !taken_over + .to_string() + .contains("vendor_lock_entry_not_found"), + "{taken_over}" + ); + let vendor_lock = fixture.lock(); + assert!( + !vendor_lock.windows(uri.len()).any(|w| w == uri.as_bytes()), + "no hosted URL is left in bun.lockb" + ); + + let reverted = cli(project, &["vendor", "--revert", "--offline"]); + assert_eq!( + fixture.lock(), + fixture.original_lock, + "the revert restores the pre-hosted bytes exactly: {reverted}" + ); +} + #[tokio::test(flavor = "multi_thread")] #[serial_test::serial] async fn native_binary_scan_vendored() { diff --git a/crates/socket-patch-core/src/vex/discover/bun.rs b/crates/socket-patch-core/src/vex/discover/bun.rs index e8a7e7c70..fa0ba5679 100644 --- a/crates/socket-patch-core/src/vex/discover/bun.rs +++ b/crates/socket-patch-core/src/vex/discover/bun.rs @@ -212,6 +212,31 @@ impl Bundled { } } + /// Record a `bun.lockb` record Bun shares between a regular and a + /// bundled install: it is classified as the regular install, so a ref + /// it makes is kept for [`Bundled::contest`] to shadow (never attested, + /// but still a pin list / rollback / remove / the vendored takeover can + /// unwind), and its `name@version` is recorded as a bundled copy. + fn share(&mut self, ctx: &DiscoverCtx<'_>, file: &str, entry: Entry<'_>, out: &mut Discovery) { + let (label, name) = (entry.label.to_string(), entry.name); + let recorded = entry.recorded_version; + let mut alone = Discovery::default(); + classify(ctx, file, entry, &mut alone); + out.diagnostics.extend(alone.diagnostics); + let mut purls: Vec = alone.elsewhere.into_iter().map(|e| e.purl).collect(); + for r in alone.refs { + purls.push(r.purl.clone()); + out.push(r); + } + if purls.is_empty() { + purls.extend(recorded.and_then(|version| npm_purl(name, version))); + } + for purl in purls { + out.resolved_elsewhere(file, Some(purl.clone())); + self.copies.entry(purl).or_insert_with(|| label.clone()); + } + } + /// Withdraw every ref of `file` whose `name@version` a bundled copy in /// the same lock also installs: that copy stays unpatched beside it. fn contest(&self, file: &str, out: &mut Discovery) { @@ -283,9 +308,13 @@ async fn extract_binary(ctx: &DiscoverCtx<'_>, out: &mut Discovery) { // A record some bundled edge reaches installs (also) as a copy // unpacked from that parent's tarball. Bun keeps ONE record for a // regular and a bundled install of the same version, so even a - // record a regular edge also reaches is never attested. - if p.bundled { + // record a regular edge also reaches is never attested; its ref is + // still the pin the hosted writer wired for that regular install + // (#1243), so it is shadowed rather than dropped. + if p.bundled_only { bundled.record(ctx, BUN_LOCKB, classified, out); + } else if p.bundled { + bundled.share(ctx, BUN_LOCKB, classified, out); } else { unwired.record(classify(ctx, BUN_LOCKB, classified, out), &label); } @@ -1298,6 +1327,20 @@ mod tests { "{shape}: {:?}", diag_codes(&out) ); + // REGRESSION (#1243): a record Bun shares with a regular install + // is the pin the hosted writer wired for that install, so it is + // shadowed (visible to list / rollback / remove / the vendored + // takeover), like the text lock's regular entry beside a bundled + // one. A record only bundled edges reach wires nothing. + let shadowed: Vec<_> = out + .shadowed + .iter() + .map(|r| (r.purl.as_str(), r.uuid.as_str())) + .collect(); + match shape { + "both" => assert_eq!(shadowed, [("pkg:npm/is-number@7.0.0", UUID_A)], "{shape}"), + _ => assert!(shadowed.is_empty(), "{shape}: {shadowed:?}"), + } } } From bace5735028adf46f4c608285c0805d849e9d378 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 09:00:30 +0000 Subject: [PATCH 3/4] Test a hosted pin on a shared bundled bun.lockb record Bun 1.2+ e2e: a root that depends on minimist@1.2.2 and on a local parent that bundles its own minimist@1.2.2. After the hosted scan, `list` must name the pin (it exited 1 with hosted_wiring_contested), the online takeover must vendor over it (it failed with vendor_lock_entry_not_found), `vendor --revert` must restore the original bytes, and `rollback` must refuse it with the checkout remedy like any binary hosted pin. Older Bun keeps no shared record, so the leg skips there. Refs #1243 Assisted-by: Claude Code:claude-opus-5-5 --- crates/socket-patch-cli/tests/e2e_bun_lockb.rs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/crates/socket-patch-cli/tests/e2e_bun_lockb.rs b/crates/socket-patch-cli/tests/e2e_bun_lockb.rs index 060824c61..127737bed 100644 --- a/crates/socket-patch-cli/tests/e2e_bun_lockb.rs +++ b/crates/socket-patch-cli/tests/e2e_bun_lockb.rs @@ -1051,7 +1051,9 @@ async fn native_binary_shared_bundled_record_hosted_pin_is_managed() { } assert_ne!(fixture.lock(), fixture.original_lock); - let (code, listed) = cli_code(project, &["list"]); + // `--patch-server-url`: the mock serves the hosted artifact, so name + // it the hosted origin (as the vendor run below does). + let (code, listed) = cli_code(project, &["list", "--patch-server-url", &uri]); assert_eq!( code, 0, "list must accept the hosted pin it wrote: {listed}" @@ -1061,8 +1063,8 @@ async fn native_binary_shared_bundled_record_hosted_pin_is_managed() { "{listed}" ); assert!( - listed.to_string().contains(UUID), - "list names the pin: {listed}" + listed.to_string().contains(PURL), + "list names the hosted pin: {listed}" ); // The npm registry's version document for minimist@1.2.2, served @@ -1115,6 +1117,16 @@ async fn native_binary_shared_bundled_record_hosted_pin_is_managed() { fixture.original_lock, "the revert restores the pre-hosted bytes exactly: {reverted}" ); + + // `rollback` of the same pin refuses it as any binary hosted pin is + // refused (the checkout remedy), not as contested wiring. + let hosted = scan(project, &server, "hosted", &[]); + assert_eq!( + hosted["redirect"]["redirected"], 1, + "hosted again: {hosted}" + ); + rollback_refuses_binary_hosted_pin_then_checkout(&fixture, &server); + assert_eq!(fixture.lock(), fixture.original_lock); } #[tokio::test(flavor = "multi_thread")] From 71410b6c8bafd71e4604630f0be384198d53aa08 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 09:46:43 +0000 Subject: [PATCH 4/4] Run the shared bundled bun.lockb test on Bun 1.4 The Bun compatibility backtest runs every `native_binary_` test and expects exactly three to pass, so the new #1243 test's name broke all three `binary` legs. Rename it out of that prefix, and add it to the Bun 1.4.2 e2e_bun_lockb leg: the 1.0 and 1.1 legs keep no shared record and skip it, so without this no CI leg ran it for real. Refs #1243 Assisted-by: Claude Code:claude-opus-5-5 --- .github/workflows/ci.yml | 6 ++++-- crates/socket-patch-cli/tests/e2e_bun_lockb.rs | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6265beb0a..d7cd03bed 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1127,8 +1127,10 @@ jobs: - {os: ubuntu-latest, suite: e2e_bun_lockb, bun: '1.0.36', test_filter: --include-ignored} - {os: ubuntu-latest, suite: e2e_bun_lockb, bun: '1.1.45', test_filter: --include-ignored} # Bun >= 1.4 migrating a hosted workspace bun.lockb to bun.lock - # (#803): the only binary-lock test whose reader must be 1.4+. - - {os: ubuntu-latest, suite: e2e_bun_lockb, bun: '1.4.2', test_filter: --include-ignored workspace_text_migration_heals_on_rerun} + # (#803), and a hosted pin on a record Bun 1.2+ shares between a + # regular and a bundled install (#1243; older Bun keeps no shared + # record, so the 1.0/1.1 legs above skip it). + - {os: ubuntu-latest, suite: e2e_bun_lockb, bun: '1.4.2', test_filter: --include-ignored workspace_text_migration_heals_on_rerun binary_shared_bundled_record_hosted_pin_is_managed} # Real-vlt capstones (DESIGN §8.4): wiremock patch service and a local # npm registry fed from npmjs, driven by the pinned vlt release # (`node vlt.js`, installed below from a sha512-checked `npm pack`). diff --git a/crates/socket-patch-cli/tests/e2e_bun_lockb.rs b/crates/socket-patch-cli/tests/e2e_bun_lockb.rs index 127737bed..e666af744 100644 --- a/crates/socket-patch-cli/tests/e2e_bun_lockb.rs +++ b/crates/socket-patch-cli/tests/e2e_bun_lockb.rs @@ -1029,7 +1029,7 @@ async fn native_binary_hosted_vendored_takeover_roundtrip() { /// it, and `vendor --revert` gives back the pre-hosted bytes exactly. #[tokio::test(flavor = "multi_thread")] #[serial_test::serial] -async fn native_binary_shared_bundled_record_hosted_pin_is_managed() { +async fn binary_shared_bundled_record_hosted_pin_is_managed() { let Some(fixture) = Fixture::new("bundled") else { return; };