From 5ab8e870603f892b842bd45f4fcb5f54597145a7 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 3 Oct 2026 07:35:47 +0000 Subject: [PATCH 1/2] Bench: cover Bun's isolated .bun store Bun 1.3.2+ installs with the isolated linker by default, keeping every package only in node_modules/.bun/@/node_modules/. #496 taught the npm crawler (scan, apply's resolver, VEX) to walk that store, but the suite only had a hoisted Bun layout, so the new walk was never timed and a regression back to "2700 lockfile-only packages" would have gone unnoticed. Add bun-isolated/{hosted,rescan}: the same text bun.lock as bun/*, with a pnpm-shaped .bun store, per-entry dependency links, Bun's .bun/node_modules hoist links and root links for direct deps only. A pre-#496 binary fails it (lockfileOnlyPackages: got 2700, want 0). Co-Authored-By: Claude Opus 5.5 (1M context) --- crates/socket-patch-bench/README.md | 5 ++- crates/socket-patch-bench/src/fixtures/mod.rs | 7 +++ crates/socket-patch-bench/src/fixtures/npm.rs | 43 ++++++++++++++++++- 3 files changed, 52 insertions(+), 3 deletions(-) diff --git a/crates/socket-patch-bench/README.md b/crates/socket-patch-bench/README.md index 083f5ff75..51028a4d9 100644 --- a/crates/socket-patch-bench/README.md +++ b/crates/socket-patch-bench/README.md @@ -35,8 +35,9 @@ Each scenario is one project and one `scan` invocation: Package managers (``), each in its native lockfile format and install layout: `npm`, `pnpm` (isolated `.pnpm` store with symlinks), `yarn-classic`, -`yarn-berry` (node-modules linker), `bun` (text `bun.lock`), `vlt` -(`.vlt` store), `pip` (hash-pinned `requirements.txt`), `uv`, `pylock` +`yarn-berry` (node-modules linker), `bun` (text `bun.lock`, hoisted), +`bun-isolated` (the same lockfile, Bun 1.3's isolated `.bun` store), +`vlt` (`.vlt` store), `pip` (hash-pinned `requirements.txt`), `uv`, `pylock` (PEP 751), `poetry`, `pipenv`, `pdm`, `bundler`, `composer`, `cargo`, `golang`, `nuget` and `maven`. Deno has no hosted rewrite and is not benchmarked separately. diff --git a/crates/socket-patch-bench/src/fixtures/mod.rs b/crates/socket-patch-bench/src/fixtures/mod.rs index eee4f751c..8ea199430 100644 --- a/crates/socket-patch-bench/src/fixtures/mod.rs +++ b/crates/socket-patch-bench/src/fixtures/mod.rs @@ -132,6 +132,13 @@ pub static ALL: &[Pm] = &[ patched: 60, build: npm::build_bun, }, + Pm { + name: "bun-isolated", + description: "bun (text bun.lock v1, isolated node_modules/.bun store)", + packages: 3000, + patched: 60, + build: npm::build_bun_isolated, + }, Pm { name: "vlt", description: "vlt (vlt-lock.json v1, node_modules/.vlt store)", diff --git a/crates/socket-patch-bench/src/fixtures/npm.rs b/crates/socket-patch-bench/src/fixtures/npm.rs index af1ee68e4..f5da76325 100644 --- a/crates/socket-patch-bench/src/fixtures/npm.rs +++ b/crates/socket-patch-bench/src/fixtures/npm.rs @@ -1,5 +1,5 @@ //! npm-family projects. One deterministic dependency graph is written out -//! as each package manager's lockfile and install layout, so the six +//! as each package manager's lockfile and install layout, so the seven //! scenarios differ only in what each package manager puts on disk. use std::collections::BTreeMap; @@ -689,6 +689,47 @@ pub fn build_bun(t: &mut Tree, size: Size) -> std::io::Result { Ok(fixture(&g, g.patches(false), &["bun.lock"], &[])) } +/// Bun's isolated linker (the default since Bun 1.3.2): the same text +/// `bun.lock`, but every package lives only in the pnpm-shaped store +/// `node_modules/.bun/@/node_modules/` (scoped +/// `@scope+leaf@…`), its dependencies linked beside it, the root linking +/// direct dependencies only, and `.bun/node_modules` holding Bun's hoist +/// links. +pub fn build_bun_isolated(t: &mut Tree, size: Size) -> std::io::Result { + let g = graph("bun-isolated", size); + t.write("project/package.json", g.package_json())?; + t.write("project/bun.lock", bun_lock(&g))?; + let mut rng = Rng::new("bun-isolated-install"); + for p in &g.pkgs { + let entry = format!("project/node_modules/.bun/{}/node_modules", p.store_key()); + write_package(t, &format!("{entry}/{}", p.name), p, &mut rng)?; + for (d, v) in &p.deps { + let Some(dep) = g.resolve(p, d, v) else { + continue; + }; + let up = "../".repeat(1 + d.matches('/').count()); + let target = format!("{up}../{}/node_modules/{d}", dep.store_key()); + t.symlink(&target, &format!("{entry}/{d}"))?; + } + if !p.is_nested() { + let up = "../".repeat(1 + p.name.matches('/').count()); + t.symlink( + &format!("{up}{}/node_modules/{}", p.store_key(), p.name), + &format!("project/node_modules/.bun/node_modules/{}", p.name), + )?; + } + } + for p in g.direct() { + let up = "../".repeat(p.name.matches('/').count()); + t.symlink( + &format!("{up}.bun/{}/node_modules/{}", p.store_key(), p.name), + &format!("project/node_modules/{}", p.name), + )?; + } + t.mkdir("home")?; + Ok(fixture(&g, g.patches(false), &["bun.lock"], &[])) +} + // ── vlt ──────────────────────────────────────────────────────────────── fn vlt_id(p: &Pkg) -> String { From 93b1c9295b938365c30be564fb21d1f7b35de1b4 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 07:14:43 +0000 Subject: [PATCH 2/2] Restore fixture after a failed rescan prep A rescan's preparatory scan returned early on a validation failure, skipping the tree restore. The project stayed rewritten, so in `compare` the other binary's next run started from a rescanned tree and was reported INVALID for the first binary's fault: on the 2026-10-04 weekly A/B a pre-#496 base failed bun-isolated/rescan's prep and the head (valid on its own) was flagged invalid too. Restore before returning the error, as the measured run already does. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_016Gubp3gsnxFDvRbguaeLqT --- crates/socket-patch-bench/src/engine.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/crates/socket-patch-bench/src/engine.rs b/crates/socket-patch-bench/src/engine.rs index 4baf217da..bd9487e0c 100644 --- a/crates/socket-patch-bench/src/engine.rs +++ b/crates/socket-patch-bench/src/engine.rs @@ -197,8 +197,17 @@ impl<'a> Prepared<'a> { let prep = crate::process::run(self.command(bin, false), &self.capture) .map_err(|e| format!("spawning {}: {e}", bin.display()))?; let stats = self.mock.take_stats(); - validate(self, &prep, &stats, Kind::Hosted) - .map_err(|e| format!("the rescan's preparatory scan failed validation: {e}"))?; + if let Err(e) = validate(self, &prep, &stats, Kind::Hosted) { + // A failed preparatory scan can still have rewritten the + // project. Restore it, or the next run (the other binary's, + // in `compare`) starts from a rescanned tree and fails + // validation for this binary's fault. + tree::restore(&self.work, &self.pristine, &mut self.snapshot) + .map_err(|e| format!("restoring the fixture: {e}"))?; + return Err(format!( + "the rescan's preparatory scan failed validation: {e}" + )); + } } self.mock.take_stats(); let dry = self.scenario.kind == Kind::DryRun;