From 8f73a2612b823cba06b450d43b905045ef94d720 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 5 Aug 2026 23:35:27 +0000 Subject: [PATCH 1/6] install: add BUN_DISABLE_SLOW_FILESYSTEM_WARNING, set it in the test harness The "Slow filesystem detected" warning fires whenever creating and renaming a temp file in the install cache takes over 100ms, which happens regularly on loaded CI machines. Tests that assert on stderr then fail, most recently bun-run-dir.test.ts on the alpine lanes: stderrForInstall stripped the warning text but not its trailing newline, so expect(err).toBe("") received "\n". Instead of patching the filter into every test that spawns an install, add a feature flag env var (same pattern as BUN_DISABLE_SLOW_LIFECYCLE_SCRIPT_LOGGING) that suppresses the warning, and set it in bunEnv and in the CI runner's child env so every test-spawned bun is covered. Also make stderrForInstall consume the line terminator, which keeps stderr assertions stable when tests run against older binaries that do not know the flag, and filter the one unfiltered stderr read in bun-run-dir.test.ts. Verified by delaying renameat via an LD_PRELOAD shim to force the warning: the old harness reproduces the exact CI failure, the new one passes with both an old binary (regex path) and the new one (flag path). --- scripts/runner.node.mjs | 1 + src/bun_core/env_var.rs | 3 +++ .../PackageManager/PackageManagerDirectories.rs | 10 +++++++--- test/cli/install/bun-run-dir.test.ts | 4 ++-- test/harness.ts | 5 ++++- 5 files changed, 17 insertions(+), 6 deletions(-) diff --git a/scripts/runner.node.mjs b/scripts/runner.node.mjs index a04e0c5148d1..b6e5bddf8b2e 100755 --- a/scripts/runner.node.mjs +++ b/scripts/runner.node.mjs @@ -1784,6 +1784,7 @@ async function spawnBun(execPath, { args, cwd, timeout, gracefulTimeout, idleTim FORCE_COLOR: "1", BUN_FEATURE_FLAG_INTERNAL_FOR_TESTING: "1", BUN_DEBUG_QUIET_LOGS: "1", + BUN_DISABLE_SLOW_FILESYSTEM_WARNING: "1", BUN_GARBAGE_COLLECTOR_LEVEL: "1", BUN_JSC_randomIntegrityAuditRate: "1.0", BUN_RUNTIME_TRANSPILER_CACHE_PATH: "0", diff --git a/src/bun_core/env_var.rs b/src/bun_core/env_var.rs index bf8e49fb2e5a..e2c67b659ded 100644 --- a/src/bun_core/env_var.rs +++ b/src/bun_core/env_var.rs @@ -234,6 +234,9 @@ pub mod feature_flag { // Fall back to the scalar byte-at-a-time VLQ decode in // bun_sourcemap::mapping::parse (skips the Highway-dispatched path). new_feature_flag!(pub BUN_FEATURE_FLAG_DISABLE_SIMD_SOURCEMAP, "BUN_FEATURE_FLAG_DISABLE_SIMD_SOURCEMAP", {}); + // Suppress the `bun install` "Slow filesystem detected" warning. Set by the + // test harness so stderr assertions don't flake on slow CI filesystems. + new_feature_flag!(pub BUN_DISABLE_SLOW_FILESYSTEM_WARNING, "BUN_DISABLE_SLOW_FILESYSTEM_WARNING", {}); new_feature_flag!(pub BUN_DISABLE_SLOW_LIFECYCLE_SCRIPT_LOGGING, "BUN_DISABLE_SLOW_LIFECYCLE_SCRIPT_LOGGING", {}); new_feature_flag!(pub BUN_DISABLE_SOURCE_CODE_PREVIEW, "BUN_DISABLE_SOURCE_CODE_PREVIEW", {}); new_feature_flag!(pub BUN_FEATURE_FLAG_DISABLE_SOURCE_MAPS, "BUN_FEATURE_FLAG_DISABLE_SOURCE_MAPS", {}); diff --git a/src/install/PackageManager/PackageManagerDirectories.rs b/src/install/PackageManager/PackageManagerDirectories.rs index 3e075528d548..950f3ef74b54 100644 --- a/src/install/PackageManager/PackageManagerDirectories.rs +++ b/src/install/PackageManager/PackageManagerDirectories.rs @@ -183,7 +183,11 @@ fn get_temporary_directory_run(manager: &mut PackageManager) -> TemporaryDirecto let tmpname = FileSystem::tmpname(b"hm", &mut tmpbuf, bun_core::fast_random()).expect("unreachable"); - let mut timer = if manager.options.log_level != LogLevel::Silent { + let mut timer = if manager.options.log_level != LogLevel::Silent + && !bun_core::env_var::feature_flag::BUN_DISABLE_SLOW_FILESYSTEM_WARNING + .get() + .unwrap_or(false) + { Some(bun_core::time::Timer::start()) } else { None @@ -272,8 +276,8 @@ fn get_temporary_directory_run(manager: &mut PackageManager) -> TemporaryDirecto break; } - if manager.options.log_level != LogLevel::Silent { - let elapsed = timer.as_mut().unwrap().read(); + if let Some(timer) = timer.as_mut() { + let elapsed = timer.read(); if elapsed > bun_core::time::NS_PER_MS * 100 { let mut path_buf = PathBuffer::uninit(); let cache_dir_path: &[u8] = match sys::get_fd_path(cache_directory_fd, &mut path_buf) { diff --git a/test/cli/install/bun-run-dir.test.ts b/test/cli/install/bun-run-dir.test.ts index d4b6b81ae830..f0bfe1791914 100644 --- a/test/cli/install/bun-run-dir.test.ts +++ b/test/cli/install/bun-run-dir.test.ts @@ -127,8 +127,8 @@ for (const entry of await decompress(Buffer.from(buffer))) { BUN_INSTALL_CACHE_DIR: join(run_dir, ".cache"), }, }); - const err2 = await new Response(stderr2).text(); - if (err2) throw new Error(err2); + const err2 = stderrForInstall(await new Response(stderr2).text()); + expect(err2).toBe(""); expect(await readdirSorted(run_dir)).toEqual([".cache", "test.js"]); expect(await readdirSorted(join(run_dir, ".cache"))).toContain("decompress"); expect(await readdirSorted(join(run_dir, ".cache", "decompress"))).toEqual(["4.2.1@@@1"]); diff --git a/test/harness.ts b/test/harness.ts index ae197e7bbba2..e9d0bc5d6e5b 100644 --- a/test/harness.ts +++ b/test/harness.ts @@ -74,6 +74,9 @@ export const bunEnv: NodeJS.Dict = { CI: "1", BUN_RUNTIME_TRANSPILER_CACHE_PATH: "0", BUN_FEATURE_FLAG_INTERNAL_FOR_TESTING: "1", + // The `bun install` "Slow filesystem detected" warning is timing-dependent + // and flakes stderr assertions on slow CI filesystems. + BUN_DISABLE_SLOW_FILESYSTEM_WARNING: "1", // Tests drive `bun update --interactive` by writing keystrokes to a pipe; // the real command refuses on non-TTY stdin. Bypass that gate under test. BUN_INTERNAL_INTERACTIVE_ASSUME_TTY: "1", @@ -1513,7 +1516,7 @@ export async function runBunInstall( // stderr with `slow filesystem` warning removed export function stderrForInstall(err: string) { - return err.replace(/warn: Slow filesystem.*/g, ""); + return err.replace(/warn: Slow filesystem.*\r?\n?/g, ""); } export async function runBunUpdate( From 2044dc81397f30e4b5326cc76025d6c443180258 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Thu, 6 Aug 2026 00:09:41 +0000 Subject: [PATCH 2/6] test: remove stderrForInstall, add a deterministic test for the warning knob With BUN_DISABLE_SLOW_FILESYSTEM_WARNING set by the harness and the CI runner, the slow filesystem warning can no longer reach test stderr, so the per-call-site filtering is dead weight. Remove the helper and unwrap its call sites. Add a test that forces the slow path deterministically (an LD_PRELOAD shim compiled at test time delays renameat past the 100ms threshold) and asserts the warning fires without the knob and stays silent with it. Linux glibc only, skipped when no C compiler is available. Also raise the file's default timeout: its auto-install tests download from the npm registry, which exceeds the 5s default under sanitizer builds on slow networks. --- .../bun-install-lifecycle-scripts.test.ts | 51 ++++++------ test/cli/install/bun-install-registry.test.ts | 11 ++- test/cli/install/bun-link.test.ts | 37 ++++----- test/cli/install/bun-lockb.test.ts | 14 ++-- test/cli/install/bun-publish.test.ts | 13 +-- test/cli/install/bun-run-dir.test.ts | 83 +++++++++++++++++-- test/cli/install/catalogs.test.ts | 10 +-- test/cli/install/config-version.test.ts | 4 +- test/cli/install/npmrc.test.ts | 4 +- test/harness.ts | 7 +- 10 files changed, 143 insertions(+), 91 deletions(-) diff --git a/test/cli/install/bun-install-lifecycle-scripts.test.ts b/test/cli/install/bun-install-lifecycle-scripts.test.ts index 8123acc24bdf..73b280bdb58c 100644 --- a/test/cli/install/bun-install-lifecycle-scripts.test.ts +++ b/test/cli/install/bun-install-lifecycle-scripts.test.ts @@ -10,7 +10,6 @@ import { isWindows, readdirSorted, runBunInstall, - stderrForInstall, } from "harness"; import { join, sep } from "path"; @@ -1716,7 +1715,7 @@ for (const forceWaiterThread of isLinux ? [false, true] : [false]) { env: testEnv, })); - err = stderrForInstall(await stderr.text()); + err = await stderr.text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); @@ -2025,7 +2024,7 @@ for (const forceWaiterThread of isLinux ? [false, true] : [false]) { env: testEnv, })); - err = stderrForInstall(await stderr.text()); + err = await stderr.text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); @@ -2294,7 +2293,7 @@ for (const forceWaiterThread of isLinux ? [false, true] : [false]) { env: testEnv, })); - err = stderrForInstall(await stderr.text()); + err = await stderr.text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); @@ -3184,7 +3183,7 @@ for (const forceWaiterThread of isLinux ? [false, true] : [false]) { env: testEnv, }); - let err = stderrForInstall(await stderr.text()); + let err = await stderr.text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); @@ -3217,7 +3216,7 @@ for (const forceWaiterThread of isLinux ? [false, true] : [false]) { env: testEnv, })); - err = stderrForInstall(await stderr.text()); + err = await stderr.text(); expect(err).not.toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); @@ -3253,7 +3252,7 @@ for (const forceWaiterThread of isLinux ? [false, true] : [false]) { env: testEnv, }); - const err = stderrForInstall(await stderr.text()); + const err = await stderr.text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); @@ -3295,7 +3294,7 @@ for (const forceWaiterThread of isLinux ? [false, true] : [false]) { env: testEnv, }); - let err = stderrForInstall(await stderr.text()); + let err = await stderr.text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); @@ -3384,7 +3383,7 @@ for (const forceWaiterThread of isLinux ? [false, true] : [false]) { env: testEnv, }); - let err = stderrForInstall(await stderr.text()); + let err = await stderr.text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); @@ -3419,7 +3418,7 @@ for (const forceWaiterThread of isLinux ? [false, true] : [false]) { env: testEnv, })); - err = stderrForInstall(await stderr.text()); + err = await stderr.text(); expect(err).not.toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); @@ -3459,7 +3458,7 @@ for (const forceWaiterThread of isLinux ? [false, true] : [false]) { env: testEnv, }); - let err = stderrForInstall(await stderr.text()); + let err = await stderr.text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); @@ -3506,7 +3505,7 @@ for (const forceWaiterThread of isLinux ? [false, true] : [false]) { env: testEnv, })); - err = stderrForInstall(await stderr.text()); + err = await stderr.text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); @@ -3553,7 +3552,7 @@ for (const forceWaiterThread of isLinux ? [false, true] : [false]) { env: testEnv, }); - let err = stderrForInstall(await stderr.text()); + let err = await stderr.text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); @@ -3602,7 +3601,7 @@ for (const forceWaiterThread of isLinux ? [false, true] : [false]) { env: testEnv, })); - err = stderrForInstall(await stderr.text()); + err = await stderr.text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); @@ -3650,7 +3649,7 @@ for (const forceWaiterThread of isLinux ? [false, true] : [false]) { env.PATH = originalPath; - let err = stderrForInstall(await stderr.text()); + let err = await stderr.text(); expect(err).toContain("No packages! Deleted empty lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); @@ -3691,7 +3690,7 @@ for (const forceWaiterThread of isLinux ? [false, true] : [false]) { env.PATH = originalPath; - let err = stderrForInstall(await stderr.text()); + let err = await stderr.text(); expect(err).toContain("No packages! Deleted empty lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); @@ -3723,7 +3722,7 @@ for (const forceWaiterThread of isLinux ? [false, true] : [false]) { env: testEnv, }); - let err = stderrForInstall(await stderr.text()); + let err = await stderr.text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("error:"); expect(err).not.toContain("warn:"); @@ -3753,7 +3752,7 @@ for (const forceWaiterThread of isLinux ? [false, true] : [false]) { env: testEnv, })); - err = stderrForInstall(await stderr.text()); + err = await stderr.text(); expect(err).toContain("bun pm untrusted"); expect(err).not.toContain("error:"); expect(err).not.toContain("warn:"); @@ -3806,7 +3805,7 @@ for (const forceWaiterThread of isLinux ? [false, true] : [false]) { env: testEnv, }); - let err = stderrForInstall(await stderr.text()); + let err = await stderr.text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); @@ -3836,7 +3835,7 @@ for (const forceWaiterThread of isLinux ? [false, true] : [false]) { env: testEnv, })); - err = stderrForInstall(await stderr.text()); + err = await stderr.text(); expect(err).not.toContain("error:"); expect(err).not.toContain("warn:"); out = await stdout.text(); @@ -3863,7 +3862,7 @@ for (const forceWaiterThread of isLinux ? [false, true] : [false]) { env: testEnv, })); - err = stderrForInstall(await stderr.text()); + err = await stderr.text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); @@ -3891,7 +3890,7 @@ for (const forceWaiterThread of isLinux ? [false, true] : [false]) { env: testEnv, })); - err = stderrForInstall(await stderr.text()); + err = await stderr.text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); @@ -3926,7 +3925,7 @@ for (const forceWaiterThread of isLinux ? [false, true] : [false]) { env: testEnv, })); - err = stderrForInstall(await stderr.text()); + err = await stderr.text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); @@ -3954,7 +3953,7 @@ for (const forceWaiterThread of isLinux ? [false, true] : [false]) { env: testEnv, })); - err = stderrForInstall(await stderr.text()); + err = await stderr.text(); expect(err).not.toContain("error:"); expect(err).not.toContain("warn:"); out = await stdout.text(); @@ -4072,7 +4071,7 @@ for (const forceWaiterThread of isLinux ? [false, true] : [false]) { env: testEnv, }); - const err = stderrForInstall(await stderr.text()); + const err = await stderr.text(); expect(err).not.toContain("error:"); expect(err).not.toContain("warn:"); expect(splitErrLines(err)).toEqual([ @@ -4127,7 +4126,7 @@ for (const forceWaiterThread of isLinux ? [false, true] : [false]) { env: testEnv, }); - const err = stderrForInstall(await stderr.text()); + const err = await stderr.text(); expect(err).not.toContain("error:"); expect(err).not.toContain("warn:"); expect(splitErrLines(err)).toEqual([ diff --git a/test/cli/install/bun-install-registry.test.ts b/test/cli/install/bun-install-registry.test.ts index 5b0bdeecf241..b82c40ff48bd 100644 --- a/test/cli/install/bun-install-registry.test.ts +++ b/test/cli/install/bun-install-registry.test.ts @@ -14,7 +14,6 @@ import { readdirSorted, runBunInstall, runBunUpdate, - stderrForInstall, tempDir, tls, tmpdirSync, @@ -212,7 +211,7 @@ describe("certificate authority", () => { env, }); let out = await stdout.text(); - let err = stderrForInstall(await stderr.text()); + let err = await stderr.text(); expect(err).toContain("DEPTH_ZERO_SELF_SIGNED_CERT"); expect(await exited).toBe(1); @@ -6239,7 +6238,7 @@ describe("pm trust", async () => { env, }); - let err = stderrForInstall(await stderr.text()); + let err = await stderr.text(); expect(err).not.toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); @@ -6266,7 +6265,7 @@ describe("pm trust", async () => { env, }); - let err = stderrForInstall(await stderr.text()); + let err = await stderr.text(); expect(err).toContain("error: Lockfile not found"); let out = await stdout.text(); expect(out).toBeEmpty(); @@ -6292,7 +6291,7 @@ describe("pm trust", async () => { env, }); - let err = stderrForInstall(await stderr.text()); + let err = await stderr.text(); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); expect(err).not.toContain("warn:"); @@ -6319,7 +6318,7 @@ describe("pm trust", async () => { env, })); - err = stderrForInstall(await stderr.text()); + err = await stderr.text(); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); expect(err).not.toContain("warn:"); diff --git a/test/cli/install/bun-link.test.ts b/test/cli/install/bun-link.test.ts index 76438f8dadf1..8a937dad63fd 100644 --- a/test/cli/install/bun-link.test.ts +++ b/test/cli/install/bun-link.test.ts @@ -7,7 +7,6 @@ import { isWindows, readdirSorted, runBunInstall, - stderrForInstall, tmpdirSync, toBeValidBin, toHaveBins, @@ -75,7 +74,7 @@ it("should link and unlink workspace package", async () => { env, }); - err = stderrForInstall(await stderr.text()); + err = await stderr.text(); expect(err.split(/\r?\n/)).toEqual([""]); expect(await stdout.text()).toContain(`Success! Registered "moo"`); expect(await exited).toBe(0); @@ -89,7 +88,7 @@ it("should link and unlink workspace package", async () => { env, })); - err = stderrForInstall(await stderr.text()); + err = await stderr.text(); expect(err.split(/\r?\n/)).toEqual([""]); expect((await stdout.text()).replace(/\s*\[[0-9\.]+ms\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun link v1."), @@ -113,7 +112,7 @@ it("should link and unlink workspace package", async () => { env, })); - err = stderrForInstall(await stderr.text()); + err = await stderr.text(); expect(err.split(/\r?\n/)).toEqual([""]); expect(await stdout.text()).toContain(`success: unlinked package "moo"`); expect(await exited).toBe(0); @@ -128,7 +127,7 @@ it("should link and unlink workspace package", async () => { env, })); - err = stderrForInstall(await stderr.text()); + err = await stderr.text(); expect(err.split(/\r?\n/)).toEqual([""]); expect(await stdout.text()).toContain(`Success! Registered "foo"`); expect(await exited).toBe(0); @@ -142,7 +141,7 @@ it("should link and unlink workspace package", async () => { env, })); - err = stderrForInstall(await stderr.text()); + err = await stderr.text(); expect(err.split(/\r?\n/)).toEqual([""]); expect((await stdout.text()).replace(/\s*\[[0-9\.]+ms\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun link v1."), @@ -167,7 +166,7 @@ it("should link and unlink workspace package", async () => { env, })); - err = stderrForInstall(await stderr.text()); + err = await stderr.text(); expect(err.split(/\r?\n/)).toEqual([""]); expect(await stdout.text()).toContain(`success: unlinked package "foo"`); expect(await exited).toBe(0); @@ -202,7 +201,7 @@ it("should link package", async () => { stderr: "pipe", env, }); - const err1 = stderrForInstall(await new Response(stderr1).text()); + const err1 = await new Response(stderr1).text(); expect(err1.split(/\r?\n/)).toEqual([""]); expect(await new Response(stdout1).text()).toContain(`Success! Registered "${link_name}"`); expect(await exited1).toBe(0); @@ -219,7 +218,7 @@ it("should link package", async () => { stderr: "pipe", env, }); - const err2 = stderrForInstall(await new Response(stderr2).text()); + const err2 = await new Response(stderr2).text(); expect(err2.split(/\r?\n/)).toEqual([""]); const out2 = await new Response(stdout2).text(); expect(out2.replace(/\s*\[[0-9\.]+ms\]\s*$/, "").split(/\r?\n/)).toEqual([ @@ -243,7 +242,7 @@ it("should link package", async () => { stderr: "pipe", env, }); - const err3 = stderrForInstall(await new Response(stderr3).text()); + const err3 = await new Response(stderr3).text(); expect(err3.split(/\r?\n/)).toEqual([""]); expect(await new Response(stdout3).text()).toContain(`success: unlinked package "${link_name}"`); expect(await exited3).toBe(0); @@ -260,7 +259,7 @@ it("should link package", async () => { stderr: "pipe", env, }); - const err4 = stderrForInstall(await new Response(stderr4).text()); + const err4 = await new Response(stderr4).text(); expect(err4).toContain(`error: Package "${link_name}" is not linked`); expect(await new Response(stdout4).text()).toEqual(expect.stringContaining("bun link v1.")); expect(await exited4).toBe(1); @@ -295,7 +294,7 @@ it("should link scoped package", async () => { stderr: "pipe", env, }); - const err1 = stderrForInstall(await new Response(stderr1).text()); + const err1 = await new Response(stderr1).text(); expect(err1.split(/\r?\n/)).toEqual([""]); expect(await new Response(stdout1).text()).toContain(`Success! Registered "${link_name}"`); expect(await exited1).toBe(0); @@ -312,7 +311,7 @@ it("should link scoped package", async () => { stderr: "pipe", env, }); - const err2 = stderrForInstall(await new Response(stderr2).text()); + const err2 = await new Response(stderr2).text(); expect(err2.split(/\r?\n/)).toEqual([""]); const out2 = await new Response(stdout2).text(); expect(out2.replace(/\s*\[[0-9\.]+ms\]\s*$/, "").split(/\r?\n/)).toEqual([ @@ -336,7 +335,7 @@ it("should link scoped package", async () => { stderr: "pipe", env, }); - const err3 = stderrForInstall(await new Response(stderr3).text()); + const err3 = await new Response(stderr3).text(); expect(err3.split(/\r?\n/)).toEqual([""]); expect(await new Response(stdout3).text()).toContain(`success: unlinked package "${link_name}"`); expect(await exited3).toBe(0); @@ -353,7 +352,7 @@ it("should link scoped package", async () => { stderr: "pipe", env, }); - const err4 = stderrForInstall(await new Response(stderr4).text()); + const err4 = await new Response(stderr4).text(); expect(err4).toContain(`error: Package "${link_name}" is not linked`); expect((await new Response(stdout4).text()).split(/\r?\n/)).toEqual([expect.stringContaining("bun link v1."), ""]); expect(await exited4).toBe(1); @@ -396,13 +395,13 @@ it("should link dependency without crashing", async () => { stderr: "pipe", env, }); - const err1 = stderrForInstall(await new Response(stderr1).text()); + const err1 = await new Response(stderr1).text(); expect(err1.split(/\r?\n/)).toEqual([""]); expect(await new Response(stdout1).text()).toContain(`Success! Registered "${link_name}"`); expect(await exited1).toBe(0); const { out: stdout2, err: stderr2, exited: exited2 } = await runBunInstall(env, package_dir); - const err2 = stderrForInstall(await new Response(stderr2).text()); + const err2 = await new Response(stderr2).text(); expect(err2.split(/\r?\n/).slice(-2)).toEqual(["Saved lockfile", ""]); const out2 = await new Response(stdout2).text(); expect(out2.replace(/\s*\[[0-9\.]+ms\]\s*$/, "").split(/\r?\n/)).toEqual([ @@ -441,7 +440,7 @@ it("should link dependency without crashing", async () => { stderr: "pipe", env, }); - const err3 = stderrForInstall(await new Response(stderr3).text()); + const err3 = await new Response(stderr3).text(); expect(err3.split(/\r?\n/)).toEqual([""]); expect(await new Response(stdout3).text()).toContain(`success: unlinked package "${link_name}"`); expect(await exited3).toBe(0); @@ -458,7 +457,7 @@ it("should link dependency without crashing", async () => { stderr: "pipe", env, }); - const err4 = stderrForInstall(await new Response(stderr4).text()); + const err4 = await new Response(stderr4).text(); expect(err4).toContain(`FileNotFound: failed linking dependency/workspace to node_modules for package ${link_name}`); const out4 = await new Response(stdout4).text(); expect(out4.replace(/\[[0-9\.]+m?s\]/, "[]").split(/\r?\n/)).toEqual([ diff --git a/test/cli/install/bun-lockb.test.ts b/test/cli/install/bun-lockb.test.ts index 60a59f10a057..a3cc19ef1f9d 100644 --- a/test/cli/install/bun-lockb.test.ts +++ b/test/cli/install/bun-lockb.test.ts @@ -1,7 +1,7 @@ import { file, spawn, write } from "bun"; import { afterAll, beforeAll, expect, it } from "bun:test"; import { copyFile, exists, open, rm, writeFile } from "fs/promises"; -import { bunExe, bunEnv as env, isWindows, runBunInstall, stderrForInstall, VerdaccioRegistry } from "harness"; +import { bunExe, bunEnv as env, isWindows, runBunInstall, VerdaccioRegistry } from "harness"; import { join } from "path"; const registry = new VerdaccioRegistry(); @@ -162,7 +162,7 @@ it("recovers from a corrupted binary lockfile instead of panicking", async () => env, }); const [out, rawErr, code] = await Promise.all([stdout.text(), stderr.text(), exited]); - const err = stderrForInstall(rawErr); + const err = rawErr; // The garbage `meta.id` deserialized from the corrupt lockfile used to // panic_bounds_check in Package::clone. Released Bun tolerates it: it @@ -248,7 +248,7 @@ index d156130662798530e852e1afaec5b1c03d429cdc..b4ddf35975a952fdaed99f2b14236519 env, }); const [out, rawErr, code] = await Promise.all([stdout.text(), stderr.text(), exited]); - const err = stderrForInstall(rawErr); + const err = rawErr; // The out-of-range flag byte must fail lockfile parsing so the install // falls back to a fresh resolve instead of consuming the bad byte. @@ -307,7 +307,7 @@ it("rejects a binary lockfile whose package scripts flag byte is out of range", env, }); const [out, rawErr, code] = await Promise.all([stdout.text(), stderr.text(), exited]); - const err = stderrForInstall(rawErr); + const err = rawErr; expect(err).toContain("invalid package scripts"); expect(err).toContain("Ignoring lockfile"); @@ -363,7 +363,7 @@ it("rejects a binary lockfile whose git resolved tag contains path separators", env: installEnv, }); const [out, rawErr, code] = await Promise.all([stdout.text(), stderr.text(), exited]); - const err = stderrForInstall(rawErr); + const err = rawErr; expect(err).toContain("Saved lockfile"); expect(err).not.toContain("error:"); expect(out).toBeDefined(); @@ -385,7 +385,7 @@ it("rejects a binary lockfile whose git resolved tag contains path separators", env: installEnv, }); const [out, rawErr, code] = await Promise.all([stdout.text(), stderr.text(), exited]); - const err = stderrForInstall(rawErr); + const err = rawErr; expect(err).not.toContain("Invalid git dependency tag"); expect(err).not.toContain("error:"); expect(out).toBeDefined(); @@ -413,7 +413,7 @@ it("rejects a binary lockfile whose git resolved tag contains path separators", env: installEnv, }); const [out, rawErr, code] = await Promise.all([stdout.text(), stderr.text(), exited]); - const err = stderrForInstall(rawErr); + const err = rawErr; // The tampered resolved value must fail binary lockfile loading (the same // fail-closed rule the text lockfile parser applies) instead of flowing into diff --git a/test/cli/install/bun-publish.test.ts b/test/cli/install/bun-publish.test.ts index 1306b9e93e2d..d44911ffa962 100644 --- a/test/cli/install/bun-publish.test.ts +++ b/test/cli/install/bun-publish.test.ts @@ -1,16 +1,7 @@ import { file, spawn, write } from "bun"; import { afterAll, beforeAll, describe, expect, it, test } from "bun:test"; import { exists, rm } from "fs/promises"; -import { - VerdaccioRegistry, - bunExe, - bunEnv as env, - isWindows, - pack, - runBunInstall, - stderrForInstall, - tmpdirSync, -} from "harness"; +import { VerdaccioRegistry, bunExe, bunEnv as env, isWindows, pack, runBunInstall, tmpdirSync } from "harness"; import { join } from "path"; const registry = new VerdaccioRegistry(); @@ -37,7 +28,7 @@ export async function publish( }); const out = await stdout.text(); - const err = stderrForInstall(await stderr.text()); + const err = await stderr.text(); const exitCode = await exited; return { out, err, exitCode }; } diff --git a/test/cli/install/bun-run-dir.test.ts b/test/cli/install/bun-run-dir.test.ts index f0bfe1791914..78888784d7a0 100644 --- a/test/cli/install/bun-run-dir.test.ts +++ b/test/cli/install/bun-run-dir.test.ts @@ -1,9 +1,13 @@ import { file, spawn } from "bun"; -import { expect, it } from "bun:test"; -import { exists, writeFile } from "fs/promises"; -import { bunExe, bunEnv as env, readdirSorted, stderrForInstall, tmpdirSync } from "harness"; +import { expect, it, setDefaultTimeout } from "bun:test"; +import { exists, mkdir, writeFile } from "fs/promises"; +import { bunExe, bunEnv as env, isLinux, isMusl, readdirSorted, tmpdirSync } from "harness"; import { join } from "path"; +// These tests auto-install from the npm registry, which can exceed the default +// timeout under slow networks and sanitizer builds. +setDefaultTimeout(90_000); + it.concurrent("should download dependency to run local file", async () => { const run_dir = tmpdirSync(); await writeFile( @@ -29,7 +33,7 @@ console.log(minify("print(6 * 7)").code); BUN_INSTALL_CACHE_DIR: join(run_dir, ".cache"), }, }); - const err1 = stderrForInstall(await new Response(stderr1).text()); + const err1 = await new Response(stderr1).text(); expect(err1).toBe(""); expect(await readdirSorted(run_dir)).toEqual([".cache", "test.js"]); expect(await readdirSorted(join(run_dir, ".cache"))).toContain("uglify-js"); @@ -54,7 +58,7 @@ console.log(minify("print(6 * 7)").code); BUN_INSTALL_CACHE_DIR: join(run_dir, ".cache"), }, }); - const err2 = stderrForInstall(await new Response(stderr2).text()); + const err2 = await new Response(stderr2).text(); expect(err2).toBe(""); expect(await readdirSorted(run_dir)).toEqual([".cache", "test.js"]); expect(await readdirSorted(join(run_dir, ".cache"))).toContain("uglify-js"); @@ -94,7 +98,7 @@ for (const entry of await decompress(Buffer.from(buffer))) { BUN_INSTALL_CACHE_DIR: join(run_dir, ".cache"), }, }); - const err1 = stderrForInstall(await new Response(stderr1).text()); + const err1 = await new Response(stderr1).text(); expect(err1).toBe(""); expect(await readdirSorted(run_dir)).toEqual([".cache", "test.js"]); expect(await readdirSorted(join(run_dir, ".cache"))).toContain("decompress"); @@ -127,7 +131,7 @@ for (const entry of await decompress(Buffer.from(buffer))) { BUN_INSTALL_CACHE_DIR: join(run_dir, ".cache"), }, }); - const err2 = stderrForInstall(await new Response(stderr2).text()); + const err2 = await new Response(stderr2).text(); expect(err2).toBe(""); expect(await readdirSorted(run_dir)).toEqual([".cache", "test.js"]); expect(await readdirSorted(join(run_dir, ".cache"))).toContain("decompress"); @@ -168,3 +172,68 @@ import { prueba } from "pruebadfasdfasdkafasdyuif.js"; // The exit code will not be 1 if it panics. expect(await exited).toBe(1); }); + +// The "Slow filesystem detected" warning fires when populating the install +// cache takes over 100ms, which used to flake stderr assertions in this file +// on slow CI machines. Force the slow path deterministically by delaying +// renameat with an LD_PRELOAD shim, and assert the harness env knob +// (BUN_DISABLE_SLOW_FILESYSTEM_WARNING, set in bunEnv) suppresses it. +const compiler = isLinux && !isMusl ? (Bun.which("cc") ?? Bun.which("gcc") ?? Bun.which("clang")) : null; +it.skipIf(!compiler)("BUN_DISABLE_SLOW_FILESYSTEM_WARNING suppresses the slow filesystem warning", async () => { + const run_dir = tmpdirSync(); + await writeFile( + join(run_dir, "slow_rename.c"), + `#define _GNU_SOURCE +#include +#include +int renameat(int olddirfd, const char *oldpath, int newdirfd, const char *newpath) { + static int (*real)(int, const char *, int, const char *); + if (!real) real = dlsym(RTLD_NEXT, "renameat"); + usleep(250000); + return real(olddirfd, oldpath, newdirfd, newpath); +} +`, + ); + await using cc = spawn({ + cmd: [compiler!, "-shared", "-fPIC", "-o", "slow_rename.so", "slow_rename.c", "-ldl"], + cwd: run_dir, + stdout: "pipe", + stderr: "pipe", + env, + }); + expect(await cc.exited).toBe(0); + + // Each run gets a fresh project and cache so the install always populates + // the cache (a satisfied node_modules would skip the timed path). + const install = async (name: string, flag: string | undefined) => { + const proj = join(run_dir, name); + await mkdir(proj); + await writeFile( + join(proj, "package.json"), + JSON.stringify({ + name, + dependencies: { baz: `file:${join(import.meta.dir, "baz-0.0.3.tgz")}` }, + }), + ); + await using proc = spawn({ + cmd: [bunExe(), "install", "--no-save"], + cwd: proj, + stdout: "pipe", + stderr: "pipe", + env: { + ...env, + LD_PRELOAD: join(run_dir, "slow_rename.so"), + BUN_INSTALL_CACHE_DIR: join(proj, ".cache"), + BUN_DISABLE_SLOW_FILESYSTEM_WARNING: flag, + }, + }); + const [stderr, exitCode] = await Promise.all([new Response(proc.stderr).text(), proc.exited]); + expect(exitCode).toBe(0); + return stderr; + }; + + // Sanity: with the knob unset, the delayed renameat must trigger the warning. + expect(await install("warn", undefined)).toContain("Slow filesystem detected"); + // With the knob set (as bunEnv does for every test), it must stay silent. + expect(await install("quiet", "1")).not.toContain("Slow filesystem"); +}); diff --git a/test/cli/install/catalogs.test.ts b/test/cli/install/catalogs.test.ts index 1169bbf81b7b..c7f0480c1dbc 100644 --- a/test/cli/install/catalogs.test.ts +++ b/test/cli/install/catalogs.test.ts @@ -1,7 +1,7 @@ import { file, spawn, write } from "bun"; import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { exists } from "fs/promises"; -import { VerdaccioRegistry, bunEnv, bunExe, runBunInstall, stderrForInstall } from "harness"; +import { VerdaccioRegistry, bunEnv, bunExe, runBunInstall } from "harness"; import { join } from "path"; var registry = new VerdaccioRegistry(); @@ -222,7 +222,7 @@ describe("update", () => { }); const [out, err, exitCode] = await Promise.all([stdout.text(), stderr.text(), exited]); - return { out, err: stderrForInstall(err), exitCode }; + return { out, err: err, exitCode }; } // https://github.com/oven-sh/bun/issues/23739 @@ -280,7 +280,7 @@ describe("update", () => { env: bunEnv, }); const [, errText, exitCode] = await Promise.all([stdout.text(), stderr.text(), exited]); - const err = stderrForInstall(errText); + const err = errText; expect(err).not.toContain("lockfile had changes"); expect(err).not.toContain("error:"); expect(exitCode).toBe(0); @@ -545,7 +545,7 @@ describe("errors", () => { }); const out = await stdout.text(); - const err = stderrForInstall(await stderr.text()); + const err = await stderr.text(); expect(err).toContain("no-deps@catalog: failed to resolve"); expect(err).toContain("a-dep@catalog:aaaaaaaaaaaaaaaaa failed to resolve"); @@ -577,7 +577,7 @@ describe("errors", () => { }); const out = await stdout.text(); - const err = stderrForInstall(await stderr.text()); + const err = await stderr.text(); expect(err).toContain("no-deps@catalog: failed to resolve"); }); diff --git a/test/cli/install/config-version.test.ts b/test/cli/install/config-version.test.ts index 40924857f4ff..2e0cc0b328cf 100644 --- a/test/cli/install/config-version.test.ts +++ b/test/cli/install/config-version.test.ts @@ -1,7 +1,7 @@ import { file, spawn } from "bun"; import { describe, expect, test } from "bun:test"; import { exists } from "fs/promises"; -import { bunEnv, bunExe, normalizeBunSnapshot, stderrForInstall, tempDir } from "harness"; +import { bunEnv, bunExe, normalizeBunSnapshot, tempDir } from "harness"; import { join } from "path"; // These tests cover the `configVersion` field in bun.lock and the linker @@ -18,7 +18,7 @@ async function install(cwd: string) { stderr: "pipe", }); const [out, err, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); - return { out, err: stderrForInstall(err), exitCode }; + return { out, err: err, exitCode }; } describe.concurrent("configVersion", () => { diff --git a/test/cli/install/npmrc.test.ts b/test/cli/install/npmrc.test.ts index 2100be7ce978..8a3793c19019 100644 --- a/test/cli/install/npmrc.test.ts +++ b/test/cli/install/npmrc.test.ts @@ -1,7 +1,7 @@ import { write } from "bun"; import { afterAll, beforeAll, describe, expect, it, test } from "bun:test"; import { rm } from "fs/promises"; -import { VerdaccioRegistry, bunExe, bunEnv as env, stderrForInstall, tempDir } from "harness"; +import { VerdaccioRegistry, bunExe, bunEnv as env, tempDir } from "harness"; import { join } from "path"; const { iniInternals } = require("bun:internal-for-testing"); const { loadNpmrc } = iniInternals; @@ -40,7 +40,7 @@ describe("npmrc", async () => { env.BUN_INSTALL_CACHE_DIR = originalCacheDir; const out = await stdout.text(); - const err = stderrForInstall(await stderr.text()); + const err = await stderr.text(); console.log({ out, err }); expect(err).toBeEmpty(); expect(out.endsWith("hi!")).toBeTrue(); diff --git a/test/harness.ts b/test/harness.ts index e9d0bc5d6e5b..0bd8d9e18208 100644 --- a/test/harness.ts +++ b/test/harness.ts @@ -1498,7 +1498,7 @@ export async function runBunInstall( }); expect(stdout).toBeDefined(); expect(stderr).toBeDefined(); - let err: string = stderrForInstall(await stderr.text()); + let err: string = await stderr.text(); expect(err).not.toContain("panic:"); if (!options?.allowErrors) { expect(err).not.toContain("error:"); @@ -1514,11 +1514,6 @@ export async function runBunInstall( return { out, err, exited }; } -// stderr with `slow filesystem` warning removed -export function stderrForInstall(err: string) { - return err.replace(/warn: Slow filesystem.*\r?\n?/g, ""); -} - export async function runBunUpdate( env: NodeJS.ProcessEnv, cwd: string, From a437049da7ab407451af49adc427face4e0484ba Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Thu, 6 Aug 2026 00:10:18 +0000 Subject: [PATCH 3/6] Tighten the feature flag comment --- src/bun_core/env_var.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/bun_core/env_var.rs b/src/bun_core/env_var.rs index e2c67b659ded..9d0cfef32b36 100644 --- a/src/bun_core/env_var.rs +++ b/src/bun_core/env_var.rs @@ -234,8 +234,7 @@ pub mod feature_flag { // Fall back to the scalar byte-at-a-time VLQ decode in // bun_sourcemap::mapping::parse (skips the Highway-dispatched path). new_feature_flag!(pub BUN_FEATURE_FLAG_DISABLE_SIMD_SOURCEMAP, "BUN_FEATURE_FLAG_DISABLE_SIMD_SOURCEMAP", {}); - // Suppress the `bun install` "Slow filesystem detected" warning. Set by the - // test harness so stderr assertions don't flake on slow CI filesystems. + // Set by the test harness so stderr assertions don't flake on slow CI filesystems. new_feature_flag!(pub BUN_DISABLE_SLOW_FILESYSTEM_WARNING, "BUN_DISABLE_SLOW_FILESYSTEM_WARNING", {}); new_feature_flag!(pub BUN_DISABLE_SLOW_LIFECYCLE_SCRIPT_LOGGING, "BUN_DISABLE_SLOW_LIFECYCLE_SCRIPT_LOGGING", {}); new_feature_flag!(pub BUN_DISABLE_SOURCE_CODE_PREVIEW, "BUN_DISABLE_SOURCE_CODE_PREVIEW", {}); From d3fc71caa8b346cbcd82fd74555d147bfd0714af Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Thu, 6 Aug 2026 00:24:01 +0000 Subject: [PATCH 4/6] test: drain subprocess stderr and assert content before exit codes The compiler spawn piped stdout/stderr without reading them, and the install helper asserted the exit code before callers saw stderr, so a failure reported only the code with the diagnostics swallowed. --- test/cli/install/bun-run-dir.test.ts | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/test/cli/install/bun-run-dir.test.ts b/test/cli/install/bun-run-dir.test.ts index 78888784d7a0..c62ebb94ffbb 100644 --- a/test/cli/install/bun-run-dir.test.ts +++ b/test/cli/install/bun-run-dir.test.ts @@ -197,11 +197,12 @@ int renameat(int olddirfd, const char *oldpath, int newdirfd, const char *newpat await using cc = spawn({ cmd: [compiler!, "-shared", "-fPIC", "-o", "slow_rename.so", "slow_rename.c", "-ldl"], cwd: run_dir, - stdout: "pipe", + stdout: "ignore", stderr: "pipe", env, }); - expect(await cc.exited).toBe(0); + const [ccErr, ccExit] = await Promise.all([cc.stderr.text(), cc.exited]); + expect({ exitCode: ccExit, stderr: ccErr }).toMatchObject({ exitCode: 0 }); // Each run gets a fresh project and cache so the install always populates // the cache (a satisfied node_modules would skip the timed path). @@ -218,7 +219,7 @@ int renameat(int olddirfd, const char *oldpath, int newdirfd, const char *newpat await using proc = spawn({ cmd: [bunExe(), "install", "--no-save"], cwd: proj, - stdout: "pipe", + stdout: "ignore", stderr: "pipe", env: { ...env, @@ -227,13 +228,16 @@ int renameat(int olddirfd, const char *oldpath, int newdirfd, const char *newpat BUN_DISABLE_SLOW_FILESYSTEM_WARNING: flag, }, }); - const [stderr, exitCode] = await Promise.all([new Response(proc.stderr).text(), proc.exited]); - expect(exitCode).toBe(0); - return stderr; + const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]); + return { stderr, exitCode }; }; // Sanity: with the knob unset, the delayed renameat must trigger the warning. - expect(await install("warn", undefined)).toContain("Slow filesystem detected"); + const warn = await install("warn", undefined); + expect(warn.stderr).toContain("Slow filesystem detected"); + expect(warn.exitCode).toBe(0); // With the knob set (as bunEnv does for every test), it must stay silent. - expect(await install("quiet", "1")).not.toContain("Slow filesystem"); + const quiet = await install("quiet", "1"); + expect(quiet.stderr).not.toContain("Slow filesystem"); + expect(quiet.exitCode).toBe(0); }); From f6b2d7463c83989a4717da2fbac5de4a3f2f65ba Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Thu, 6 Aug 2026 00:36:37 +0000 Subject: [PATCH 5/6] Revert the bun-run-dir timeout bump and drop the LD_PRELOAD warning test --- test/cli/install/bun-run-dir.test.ts | 79 ++-------------------------- 1 file changed, 3 insertions(+), 76 deletions(-) diff --git a/test/cli/install/bun-run-dir.test.ts b/test/cli/install/bun-run-dir.test.ts index c62ebb94ffbb..e2a259cda2ac 100644 --- a/test/cli/install/bun-run-dir.test.ts +++ b/test/cli/install/bun-run-dir.test.ts @@ -1,13 +1,9 @@ import { file, spawn } from "bun"; -import { expect, it, setDefaultTimeout } from "bun:test"; -import { exists, mkdir, writeFile } from "fs/promises"; -import { bunExe, bunEnv as env, isLinux, isMusl, readdirSorted, tmpdirSync } from "harness"; +import { expect, it } from "bun:test"; +import { exists, writeFile } from "fs/promises"; +import { bunExe, bunEnv as env, readdirSorted, tmpdirSync } from "harness"; import { join } from "path"; -// These tests auto-install from the npm registry, which can exceed the default -// timeout under slow networks and sanitizer builds. -setDefaultTimeout(90_000); - it.concurrent("should download dependency to run local file", async () => { const run_dir = tmpdirSync(); await writeFile( @@ -172,72 +168,3 @@ import { prueba } from "pruebadfasdfasdkafasdyuif.js"; // The exit code will not be 1 if it panics. expect(await exited).toBe(1); }); - -// The "Slow filesystem detected" warning fires when populating the install -// cache takes over 100ms, which used to flake stderr assertions in this file -// on slow CI machines. Force the slow path deterministically by delaying -// renameat with an LD_PRELOAD shim, and assert the harness env knob -// (BUN_DISABLE_SLOW_FILESYSTEM_WARNING, set in bunEnv) suppresses it. -const compiler = isLinux && !isMusl ? (Bun.which("cc") ?? Bun.which("gcc") ?? Bun.which("clang")) : null; -it.skipIf(!compiler)("BUN_DISABLE_SLOW_FILESYSTEM_WARNING suppresses the slow filesystem warning", async () => { - const run_dir = tmpdirSync(); - await writeFile( - join(run_dir, "slow_rename.c"), - `#define _GNU_SOURCE -#include -#include -int renameat(int olddirfd, const char *oldpath, int newdirfd, const char *newpath) { - static int (*real)(int, const char *, int, const char *); - if (!real) real = dlsym(RTLD_NEXT, "renameat"); - usleep(250000); - return real(olddirfd, oldpath, newdirfd, newpath); -} -`, - ); - await using cc = spawn({ - cmd: [compiler!, "-shared", "-fPIC", "-o", "slow_rename.so", "slow_rename.c", "-ldl"], - cwd: run_dir, - stdout: "ignore", - stderr: "pipe", - env, - }); - const [ccErr, ccExit] = await Promise.all([cc.stderr.text(), cc.exited]); - expect({ exitCode: ccExit, stderr: ccErr }).toMatchObject({ exitCode: 0 }); - - // Each run gets a fresh project and cache so the install always populates - // the cache (a satisfied node_modules would skip the timed path). - const install = async (name: string, flag: string | undefined) => { - const proj = join(run_dir, name); - await mkdir(proj); - await writeFile( - join(proj, "package.json"), - JSON.stringify({ - name, - dependencies: { baz: `file:${join(import.meta.dir, "baz-0.0.3.tgz")}` }, - }), - ); - await using proc = spawn({ - cmd: [bunExe(), "install", "--no-save"], - cwd: proj, - stdout: "ignore", - stderr: "pipe", - env: { - ...env, - LD_PRELOAD: join(run_dir, "slow_rename.so"), - BUN_INSTALL_CACHE_DIR: join(proj, ".cache"), - BUN_DISABLE_SLOW_FILESYSTEM_WARNING: flag, - }, - }); - const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]); - return { stderr, exitCode }; - }; - - // Sanity: with the knob unset, the delayed renameat must trigger the warning. - const warn = await install("warn", undefined); - expect(warn.stderr).toContain("Slow filesystem detected"); - expect(warn.exitCode).toBe(0); - // With the knob set (as bunEnv does for every test), it must stay silent. - const quiet = await install("quiet", "1"); - expect(quiet.stderr).not.toContain("Slow filesystem"); - expect(quiet.exitCode).toBe(0); -}); From 1a77e073160db95c79f7d86cc25ef9e2fc0c5854 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Thu, 6 Aug 2026 00:36:37 +0000 Subject: [PATCH 6/6] test: drain subprocess pipes concurrently, drop aliases left by the filter removal runBunInstall and the publish helper read one pipe to completion before the other; a filled pipe buffer could block the child. Read stdout, stderr, and the exit together. Also collapse the no-op aliases the stderrForInstall removal left behind. --- test/cli/install/bun-lockb.test.ts | 18 ++++++------------ test/cli/install/bun-publish.test.ts | 4 +--- test/cli/install/catalogs.test.ts | 5 ++--- test/cli/install/config-version.test.ts | 2 +- test/harness.ts | 5 ++--- 5 files changed, 12 insertions(+), 22 deletions(-) diff --git a/test/cli/install/bun-lockb.test.ts b/test/cli/install/bun-lockb.test.ts index a3cc19ef1f9d..b7515bf04663 100644 --- a/test/cli/install/bun-lockb.test.ts +++ b/test/cli/install/bun-lockb.test.ts @@ -161,8 +161,7 @@ it("recovers from a corrupted binary lockfile instead of panicking", async () => stderr: "pipe", env, }); - const [out, rawErr, code] = await Promise.all([stdout.text(), stderr.text(), exited]); - const err = rawErr; + const [out, err, code] = await Promise.all([stdout.text(), stderr.text(), exited]); // The garbage `meta.id` deserialized from the corrupt lockfile used to // panic_bounds_check in Package::clone. Released Bun tolerates it: it @@ -247,8 +246,7 @@ index d156130662798530e852e1afaec5b1c03d429cdc..b4ddf35975a952fdaed99f2b14236519 stderr: "pipe", env, }); - const [out, rawErr, code] = await Promise.all([stdout.text(), stderr.text(), exited]); - const err = rawErr; + const [out, err, code] = await Promise.all([stdout.text(), stderr.text(), exited]); // The out-of-range flag byte must fail lockfile parsing so the install // falls back to a fresh resolve instead of consuming the bad byte. @@ -306,8 +304,7 @@ it("rejects a binary lockfile whose package scripts flag byte is out of range", stderr: "pipe", env, }); - const [out, rawErr, code] = await Promise.all([stdout.text(), stderr.text(), exited]); - const err = rawErr; + const [out, err, code] = await Promise.all([stdout.text(), stderr.text(), exited]); expect(err).toContain("invalid package scripts"); expect(err).toContain("Ignoring lockfile"); @@ -362,8 +359,7 @@ it("rejects a binary lockfile whose git resolved tag contains path separators", stderr: "pipe", env: installEnv, }); - const [out, rawErr, code] = await Promise.all([stdout.text(), stderr.text(), exited]); - const err = rawErr; + const [out, err, code] = await Promise.all([stdout.text(), stderr.text(), exited]); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("error:"); expect(out).toBeDefined(); @@ -384,8 +380,7 @@ it("rejects a binary lockfile whose git resolved tag contains path separators", stderr: "pipe", env: installEnv, }); - const [out, rawErr, code] = await Promise.all([stdout.text(), stderr.text(), exited]); - const err = rawErr; + const [out, err, code] = await Promise.all([stdout.text(), stderr.text(), exited]); expect(err).not.toContain("Invalid git dependency tag"); expect(err).not.toContain("error:"); expect(out).toBeDefined(); @@ -412,8 +407,7 @@ it("rejects a binary lockfile whose git resolved tag contains path separators", stderr: "pipe", env: installEnv, }); - const [out, rawErr, code] = await Promise.all([stdout.text(), stderr.text(), exited]); - const err = rawErr; + const [out, err, code] = await Promise.all([stdout.text(), stderr.text(), exited]); // The tampered resolved value must fail binary lockfile loading (the same // fail-closed rule the text lockfile parser applies) instead of flowing into diff --git a/test/cli/install/bun-publish.test.ts b/test/cli/install/bun-publish.test.ts index d44911ffa962..1bd43061a922 100644 --- a/test/cli/install/bun-publish.test.ts +++ b/test/cli/install/bun-publish.test.ts @@ -27,9 +27,7 @@ export async function publish( env, }); - const out = await stdout.text(); - const err = await stderr.text(); - const exitCode = await exited; + const [out, err, exitCode] = await Promise.all([stdout.text(), stderr.text(), exited]); return { out, err, exitCode }; } diff --git a/test/cli/install/catalogs.test.ts b/test/cli/install/catalogs.test.ts index c7f0480c1dbc..08cc2b4044db 100644 --- a/test/cli/install/catalogs.test.ts +++ b/test/cli/install/catalogs.test.ts @@ -222,7 +222,7 @@ describe("update", () => { }); const [out, err, exitCode] = await Promise.all([stdout.text(), stderr.text(), exited]); - return { out, err: err, exitCode }; + return { out, err, exitCode }; } // https://github.com/oven-sh/bun/issues/23739 @@ -279,8 +279,7 @@ describe("update", () => { stderr: "pipe", env: bunEnv, }); - const [, errText, exitCode] = await Promise.all([stdout.text(), stderr.text(), exited]); - const err = errText; + const [, err, exitCode] = await Promise.all([stdout.text(), stderr.text(), exited]); expect(err).not.toContain("lockfile had changes"); expect(err).not.toContain("error:"); expect(exitCode).toBe(0); diff --git a/test/cli/install/config-version.test.ts b/test/cli/install/config-version.test.ts index 2e0cc0b328cf..3a37dec5b2a7 100644 --- a/test/cli/install/config-version.test.ts +++ b/test/cli/install/config-version.test.ts @@ -18,7 +18,7 @@ async function install(cwd: string) { stderr: "pipe", }); const [out, err, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); - return { out, err: err, exitCode }; + return { out, err, exitCode }; } describe.concurrent("configVersion", () => { diff --git a/test/harness.ts b/test/harness.ts index 0bd8d9e18208..ae512f9a5ba7 100644 --- a/test/harness.ts +++ b/test/harness.ts @@ -1498,7 +1498,7 @@ export async function runBunInstall( }); expect(stdout).toBeDefined(); expect(stderr).toBeDefined(); - let err: string = await stderr.text(); + const [err, out, exitCode] = await Promise.all([stderr.text(), stdout.text(), exited]); expect(err).not.toContain("panic:"); if (!options?.allowErrors) { expect(err).not.toContain("error:"); @@ -1509,8 +1509,7 @@ export async function runBunInstall( if ((options?.savesLockfile ?? true) && !production && !options?.frozenLockfile) { expect(err).toContain("Saved lockfile"); } - let out: string = await stdout.text(); - expect(await exited).toBe(options?.expectedExitCode ?? 0); + expect(exitCode).toBe(options?.expectedExitCode ?? 0); return { out, err, exited }; }