From 4dd018561b23311c8c9904bf35c1c529fe303a44 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 14 Aug 2026 12:56:15 +0000 Subject: [PATCH 1/3] test(install): run bun-install-registry.test.ts concurrently and tighten its assertions Every test now creates its own project directory and env through setupTest() instead of sharing the ones assigned by a module-level beforeEach, which lets the describes run as describe.concurrent. Tests that use toMatchSnapshot(), register registry users, or depend on manifest arrival order (hoisting > peers) are marked serial. The 44 Windows-only bin shim tests share their read-only install and run concurrently as well. While there: - "disabled dev/peer/optional dependencies are still included in the lockfile" awaited the runBunInstall function object and asserted nothing; it now installs with all three groups omitted and checks the lockfile and the reinstall. - the "(bun.lockb) git dependencies" case wrote a bun.lock because its bunfig dropped saveTextLockfile = false; bundledDependencies installs now assert the lockfile flavor, the install output, and that the reinstall is a no-op. - "exact versions stay exact" and "update different dependency groups" become one test per case with the update output asserted. - bin types runs its six bins concurrently and asserts the add summary; outdated "dependency pattern args" asserted on a misspelled package name; several installs that only checked the exit code now check their output and the resulting node_modules; the peers loop asserts the re-resolve is a no-op. - the auto-install snapshot is inlined and the duplicated generateRegistryUser delegates to the registry helper. --- .../bun-install-registry.test.ts.snap | 8 - test/cli/install/bun-install-registry.test.ts | 1395 ++++++++++------- 2 files changed, 821 insertions(+), 582 deletions(-) diff --git a/test/cli/install/__snapshots__/bun-install-registry.test.ts.snap b/test/cli/install/__snapshots__/bun-install-registry.test.ts.snap index 0f74856c14a5..a42d2d10e34c 100644 --- a/test/cli/install/__snapshots__/bun-install-registry.test.ts.snap +++ b/test/cli/install/__snapshots__/bun-install-registry.test.ts.snap @@ -1,13 +1,5 @@ // Bun Snapshot v1, https://bun.sh/docs/test/snapshots -exports[`auto-install symlinks (and junctions) are created correctly in the install cache 1`] = ` -"{ - name: "is-number", - version: "2.0.0", -} -" -`; - exports[`text lockfile workspace sorting 1`] = ` "{ "lockfileVersion": 2, diff --git a/test/cli/install/bun-install-registry.test.ts b/test/cli/install/bun-install-registry.test.ts index e40b14dcaf7a..3ff513328f3c 100644 --- a/test/cli/install/bun-install-registry.test.ts +++ b/test/cli/install/bun-install-registry.test.ts @@ -1,12 +1,12 @@ import { file, spawn, write } from "bun"; import { install_test_helpers, npm_manifest_test_helpers } from "bun:internal-for-testing"; -import { afterAll, beforeEach, describe, expect, setDefaultTimeout, test } from "bun:test"; +import { afterAll, describe, expect, setDefaultTimeout, test } from "bun:test"; import { copyFileSync, mkdirSync } from "fs"; import { cp, exists, lstat, mkdir, readlink, rm, writeFile } from "fs/promises"; import { assertManifestsPopulated, + bunEnv, bunExe, - bunEnv as env, isFlaky, isMacOS, isWindows, @@ -34,32 +34,37 @@ expect.extend({ var registry: VerdaccioRegistry; var port: number; -var packageDir: string; -/** packageJson = join(packageDir, "package.json"); */ -var packageJson: string; - -let users: Record = {}; setDefaultTimeout(1000 * 60 * 5); registry = new VerdaccioRegistry(); port = registry.port; await registry.start(); -afterAll(async () => { - await Bun.$`rm -f ${import.meta.dir}/htpasswd`.throws(false); +afterAll(() => { registry.stop(); }); -beforeEach(async () => { - ({ packageDir, packageJson } = await registry.createTestDir({ +/** + * Every test starts by creating its own project directory (`packageJson` is its `package.json`, + * `bunfig.toml` points at the registry) and an `env` whose install cache and temp directory live + * inside it. Tests run concurrently (`describe.concurrent` / `test.concurrent`) unless marked + * `serial`, so nothing else may be shared between tests. The serial ones use `toMatchSnapshot()` + * (not available in concurrent tests), register registry users, or need the registry to be idle. + */ +async function setupTest() { + const { packageDir, packageJson } = await registry.createTestDir({ bunfigOpts: { saveTextLockfile: false, linker: "hoisted" }, - })); - await Bun.$`rm -f ${import.meta.dir}/htpasswd`.throws(false); - await Bun.$`rm -rf ${import.meta.dir}/packages/private-pkg-dont-touch`.throws(false); - users = {}; - env.BUN_INSTALL_CACHE_DIR = join(packageDir, ".bun-cache"); - env.BUN_TMPDIR = env.TMPDIR = env.TEMP = join(packageDir, ".bun-tmp"); -}); + }); + const tmpDir = join(packageDir, ".bun-tmp"); + const env: NodeJS.Dict = { + ...bunEnv, + BUN_INSTALL_CACHE_DIR: join(packageDir, ".bun-cache"), + BUN_TMPDIR: tmpDir, + TMPDIR: tmpDir, + TEMP: tmpDir, + }; + return { packageDir, packageJson, env }; +} function registryUrl() { return registry.registryUrl(); @@ -68,36 +73,13 @@ function registryUrl() { /** * Returns auth token */ -async function generateRegistryUser(username: string, password: string): Promise { - if (users[username]) { - throw new Error("that user already exists"); - } else users[username] = password; - - const url = `http://localhost:${port}/-/user/org.couchdb.user:${username}`; - const user = { - name: username, - password: password, - email: `${username}@example.com`, - }; - - const response = await fetch(url, { - method: "PUT", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(user), - }); - - if (response.ok) { - const data = await response.json(); - return data.token; - } else { - throw new Error("Failed to create user:", response.statusText); - } +function generateRegistryUser(username: string, password: string): Promise { + return registry.generateUser(username, password); } -describe("auto-install", () => { +describe.concurrent("auto-install", () => { test("symlinks (and junctions) are created correctly in the install cache", async () => { + const { packageDir, env } = await setupTest(); const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "--print", "require('is-number')"], cwd: packageDir, @@ -110,7 +92,13 @@ describe("auto-install", () => { }); const out = await stdout.text(); - expect(out).toMatchSnapshot(); + expect(out).toMatchInlineSnapshot(` + "{ + name: "is-number", + version: "2.0.0", + } + " + `); const err = await stderr.text(); expect(err).not.toContain("error:"); expect(await exited).toBe(0); @@ -121,7 +109,7 @@ describe("auto-install", () => { }); }); -describe("certificate authority", () => { +describe.concurrent("certificate authority", () => { const mockRegistryFetch = function (opts?: any): (req: Request) => Promise { return async function (req: Request) { if (req.url.includes("no-deps")) { @@ -131,6 +119,7 @@ describe("certificate authority", () => { }; }; test("valid --cafile", async () => { + const { packageDir, packageJson, env } = await setupTest(); using server = Bun.serve({ port: 0, fetch: mockRegistryFetch(), @@ -175,6 +164,7 @@ describe("certificate authority", () => { expect(await exited).toBe(0); }); test("valid --ca", async () => { + const { packageDir, packageJson, env } = await setupTest(); using server = Bun.serve({ port: 0, fetch: mockRegistryFetch(), @@ -231,6 +221,7 @@ describe("certificate authority", () => { expect(await exited).toBe(0); }); test(`non-existent --cafile`, async () => { + const { packageDir, packageJson, env } = await setupTest(); await write(packageJson, JSON.stringify({ name: "foo", version: "1.0.0", "dependencies": { "no-deps": "1.1.1" } })); const { stdout, stderr, exited } = spawn({ @@ -248,6 +239,7 @@ describe("certificate authority", () => { }); test("non-existent --cafile (absolute path)", async () => { + const { packageDir, packageJson, env } = await setupTest(); await write(packageJson, JSON.stringify({ name: "foo", version: "1.0.0", "dependencies": { "no-deps": "1.1.1" } })); const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install", "--cafile", "/does/not/exist"], @@ -264,6 +256,7 @@ describe("certificate authority", () => { }); test("non-existent --cafile with workspaces exits 1 without crashing", async () => { + const { packageDir, packageJson, env } = await setupTest(); // The workspace walk in `PackageManager::init()` populates the workspace // package.json cache before the HTTP thread starts. When the HTTP thread // then fails CA validation and drives process exit, the exit path must not @@ -300,6 +293,7 @@ describe("certificate authority", () => { }); test("cafile from bunfig does not exist", async () => { + const { packageDir, packageJson, env } = await setupTest(); await Promise.all([ write( packageJson, @@ -338,6 +332,7 @@ describe("certificate authority", () => { expect(await exited).toBe(1); }); test("invalid cafile", async () => { + const { packageDir, packageJson, env } = await setupTest(); await Promise.all([ write( packageJson, @@ -375,6 +370,7 @@ ljelkjwelkgjw;lekj;lkejflkj expect(await exited).toBe(1); }); test("invalid --ca", async () => { + const { packageDir, packageJson, env } = await setupTest(); await write( packageJson, JSON.stringify({ @@ -401,8 +397,11 @@ ljelkjwelkgjw;lekj;lkejflkj }); }); -describe("whoami", async () => { +// Registering a user writes to the registry's htpasswd file, which `setupTest()` resets, so these +// cannot overlap with each other. +describe.serial("whoami", async () => { test("can get username", async () => { + const { packageDir, packageJson, env } = await setupTest(); const bunfig = await registry.authBunfig("whoami"); await Promise.all([ write( @@ -430,6 +429,7 @@ describe("whoami", async () => { expect(await exited).toBe(0); }); test("username from .npmrc", async () => { + const { packageDir, packageJson, env } = await setupTest(); // It should report the username from npmrc, even without an account const bunfig = Bun.TOML.stringify({ install: { @@ -462,6 +462,7 @@ describe("whoami", async () => { expect(await exited).toBe(0); }); test("only .npmrc", async () => { + const { packageDir, packageJson, env } = await setupTest(); const token = await generateRegistryUser("whoami-npmrc", "whoami-npmrc"); const npmrc = ` //localhost:${port}/:_authToken=${token} @@ -484,6 +485,7 @@ describe("whoami", async () => { expect(await exited).toBe(0); }); test("two .npmrc", async () => { + const { packageDir, packageJson, env } = await setupTest(); const token = await generateRegistryUser("whoami-two-npmrc", "whoami-two-npmrc"); const packageNpmrc = `registry=http://localhost:${port}/`; const homeNpmrc = `//localhost:${port}/:_authToken=${token}`; @@ -511,6 +513,7 @@ describe("whoami", async () => { expect(await exited).toBe(0); }); test("not logged in", async () => { + const { packageDir, packageJson, env } = await setupTest(); await write(packageJson, JSON.stringify({ name: "whoami-pkg", version: "1.1.1" })); const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "pm", "whoami"], @@ -526,6 +529,7 @@ describe("whoami", async () => { expect(await exited).toBe(1); }); test("invalid token", async () => { + const { packageDir, packageJson, env } = await setupTest(); // create the user and provide an invalid token const token = await generateRegistryUser("invalid-token", "invalid-token"); const bunfig = Bun.TOML.stringify({ @@ -554,8 +558,9 @@ describe("whoami", async () => { }); }); -describe("package.json indentation", async () => { +describe.concurrent("package.json indentation", async () => { test("works for root and workspace packages", async () => { + const { packageDir, packageJson, env } = await setupTest(); await Promise.all([ // 5 space indentation write(packageJson, `\n{\n\n "name": "foo",\n"workspaces": ["packages/*"]\n}`), @@ -563,15 +568,19 @@ describe("package.json indentation", async () => { write(join(packageDir, "packages", "bar", "package.json"), `\n{\n\n\t"name": "bar",\n}`), ]); - let { exited } = spawn({ + let { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "add", "no-deps"], cwd: packageDir, - stdout: "ignore", - stderr: "ignore", + stdout: "pipe", + stderr: "pipe", env, }); - expect(await exited).toBe(0); + let [out, err, exitCode] = await Promise.all([stdout.text(), stderr.text(), exited]); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("error:"); + expect(out).toContain("installed no-deps@2.0.0"); + expect(exitCode).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); const rootPackageJson = await file(packageJson).text(); @@ -581,15 +590,19 @@ describe("package.json indentation", async () => { ); // now add to workspace. it should keep tab indentation - ({ exited } = spawn({ + ({ stdout, stderr, exited } = spawn({ cmd: [bunExe(), "add", "no-deps"], cwd: join(packageDir, "packages", "bar"), - stdout: "inherit", - stderr: "inherit", + stdout: "pipe", + stderr: "pipe", env, })); - expect(await exited).toBe(0); + [out, err, exitCode] = await Promise.all([stdout.text(), stderr.text(), exited]); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("error:"); + expect(out).toContain("installed no-deps@2.0.0"); + expect(exitCode).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(await file(packageJson).text()).toBe(rootPackageJson); @@ -598,21 +611,27 @@ describe("package.json indentation", async () => { }); test("install maintains indentation", async () => { + const { packageDir, packageJson, env } = await setupTest(); await write(packageJson, `{\n "dependencies": {}\n }\n`); - let { exited } = spawn({ + const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "add", "no-deps"], cwd: packageDir, - stdout: "ignore", - stderr: "ignore", + stdout: "pipe", + stderr: "pipe", env, }); - expect(await exited).toBe(0); + const [out, err, exitCode] = await Promise.all([stdout.text(), stderr.text(), exited]); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("error:"); + expect(out).toContain("installed no-deps@2.0.0"); + expect(exitCode).toBe(0); expect(await file(packageJson).text()).toBe(`{\n "dependencies": {\n "no-deps": "^2.0.0"\n }\n}\n`); }); }); -describe("text lockfile", () => { - test("workspace sorting", async () => { +describe.concurrent("text lockfile", () => { + test.serial("workspace sorting", async () => { + const { packageDir, env } = await setupTest(); await Promise.all([ write( join(packageDir, "package.json"), @@ -683,7 +702,8 @@ describe("text lockfile", () => { expect(lockfile.replaceAll(/localhost:\d+/g, "localhost:1234")).toMatchSnapshot(); }); - test("--frozen-lockfile", async () => { + test.serial("--frozen-lockfile", async () => { + const { packageDir, env } = await setupTest(); await Promise.all([ write( join(packageDir, "package.json"), @@ -752,6 +772,7 @@ describe("text lockfile", () => { for (const omit of ["dev", "peer", "optional"]) { test(`resolvable lockfile with ${omit} dependencies disabled`, async () => { + const { packageDir, env } = await setupTest(); await Promise.all([ write( join(packageDir, "package.json"), @@ -809,6 +830,7 @@ describe("text lockfile", () => { } test("optionalPeers", async () => { + const { packageDir, packageJson, env } = await setupTest(); await Promise.all([ write( packageJson, @@ -870,7 +892,8 @@ describe("text lockfile", () => { }); }); -test("--lockfile-only", async () => { +test.concurrent("--lockfile-only", async () => { + const { packageDir, packageJson, env } = await setupTest(); await Promise.all([ write( packageJson, @@ -943,9 +966,37 @@ test("--lockfile-only", async () => { expect(err).toBe(""); }); -describe("bundledDependencies", () => { +describe.concurrent("bundledDependencies", () => { + async function install(packageDir: string, env: NodeJS.Dict, args: string[]) { + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install", ...args], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + env, + }); + const [out, err, exitCode] = await Promise.all([stdout.text(), stderr.text(), exited]); + expect(err).not.toContain("error:"); + expect(exitCode).toBe(0); + return { out, err }; + } + + async function installSavingLockfile(packageDir: string, env: NodeJS.Dict, textLockfile: boolean) { + const { err } = await install(packageDir, env, textLockfile ? ["--save-text-lockfile"] : []); + expect(err).toContain("Saved lockfile"); + expect(await exists(join(packageDir, textLockfile ? "bun.lock" : "bun.lockb"))).toBeTrue(); + } + + // Installing again from the lockfile must not touch the bundled packages. + async function expectReinstallIsNoop(packageDir: string, env: NodeJS.Dict) { + const { out, err } = await install(packageDir, env, []); + expect(err).not.toContain("Saved lockfile"); + expect(out).toMatch(/Checked \d+ installs? across \d+ packages \(no changes\)/); + } + for (const textLockfile of [true, false]) { test(`(${textLockfile ? "bun.lock" : "bun.lockb"}) basic`, async () => { + const { packageDir, packageJson, env } = await setupTest(); await Promise.all([ write( packageJson, @@ -959,16 +1010,7 @@ describe("bundledDependencies", () => { ), ]); - const cmd = textLockfile ? [bunExe(), "install", "--save-text-lockfile"] : [bunExe(), "install"]; - let { exited } = spawn({ - cmd, - cwd: packageDir, - stdout: "ignore", - stderr: "ignore", - env, - }); - - expect(await exited).toBe(0); + await installSavingLockfile(packageDir, env, textLockfile); expect( await Promise.all([ @@ -977,15 +1019,7 @@ describe("bundledDependencies", () => { ]), ).toEqual([false, true]); - ({ exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "ignore", - stderr: "ignore", - env, - })); - - expect(await exited).toBe(0); + await expectReinstallIsNoop(packageDir, env); expect( await Promise.all([ @@ -996,6 +1030,7 @@ describe("bundledDependencies", () => { }); test(`(${textLockfile ? "bun.lock" : "bun.lockb"}) bundledDependencies === true`, async () => { + const { packageDir, packageJson, env } = await setupTest(); await Promise.all([ write( packageJson, @@ -1009,16 +1044,7 @@ describe("bundledDependencies", () => { ), ]); - const cmd = textLockfile ? [bunExe(), "install", "--save-text-lockfile"] : [bunExe(), "install"]; - let { exited } = spawn({ - cmd, - cwd: packageDir, - stdout: "ignore", - stderr: "ignore", - env, - }); - - expect(await exited).toBe(0); + await installSavingLockfile(packageDir, env, textLockfile); async function check() { return Promise.all([ @@ -1043,20 +1069,13 @@ describe("bundledDependencies", () => { expect(await check()).toEqual([false, false, true, true, true]); - ({ exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "ignore", - stderr: "ignore", - env, - })); - - expect(await exited).toBe(0); + await expectReinstallIsNoop(packageDir, env); expect(await check()).toEqual([false, false, true, true, true]); }); test(`(${textLockfile ? "bun.lock" : "bun.lockb"}) transitive bundled dependency collision`, async () => { + const { packageDir, packageJson, env } = await setupTest(); // Install a package with one bundled dependency and one regular dependency. // The bundled dependency has a transitive dependency of the same regular dependency, // but at a different version. Test that the regular dependency does not replace the @@ -1077,16 +1096,7 @@ describe("bundledDependencies", () => { ), ]); - const cmd = textLockfile ? [bunExe(), "install", "--save-text-lockfile"] : [bunExe(), "install"]; - let { exited } = spawn({ - cmd, - cwd: packageDir, - stdout: "ignore", - stderr: "ignore", - env, - }); - - expect(await exited).toBe(0); + await installSavingLockfile(packageDir, env, textLockfile); async function check() { expect( @@ -1119,20 +1129,13 @@ describe("bundledDependencies", () => { await check(); - ({ exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "ignore", - stderr: "ignore", - env, - })); - - expect(await exited).toBe(0); + await expectReinstallIsNoop(packageDir, env); await check(); }); test(`(${textLockfile ? "bun.lock" : "bun.lockb"}) git dependencies`, async () => { + const { packageDir, packageJson, env } = await setupTest(); await Promise.all([ write( packageJson, @@ -1146,26 +1149,20 @@ describe("bundledDependencies", () => { }, }), ), + // Both git packages depend on `zod`, which the local registry does not have: even though it is + // bundled, bun still resolves it, so this test has to use the default registry. write( join(packageDir, "bunfig.toml"), Bun.TOML.stringify({ install: { cache: join(packageDir, ".bun-cache"), + saveTextLockfile: false, }, }), ), ]); - const cmd = textLockfile ? [bunExe(), "install", "--save-text-lockfile"] : [bunExe(), "install"]; - let { exited } = spawn({ - cmd, - cwd: packageDir, - stdout: "ignore", - stderr: "ignore", - env, - }); - - expect(await exited).toBe(0); + await installSavingLockfile(packageDir, env, textLockfile); async function check() { expect( @@ -1179,20 +1176,13 @@ describe("bundledDependencies", () => { await check(); - ({ exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "ignore", - stderr: "ignore", - env, - })); - - expect(await exited).toBe(0); + await expectReinstallIsNoop(packageDir, env); await check(); }); test(`(${textLockfile ? "bun.lock" : "bun.lockb"}) workspace dependencies bundle correctly`, async () => { + const { packageDir, packageJson, env } = await setupTest(); await Promise.all([ write( packageJson, @@ -1214,16 +1204,7 @@ describe("bundledDependencies", () => { ), ]); - const cmd = textLockfile ? [bunExe(), "install", "--save-text-lockfile"] : [bunExe(), "install"]; - let { exited } = spawn({ - cmd, - cwd: packageDir, - stdout: "ignore", - stderr: "ignore", - env, - }); - - expect(await exited).toBe(0); + await installSavingLockfile(packageDir, env, textLockfile); async function check() { expect( @@ -1237,24 +1218,17 @@ describe("bundledDependencies", () => { await check(); - ({ exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "ignore", - stderr: "ignore", - env, - })); - - expect(await exited).toBe(0); + await expectReinstallIsNoop(packageDir, env); await check(); }); } }); -describe("optionalDependencies", () => { +describe.concurrent("optionalDependencies", () => { for (const optional of [true, false]) { test(`exit code is ${optional ? 0 : 1} when ${optional ? "optional" : ""} dependency tarball is missing`, async () => { + const { packageDir, packageJson, env } = await setupTest(); await write( packageJson, JSON.stringify({ @@ -1284,6 +1258,7 @@ describe("optionalDependencies", () => { for (const rootOptional of [true, false]) { test(`exit code is 0 when ${rootOptional ? "root" : ""} optional dependency does not exist in registry`, async () => { + const { packageDir, packageJson, env } = await setupTest(); await write( packageJson, JSON.stringify({ @@ -1304,6 +1279,7 @@ describe("optionalDependencies", () => { }); } test("should not install optional deps if false in bunfig", async () => { + const { packageDir, packageJson, env } = await setupTest(); await writeFile( join(packageDir, "bunfig.toml"), Bun.TOML.stringify({ @@ -1359,6 +1335,7 @@ describe("optionalDependencies", () => { }); test("lifecycle scripts failures from transitive dependencies are ignored", async () => { + const { packageDir, packageJson, env } = await setupTest(); // Dependency with a transitive optional dependency that fails during its preinstall script. await write( packageJson, @@ -1387,7 +1364,8 @@ describe("optionalDependencies", () => { }); }); -test("it should ignore peerDependencies within workspaces", async () => { +test.serial("it should ignore peerDependencies within workspaces", async () => { + const { packageDir, packageJson, env } = await setupTest(); await Promise.all([ write( packageJson, @@ -1431,28 +1409,83 @@ test("it should ignore peerDependencies within workspaces", async () => { expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual(["Baz", "a-dep", "no-deps"]); }); -test("disabled dev/peer/optional dependencies are still included in the lockfile", async () => { - await Promise.all([ - write( - packageJson, - JSON.stringify({ - devDependencies: { - "no-deps": "1.0.0", - }, - peerDependencies: { - "a-dep": "1.0.1", - }, - optionalDependencies: { - "basic-1": "1.0.0", - }, - }), - ), +test.concurrent("disabled dev/peer/optional dependencies are still included in the lockfile", async () => { + const { packageDir, packageJson, env } = await setupTest(); + await write( + packageJson, + JSON.stringify({ + name: "foo", + devDependencies: { + "no-deps": "1.0.0", + }, + peerDependencies: { + "a-dep": "1.0.1", + }, + optionalDependencies: { + "basic-1": "1.0.0", + }, + }), + ); + + let { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install", "--save-text-lockfile", "--omit=dev", "--omit=peer", "--omit=optional"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + env, + }); + + let [out, err, exitCode] = await Promise.all([stdout.text(), stderr.text(), exited]); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("error:"); + // nothing to install, so there is no summary + expect(out.split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + expect.stringMatching(/^\[[0-9.]+m?s\] done$/), + "", + ]); + expect(exitCode).toBe(0); + + expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([]); + + const lockfile = (await file(join(packageDir, "bun.lock")).text()).replaceAll(/localhost:\d+/g, "localhost:1234"); + expect(lockfile).toContain('"no-deps": ["no-deps@1.0.0",'); + expect(lockfile).toContain('"a-dep": ["a-dep@1.0.1",'); + expect(lockfile).toContain('"basic-1": ["basic-1@1.0.0",'); + + // the same lockfile resolves everything once the dependency groups are enabled again + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + env, + })); + + [out, err, exitCode] = await Promise.all([stdout.text(), stderr.text(), exited]); + expect(err).not.toContain("Saved lockfile"); + expect(err).not.toContain("error:"); + // the summary follows the lockfile's order: devDependencies, optionalDependencies, peerDependencies + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ no-deps@1.0.0", + "+ basic-1@1.0.0", + "+ a-dep@1.0.1", + "", + "3 packages installed", ]); + expect(exitCode).toBe(0); - await runBunInstall; + expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual(["a-dep", "basic-1", "no-deps"]); + expect((await file(join(packageDir, "bun.lock")).text()).replaceAll(/localhost:\d+/g, "localhost:1234")).toBe( + lockfile, + ); }); -test("tarball override does not crash", async () => { +test.concurrent("tarball override does not crash", async () => { + const { packageDir, packageJson, env } = await setupTest(); await write( packageJson, JSON.stringify({ @@ -1476,7 +1509,7 @@ test("tarball override does not crash", async () => { }); }); -describe.each(["--production", "without --production"])("%s", flag => { +describe.concurrent.each(["--production", "without --production"])("%s", flag => { const prod = flag === "--production"; const order = ["devDependencies", "dependencies"]; // const stdio = process.versions.bun.includes("debug") ? "inherit" : "ignore"; @@ -1484,6 +1517,7 @@ describe.each(["--production", "without --production"])("%s", flag => { if (prod) { test("modifying package.json with --production should not save lockfile", async () => { + const { packageDir, packageJson, env } = await setupTest(); await writeFile( packageJson, JSON.stringify({ @@ -1581,6 +1615,7 @@ describe.each(["--production", "without --production"])("%s", flag => { } test(`should prefer ${order[+prod % 2]} over ${order[1 - (+prod % 2)]}`, async () => { + const { packageDir, packageJson, env } = await setupTest(); await writeFile( packageJson, JSON.stringify({ @@ -1675,7 +1710,8 @@ describe.each(["--production", "without --production"])("%s", flag => { }); }); -test("hardlinks on windows dont fail with long paths", async () => { +test.concurrent("hardlinks on windows dont fail with long paths", async () => { + const { packageDir, packageJson, env } = await setupTest(); await mkdir(join(packageDir, "a-package")); await writeFile( join(packageDir, "a-package", "package.json"), @@ -1722,7 +1758,8 @@ test("hardlinks on windows dont fail with long paths", async () => { assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); }); -test("basic 1", async () => { +test.concurrent("basic 1", async () => { + const { packageDir, packageJson, env } = await setupTest(); await writeFile( packageJson, JSON.stringify({ @@ -1788,7 +1825,8 @@ test("basic 1", async () => { assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); }); -test("manifest cache will invalidate when registry changes", async () => { +test.concurrent("manifest cache will invalidate when registry changes", async () => { + const { packageDir, packageJson, env } = await setupTest(); const cacheDir = join(packageDir, ".bun-cache"); await Promise.all([ write( @@ -1846,7 +1884,8 @@ test("manifest cache will invalidate when registry changes", async () => { } }); -test("dependency from root satisfies range from dependency", async () => { +test.concurrent("dependency from root satisfies range from dependency", async () => { + const { packageDir, packageJson, env } = await setupTest(); await writeFile( packageJson, JSON.stringify({ @@ -1916,7 +1955,8 @@ test("dependency from root satisfies range from dependency", async () => { assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); }); -test("duplicate names and versions in a manifest do not install incorrect packages", async () => { +test.concurrent("duplicate names and versions in a manifest do not install incorrect packages", async () => { + const { packageDir, packageJson, env } = await setupTest(); /** * `duplicate-name-and-version` has two versions: * 1.0.1: @@ -1963,7 +2003,7 @@ test("duplicate names and versions in a manifest do not install incorrect packag ]); }); -describe("peerDependency index out of bounds", async () => { +describe.concurrent("peerDependency index out of bounds", async () => { // Test for "index of out bounds" errors with peer dependencies when adding/removing a package // // Repro: @@ -1978,6 +2018,7 @@ describe("peerDependency index out of bounds", async () => { for (const secondDep of dependencies) { if (firstDep === secondDep) continue; test(`replacing ${firstDep} with ${secondDep}`, async () => { + const { packageDir, packageJson, env } = await setupTest(); await write( packageJson, JSON.stringify({ @@ -2045,6 +2086,7 @@ describe("peerDependency index out of bounds", async () => { // internally, testing for index out of bounds. It's also important cache is deleted to ensure a tarball // task is created for it. test("optional", async () => { + const { packageDir, packageJson, env } = await setupTest(); await write( packageJson, JSON.stringify({ @@ -2084,90 +2126,95 @@ describe("peerDependency index out of bounds", async () => { }); }); -test("peerDependency in child npm dependency should not maintain old version when package is upgraded", async () => { - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "peer-deps-fixed": "1.0.0", - "no-deps": "1.0.0", - }, - }), - ); - - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); +test.concurrent( + "peerDependency in child npm dependency should not maintain old version when package is upgraded", + async () => { + const { packageDir, packageJson, env } = await setupTest(); + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "peer-deps-fixed": "1.0.0", + "no-deps": "1.0.0", + }, + }), + ); - var err = await stderr.text(); - var out = await stdout.text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - expect.stringContaining("+ no-deps@1.0.0"), - "+ peer-deps-fixed@1.0.0", - "", - "2 packages installed", - ]); - expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toEqual({ - name: "no-deps", - version: "1.0.0", - } as any); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); - await writeFile( - packageJson, - JSON.stringify({ - name: "foo", + var err = await stderr.text(); + var out = await stdout.text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + expect.stringContaining("+ no-deps@1.0.0"), + "+ peer-deps-fixed@1.0.0", + "", + "2 packages installed", + ]); + expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toEqual({ + name: "no-deps", version: "1.0.0", - dependencies: { - "peer-deps-fixed": "1.0.0", - "no-deps": "1.0.1", // upgrade the package - }, - }), - ); + } as any); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - })); + await writeFile( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "peer-deps-fixed": "1.0.0", + "no-deps": "1.0.1", // upgrade the package + }, + }), + ); - err = await stderr.text(); - out = await stdout.text(); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toEqual({ - name: "no-deps", - version: "1.0.1", - } as any); - expect(await exists(join(packageDir, "node_modules", "peer-deps-fixed", "node_modules"))).toBeFalse(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - expect.stringContaining("+ no-deps@1.0.1"), - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); -}); + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + })); -test("package added after install", async () => { + err = await stderr.text(); + out = await stdout.text(); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toEqual({ + name: "no-deps", + version: "1.0.1", + } as any); + expect(await exists(join(packageDir, "node_modules", "peer-deps-fixed", "node_modules"))).toBeFalse(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + expect.stringContaining("+ no-deps@1.0.1"), + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + }, +); + +test.concurrent("package added after install", async () => { + const { packageDir, packageJson, env } = await setupTest(); await writeFile( packageJson, JSON.stringify({ @@ -2283,7 +2330,8 @@ test("package added after install", async () => { assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); }); -test("--production excludes devDependencies in workspaces", async () => { +test.concurrent("--production excludes devDependencies in workspaces", async () => { + const { packageDir, packageJson, env } = await setupTest(); await Promise.all([ write( packageJson, @@ -2379,7 +2427,8 @@ test("--production excludes devDependencies in workspaces", async () => { expect(results).toMatchObject(expectedResults); }); -test("--production without a lockfile will install and not save lockfile", async () => { +test.concurrent("--production without a lockfile will install and not save lockfile", async () => { + const { packageDir, packageJson, env } = await setupTest(); await writeFile( packageJson, JSON.stringify({ @@ -2418,10 +2467,11 @@ test("--production without a lockfile will install and not save lockfile", async expect(await exists(join(packageDir, "node_modules", "no-deps", "index.js"))).toBeTrue(); }); -describe("binaries", () => { +describe.concurrent("binaries", () => { for (const global of [false, true]) { describe(`existing destinations${global ? " (global)" : ""}`, () => { test("existing non-symlink", async () => { + const { packageDir, packageJson, env } = await setupTest(); await Promise.all([ write( packageJson, @@ -2445,6 +2495,7 @@ describe("binaries", () => { }); } test("it should correctly link binaries after deleting node_modules", async () => { + const { packageDir, packageJson, env } = await setupTest(); const json: any = { name: "foo", version: "1.0.0", @@ -2515,6 +2566,7 @@ describe("binaries", () => { }); test("will link binaries for packages installed multiple times", async () => { + const { packageDir, packageJson, env } = await setupTest(); await Promise.all([ write( packageJson, @@ -2565,6 +2617,7 @@ describe("binaries", () => { }); test("it should re-symlink binaries that become invalid when updating package versions", async () => { + const { packageDir, packageJson, env } = await setupTest(); await writeFile( packageJson, JSON.stringify({ @@ -2649,6 +2702,7 @@ describe("binaries", () => { }); test("will only link global binaries for requested packages", async () => { + const { packageDir, env } = await setupTest(); await Promise.all([ write( join(packageDir, "bunfig.toml"), @@ -2709,6 +2763,7 @@ describe("binaries", () => { for (const global of [false, true]) { test(`bin types${global ? " (global)" : ""}`, async () => { + const { packageDir, packageJson, env } = await setupTest(); if (global) { await write( join(packageDir, "bunfig.toml"), @@ -2748,22 +2803,38 @@ describe("binaries", () => { env: global ? { ...env, BUN_INSTALL: join(packageDir, "global-install-dir") } : env, }); - const err = await stderr.text(); + const [out, err, exitCode] = await Promise.all([stdout.text(), stderr.text(), exited]); expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun add v1."), + "", + "installed dep-with-file-bin@1.0.0 with binaries:", + " - dep-with-file-bin", + "installed dep-with-single-entry-map-bin@1.0.0 with binaries:", + " - single-entry-map-bin", + // the summary does not enumerate `directories.bin` entries + "installed dep-with-directory-bins@1.0.0", + "installed dep-with-map-bins@1.0.0 with binaries:", + " - map-bin-1", + " - map-bin-2", + "", + "4 packages installed", + ]); + expect(exitCode).toBe(0); - const out = await stdout.text(); - expect(await exited).toBe(0); - - await runBin("dep-with-file-bin", "file-bin\n", global); - await runBin("single-entry-map-bin", "single-entry-map-bin\n", global); - await runBin("directory-bin-1", "directory-bin-1\n", global); - await runBin("directory-bin-2", "directory-bin-2\n", global); - await runBin("map-bin-1", "map-bin-1\n", global); - await runBin("map-bin-2", "map-bin-2\n", global); + await Promise.all([ + runBin(packageDir, env, "dep-with-file-bin", "file-bin\n", global), + runBin(packageDir, env, "single-entry-map-bin", "single-entry-map-bin\n", global), + runBin(packageDir, env, "directory-bin-1", "directory-bin-1\n", global), + runBin(packageDir, env, "directory-bin-2", "directory-bin-2\n", global), + runBin(packageDir, env, "map-bin-1", "map-bin-1\n", global), + runBin(packageDir, env, "map-bin-2", "map-bin-2\n", global), + ]); }); } - test("each type of binary serializes correctly to text lockfile", async () => { + test.serial("each type of binary serializes correctly to text lockfile", async () => { + const { packageDir, packageJson, env } = await setupTest(); await Promise.all([ write( packageJson, @@ -2886,7 +2957,8 @@ describe("binaries", () => { expect(join(packageDir, "node_modules", ".bin", "map-bin-2")).toBeValidBin(join("..", "map-bin", "map-bin-2.js")); }); - test.todo("text lockfile updates with new bin entry for folder dependencies", async () => { + test.serial.todo("text lockfile updates with new bin entry for folder dependencies", async () => { + const { packageDir, packageJson, env } = await setupTest(); await Promise.all([ write( packageJson, @@ -2981,7 +3053,8 @@ describe("binaries", () => { ); }); - test("root resolution bins", async () => { + test.serial("root resolution bins", async () => { + const { packageDir, env } = await setupTest(); // As of writing this test, the only way to get a root resolution // is to migrate a package-lock.json with a root resolution. For now, // we'll just mock the bun.lock. @@ -3071,7 +3144,13 @@ describe("binaries", () => { expect(join(packageDir, "node_modules", ".bin", "fooooo")).toBeValidBin(join("..", "fooooo", "fooooo.js")); }); - async function runBin(binName: string, expected: string, global: boolean) { + async function runBin( + packageDir: string, + env: NodeJS.Dict, + binName: string, + expected: string, + global: boolean, + ) { const args = global ? [`./global-bin-dir/${binName}`] : [bunExe(), binName]; const result = Bun.spawn({ cmd: [...args, "--linker=hoisted"], @@ -3081,14 +3160,12 @@ describe("binaries", () => { env, }); - const out = await result.stdout.text(); - expect(out).toEqual(expected); - const err = await result.stderr.text(); - expect(err).toBeEmpty(); - expect(await result.exited).toBe(0); + const [out, err, exitCode] = await Promise.all([result.stdout.text(), result.stderr.text(), result.exited]); + expect({ binName, out, err, exitCode }).toEqual({ binName, out: expected, err: "", exitCode: 0 }); } test("it will skip (without errors) if a folder from `directories.bin` does not exist", async () => { + const { packageDir, packageJson, env } = await setupTest(); await Promise.all([ write( packageJson, @@ -3102,20 +3179,32 @@ describe("binaries", () => { cp(join(import.meta.dir, "missing-directory-bin-1.1.1.tgz"), join(packageDir, "missing-directory-bin-1.1.1.tgz")), ]); - const { stderr, exited } = spawn({ + const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stdout: "pipe", stderr: "pipe", env, }); - const err = await stderr.text(); + const [out, err, exitCode] = await Promise.all([stdout.text(), stderr.text(), exited]); + expect(err).toContain("Saved lockfile"); expect(err).not.toContain("error:"); - expect(await exited).toBe(0); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ missing-directory-bin@missing-directory-bin-1.1.1.tgz", + "", + "1 package installed", + ]); + expect(exitCode).toBe(0); + + // the package itself is installed, it just has no bins to link + expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual(["missing-directory-bin"]); }); }); -test("--config cli flag works", async () => { +test.concurrent("--config cli flag works", async () => { + const { packageDir, env } = await setupTest(); await Promise.all([ write( join(packageDir, "package.json"), @@ -3142,7 +3231,7 @@ test("--config cli flag works", async () => { ]); // should install dev dependencies - let { exited } = spawn({ + let { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "i"], cwd: packageDir, stdout: "pipe", @@ -3150,7 +3239,18 @@ test("--config cli flag works", async () => { env, }); - expect(await exited).toBe(0); + let [out, err, exitCode] = await Promise.all([stdout.text(), stderr.text(), exited]); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + expect.stringContaining("+ a-dep@1.0.1"), + expect.stringContaining("+ no-deps@1.0.0"), + "", + "2 packages installed", + ]); + expect(exitCode).toBe(0); expect(await file(join(packageDir, "node_modules", "a-dep", "package.json")).json()).toEqual({ name: "a-dep", version: "1.0.1", @@ -3159,7 +3259,7 @@ test("--config cli flag works", async () => { await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); // should not install dev dependencies - ({ exited } = spawn({ + ({ stdout, stderr, exited } = spawn({ cmd: [bunExe(), "i", "--config=bunfig2.toml"], cwd: packageDir, stdout: "pipe", @@ -3167,11 +3267,22 @@ test("--config cli flag works", async () => { env, })); - expect(await exited).toBe(0); - expect(await exists(join(packageDir, "node_modules", "a-dep"))).toBeFalse(); + [out, err, exitCode] = await Promise.all([stdout.text(), stderr.text(), exited]); + expect(err).not.toContain("Saved lockfile"); + expect(err).not.toContain("error:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ no-deps@1.0.0", + "", + "1 package installed", + ]); + expect(exitCode).toBe(0); + expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual(["no-deps"]); }); -test("it should invalid cached package if package.json is missing", async () => { +test.concurrent("it should invalid cached package if package.json is missing", async () => { + const { packageDir, packageJson, env } = await setupTest(); await Promise.all([ write( packageJson, @@ -3234,7 +3345,8 @@ test("it should invalid cached package if package.json is missing", async () => ]); }); -test("it should install with missing bun.lockb, node_modules, and/or cache", async () => { +test.concurrent("it should install with missing bun.lockb, node_modules, and/or cache", async () => { + const { packageDir, packageJson, env } = await setupTest(); // first clean install await writeFile( packageJson, @@ -3438,9 +3550,10 @@ test("it should install with missing bun.lockb, node_modules, and/or cache", asy ]); }); -describe("hoisting", async () => { +describe.concurrent("hoisting", async () => { // https://github.com/oven-sh/bun/issues/7241 test("nested dependency is preserved when its parent is reinstalled from an empty cache", async () => { + const { packageDir, packageJson, env } = await setupTest(); // `one-fixed-dep@1.0.0` depends on `no-deps@1.0.0`. With `no-deps@2.0.0` // at the root, `no-deps@1.0.0` is forced to nest under // `node_modules/one-fixed-dep/node_modules/no-deps`. @@ -3489,6 +3602,7 @@ describe("hoisting", async () => { // https://github.com/oven-sh/bun/issues/16968 test("nested dependency is preserved when its parent changes version via lockfile", async () => { + const { packageDir, packageJson, env } = await setupTest(); // has-bin-entries@1.0.0 and @2.0.0 both depend on no-deps@1.0.0. With // no-deps@2.0.0 at the root, no-deps@1.0.0 nests under has-bin-entries. @@ -3545,6 +3659,7 @@ describe("hoisting", async () => { }); test("nested dependency deferred behind an unrelated root reinstall is skipped, not reinstalled", async () => { + const { packageDir, packageJson, env } = await setupTest(); await writeFile( packageJson, JSON.stringify({ @@ -3660,6 +3775,7 @@ describe("hoisting", async () => { for (const { dependencies, expected, situation } of tests) { test(`it should hoist ${expected} when ${situation}`, async () => { + const { packageDir, packageJson, env } = await setupTest(); await writeFile( packageJson, JSON.stringify({ @@ -3712,7 +3828,9 @@ describe("hoisting", async () => { }); } - describe("peers", async () => { + // Which `a-dep` gets hoisted depends on the order the manifests arrive in, so these run one at a + // time to keep the registry quiet while they run. + describe.serial("peers", async () => { var peerTests: any = [ { situation: "peer 1.0.2", @@ -3822,6 +3940,7 @@ describe("hoisting", async () => { test.todoIf(isFlaky && isMacOS && situation === "peer ^1.0.2")( `it should hoist ${expected} when ${situation}`, async () => { + const { packageDir, packageJson, env } = await setupTest(); await writeFile( packageJson, JSON.stringify({ @@ -3868,10 +3987,8 @@ describe("hoisting", async () => { expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); - if (out.includes("installed")) { - console.log("stdout:", out); - } - expect(out).not.toContain("package installed"); + // re-resolving without a lockfile must land on the same tree + expect(out).toMatch(/Checked \d+ installs across \d+ packages \(no changes\)/); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); @@ -3893,7 +4010,10 @@ describe("hoisting", async () => { expect(err).not.toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); - expect(out).not.toContain("package installed"); + for (const dep of Object.keys(dependencies)) { + expect(out).toContain(`+ ${dep}@${dependencies[dep]}`); + } + expect(out).toMatch(/\d+ packages installed/); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); @@ -3904,6 +4024,7 @@ describe("hoisting", async () => { }); test("hoisting/using incorrect peer dep after install", async () => { + const { packageDir, packageJson, env } = await setupTest(); await writeFile( packageJson, JSON.stringify({ @@ -4008,6 +4129,7 @@ describe("hoisting", async () => { }); test("root workspace (other than root) dependency will not hoist incorrect peer", async () => { + const { packageDir, packageJson, env } = await setupTest(); await Promise.all([ write( packageJson, @@ -4080,6 +4202,7 @@ describe("hoisting", async () => { }); test("hoisting/using incorrect peer dep on initial install", async () => { + const { packageDir, packageJson, env } = await setupTest(); await writeFile( packageJson, JSON.stringify({ @@ -4185,6 +4308,7 @@ describe("hoisting", async () => { describe("devDependencies", () => { test("from normal dependency", async () => { + const { packageDir, packageJson, env } = await setupTest(); // Root package should choose no-deps@1.0.1. // // `normal-dep-and-dev-dep` should install `no-deps@1.0.0` and `normal-dep@1.0.1`. @@ -4236,6 +4360,7 @@ describe("hoisting", async () => { }); test("from workspace", async () => { + const { packageDir, packageJson, env } = await setupTest(); await writeFile( packageJson, JSON.stringify({ @@ -4297,6 +4422,7 @@ describe("hoisting", async () => { }); test("from linked package", async () => { + const { packageDir, packageJson, env } = await setupTest(); await writeFile( packageJson, JSON.stringify({ @@ -4365,6 +4491,7 @@ describe("hoisting", async () => { }); test("dependency with normal dependency same as root", async () => { + const { packageDir, packageJson, env } = await setupTest(); await writeFile( packageJson, JSON.stringify({ @@ -4409,7 +4536,8 @@ describe("hoisting", async () => { }); }); - test.todoIf(isFlaky && isWindows)("text lockfile is hoisted", async () => { + test.serial.todoIf(isFlaky && isWindows)("text lockfile is hoisted", async () => { + const { packageDir, packageJson, env } = await setupTest(); // Each dependency depends on 'hoist-lockfile-shared'. // 1 - "*" // 2 - "^1.0.1" @@ -4468,8 +4596,8 @@ describe("hoisting", async () => { }); }); -describe("transitive file dependencies", () => { - async function checkHoistedFiles() { +describe.concurrent("transitive file dependencies", () => { + async function checkHoistedFiles(packageDir: string) { const aliasedFileDepFilesPackageJson = join( packageDir, "node_modules", @@ -4529,7 +4657,7 @@ describe("transitive file dependencies", () => { ]); } - async function checkUnhoistedFiles() { + async function checkUnhoistedFiles(packageDir: string) { const results = await Promise.all([ file(join(packageDir, "node_modules", "dep-file-dep", "package.json")).json(), file(join(packageDir, "node_modules", "file-dep", "package.json")).json(), @@ -4612,6 +4740,7 @@ describe("transitive file dependencies", () => { } test("from hoisted workspace dependencies", async () => { + const { packageDir, packageJson, env } = await setupTest(); await Promise.all([ write( packageJson, @@ -4653,7 +4782,7 @@ describe("transitive file dependencies", () => { "14 packages installed", ]); - await checkHoistedFiles(); + await checkHoistedFiles(packageDir); expect(await exists(join(packageDir, "pkg1", "node_modules"))).toBeFalse(); await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); @@ -4668,7 +4797,7 @@ describe("transitive file dependencies", () => { "14 packages installed", ]); - await checkHoistedFiles(); + await checkHoistedFiles(packageDir); ({ out } = await runBunInstall(env, packageDir, { savesLockfile: false })); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); @@ -4679,7 +4808,7 @@ describe("transitive file dependencies", () => { "1 package installed", ]); - await checkHoistedFiles(); + await checkHoistedFiles(packageDir); await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); await rm(join(packageDir, "bun.lockb"), { force: true }); @@ -4702,7 +4831,7 @@ describe("transitive file dependencies", () => { "14 packages installed", ]); - await checkHoistedFiles(); + await checkHoistedFiles(packageDir); expect(await exists(join(packageDir, "pkg1", "node_modules"))).toBeFalse(); ({ out } = await runBunInstall(env, join(packageDir, "pkg1"), { savesLockfile: false })); @@ -4732,9 +4861,12 @@ describe("transitive file dependencies", () => { "", "14 packages installed", ]); + + await checkHoistedFiles(packageDir); }); test("from non-hoisted workspace dependencies", async () => { + const { packageDir, packageJson, env } = await setupTest(); await Promise.all([ write( packageJson, @@ -4795,7 +4927,7 @@ describe("transitive file dependencies", () => { "15 packages installed", ]); - await checkUnhoistedFiles(); + await checkUnhoistedFiles(packageDir); await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); await rm(join(packageDir, "pkg1", "node_modules"), { recursive: true, force: true }); @@ -4818,7 +4950,7 @@ describe("transitive file dependencies", () => { "15 packages installed", ]); - await checkUnhoistedFiles(); + await checkUnhoistedFiles(packageDir); ({ out } = await runBunInstall(env, packageDir, { savesLockfile: false })); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); @@ -4829,7 +4961,7 @@ describe("transitive file dependencies", () => { "1 package installed", ]); - await checkUnhoistedFiles(); + await checkUnhoistedFiles(packageDir); await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); await rm(join(packageDir, "pkg1", "node_modules"), { recursive: true, force: true }); @@ -4853,7 +4985,7 @@ describe("transitive file dependencies", () => { "15 packages installed", ]); - await checkUnhoistedFiles(); + await checkUnhoistedFiles(packageDir); ({ out } = await runBunInstall(env, join(packageDir, "pkg1"), { savesLockfile: false })); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); @@ -4883,9 +5015,12 @@ describe("transitive file dependencies", () => { "", "15 packages installed", ]); + + await checkUnhoistedFiles(packageDir); }); test("from root dependencies", async () => { + const { packageDir, packageJson, env } = await setupTest(); await writeFile( packageJson, JSON.stringify({ @@ -4951,7 +5086,7 @@ describe("transitive file dependencies", () => { "self-file-dep", ]); - await checkHoistedFiles(); + await checkHoistedFiles(packageDir); ({ stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], @@ -4976,7 +5111,7 @@ describe("transitive file dependencies", () => { expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - await checkHoistedFiles(); + await checkHoistedFiles(packageDir); await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); @@ -4995,6 +5130,19 @@ describe("transitive file dependencies", () => { expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); expect(err).not.toContain("panic:"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ @another-scope/file-dep@1.0.0", + "+ @scoped/file-dep@1.0.0", + "+ aliased-file-dep@1.0.1", + "+ dep-file-dep@1.0.0", + expect.stringContaining("+ file-dep@1.0.0"), + "+ missing-file-dep@1.0.0", + "+ self-file-dep@1.0.0", + "", + "13 packages installed", + ]); expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([ "@another-scope", "@scoped", @@ -5007,9 +5155,10 @@ describe("transitive file dependencies", () => { expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - await checkHoistedFiles(); + await checkHoistedFiles(packageDir); }); test("it should install folder dependencies with absolute paths", async () => { + const { packageDir, packageJson, env } = await setupTest(); async function writePackages(num: number) { await rm(join(packageDir, `pkg0`), { recursive: true, force: true }); for (let i = 0; i < num; i++) { @@ -5077,7 +5226,8 @@ describe("transitive file dependencies", () => { }); }); -test("name from manifest is scoped and url encoded", async () => { +test.concurrent("name from manifest is scoped and url encoded", async () => { + const { packageDir, packageJson, env } = await setupTest(); await write( packageJson, JSON.stringify({ @@ -5105,8 +5255,9 @@ test("name from manifest is scoped and url encoded", async () => { ]); }); -describe("update", () => { +describe.concurrent("update", () => { test("duplicate peer dependency (one package is invalid_package_id)", async () => { + const { packageDir, packageJson, env } = await setupTest(); await write( packageJson, JSON.stringify({ @@ -5138,6 +5289,7 @@ describe("update", () => { }); }); test("dist-tags", async () => { + const { packageDir, packageJson, env } = await setupTest(); await write( packageJson, JSON.stringify({ @@ -5187,12 +5339,13 @@ describe("update", () => { }, }); }); - test("exact versions stay exact", async () => { + describe("exact versions stay exact", () => { const runs = [ - { version: "1.0.1", dependency: "a-dep" }, - { version: "npm:a-dep@1.0.1", dependency: "aliased" }, + { version: "1.0.1", dependency: "a-dep", latest: "1.0.10" }, + { version: "npm:a-dep@1.0.1", dependency: "aliased", latest: "npm:a-dep@1.0.10" }, ]; - for (const { version, dependency } of runs) { + test.each(runs)("$dependency", async ({ version, dependency, latest }) => { + const { packageDir, packageJson, env } = await setupTest(); await write( packageJson, JSON.stringify({ @@ -5219,22 +5372,40 @@ describe("update", () => { await runBunInstall(env, packageDir); await check(version); - await runBunUpdate(env, packageDir); + let { out } = await runBunUpdate(env, packageDir); + expect(out).toEqual([ + expect.stringContaining("bun update v1."), + "", + "Checked 1 install across 2 packages (no changes)", + ]); await check(version); - await runBunUpdate(env, packageDir, [dependency]); + ({ out } = await runBunUpdate(env, packageDir, [dependency])); + expect(out).toEqual([ + expect.stringContaining("bun update v1."), + "", + `installed ${dependency}@1.0.1`, + "", + expect.stringContaining("done"), + "", + ]); await check(version); // this will actually update the package, but the version should remain exact - await runBunUpdate(env, packageDir, ["--latest"]); - await check(dependency === "aliased" ? "npm:a-dep@1.0.10" : "1.0.10"); - - await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); - await rm(join(packageDir, "bun.lockb")); - } + ({ out } = await runBunUpdate(env, packageDir, ["--latest"])); + expect(out).toEqual([ + expect.stringContaining("bun update v1."), + "", + `^ ${dependency} 1.0.1 -> 1.0.10`, + "", + "1 package installed", + ]); + await check(latest); + }); }); describe("tilde", () => { test("without args", async () => { + const { packageDir, packageJson, env } = await setupTest(); await write( packageJson, JSON.stringify({ @@ -5287,6 +5458,7 @@ describe("update", () => { for (const latest of [true, false]) { test(`update no args${latest ? " --latest" : ""}`, async () => { + const { packageDir, packageJson, env } = await setupTest(); await write( packageJson, JSON.stringify({ @@ -5392,6 +5564,7 @@ describe("update", () => { } test("with package name in args", async () => { + const { packageDir, packageJson, env } = await setupTest(); await write( packageJson, JSON.stringify({ @@ -5452,6 +5625,7 @@ describe("update", () => { }); describe("alises", () => { test("update all", async () => { + const { packageDir, packageJson, env } = await setupTest(); await write( packageJson, JSON.stringify({ @@ -5477,6 +5651,7 @@ describe("update", () => { }); }); test("update specific aliased package", async () => { + const { packageDir, packageJson, env } = await setupTest(); await write( packageJson, JSON.stringify({ @@ -5502,6 +5677,7 @@ describe("update", () => { }); }); test("with pre and build tags", async () => { + const { packageDir, packageJson, env } = await setupTest(); await write( packageJson, JSON.stringify({ @@ -5546,6 +5722,7 @@ describe("update", () => { }); }); test("--no-save will update packages in node_modules and not save to package.json", async () => { + const { packageDir, packageJson, env } = await setupTest(); await write( packageJson, JSON.stringify({ @@ -5617,6 +5794,7 @@ describe("update", () => { }); }); test("update won't update beyond version range unless the specified version allows it", async () => { + const { packageDir, packageJson, env } = await setupTest(); await write( packageJson, JSON.stringify({ @@ -5668,6 +5846,7 @@ describe("update", () => { }); }); test("update should update all packages in the current workspace", async () => { + const { packageDir, packageJson, env } = await setupTest(); await write( packageJson, JSON.stringify({ @@ -5851,42 +6030,45 @@ describe("update", () => { }, }); }); - test("update different dependency groups", async () => { - for (const args of [true, false]) { - for (const group of ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"]) { - await write( - packageJson, - JSON.stringify({ - name: "foo", - [group]: { - "a-dep": "^1.0.0", - }, - }), - ); - - const { out } = args ? await runBunUpdate(env, packageDir, ["a-dep"]) : await runBunUpdate(env, packageDir); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - - expect(out).toEqual([ - expect.stringContaining("bun update v1."), - "", - args ? "installed a-dep@1.0.10" : expect.stringContaining("+ a-dep@1.0.10"), - "", - "1 package installed", - ]); - expect(await file(packageJson).json()).toEqual({ + describe("update different dependency groups", () => { + const groups = ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"]; + const cases = [true, false].flatMap(args => groups.map(group => ({ args, group }))); + test.each(cases)("a-dep in $group (args: $args)", async ({ args, group }) => { + const { packageDir, packageJson, env } = await setupTest(); + await write( + packageJson, + JSON.stringify({ name: "foo", [group]: { - "a-dep": "^1.0.10", + "a-dep": "^1.0.0", }, - }); + }), + ); - await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); - await rm(join(packageDir, "bun.lockb")); - } - } + const { out } = args ? await runBunUpdate(env, packageDir, ["a-dep"]) : await runBunUpdate(env, packageDir); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + + expect(out).toEqual([ + expect.stringContaining("bun update v1."), + "", + args ? "installed a-dep@1.0.10" : expect.stringContaining("+ a-dep@1.0.10"), + "", + "1 package installed", + ]); + expect(await file(packageJson).json()).toEqual({ + name: "foo", + [group]: { + "a-dep": "^1.0.10", + }, + }); + expect(await file(join(packageDir, "node_modules", "a-dep", "package.json")).json()).toEqual({ + name: "a-dep", + version: "1.0.10", + }); + }); }); test("it should update packages from update requests", async () => { + const { packageDir, packageJson, env } = await setupTest(); await write( packageJson, JSON.stringify({ @@ -6000,6 +6182,7 @@ describe("update", () => { }); test("--latest works with packages from arguments", async () => { + const { packageDir, packageJson, env } = await setupTest(); await write( packageJson, JSON.stringify({ @@ -6022,7 +6205,8 @@ describe("update", () => { }); }); -test("packages dependening on each other with aliases does not infinitely loop", async () => { +test.concurrent("packages dependening on each other with aliases does not infinitely loop", async () => { + const { packageDir, packageJson, env } = await setupTest(); await write( packageJson, JSON.stringify({ @@ -6051,7 +6235,8 @@ test("packages dependening on each other with aliases does not infinitely loop", ]); }); -test("it should re-populate .bin folder if package is reinstalled", async () => { +test.concurrent("it should re-populate .bin folder if package is reinstalled", async () => { + const { packageDir, packageJson, env } = await setupTest(); await writeFile( packageJson, JSON.stringify({ @@ -6133,7 +6318,8 @@ test("it should re-populate .bin folder if package is reinstalled", async () => } }); -test("one version with binary map", async () => { +test.concurrent("one version with binary map", async () => { + const { packageDir, packageJson, env } = await setupTest(); await writeFile( packageJson, JSON.stringify({ @@ -6172,7 +6358,8 @@ test("one version with binary map", async () => { expect(join(packageDir, "node_modules", ".bin", "map_bin")).toBeValidBin(join("..", "map-bin", "bin", "map-bin")); }); -test("multiple versions with binary map", async () => { +test.concurrent("multiple versions with binary map", async () => { + const { packageDir, packageJson, env } = await setupTest(); await writeFile( packageJson, JSON.stringify({ @@ -6216,7 +6403,8 @@ test("multiple versions with binary map", async () => { ); }); -test("duplicate dependency in optionalDependencies maintains sort order", async () => { +test.serial("duplicate dependency in optionalDependencies maintains sort order", async () => { + const { packageDir, packageJson, env } = await setupTest(); await write( packageJson, JSON.stringify({ @@ -6253,7 +6441,8 @@ test("duplicate dependency in optionalDependencies maintains sort order", async expect(await exited).toBe(0); }); -test("missing package on reinstall, some with binaries", async () => { +test.concurrent("missing package on reinstall, some with binaries", async () => { + const { packageDir, packageJson, env } = await setupTest(); await writeFile( packageJson, JSON.stringify({ @@ -6375,8 +6564,9 @@ test("missing package on reinstall, some with binaries", async () => { ).toBe(join(packageDir, "node_modules", "uses-what-bin", "node_modules", ".bin", bin)); }); -describe("pm trust", async () => { +describe.concurrent("pm trust", async () => { test("--default", async () => { + const { packageDir, packageJson, env } = await setupTest(); await writeFile( packageJson, JSON.stringify({ @@ -6404,6 +6594,7 @@ describe("pm trust", async () => { describe("--all", async () => { test("no dependencies", async () => { + const { packageDir, packageJson, env } = await setupTest(); await writeFile( packageJson, JSON.stringify({ @@ -6427,6 +6618,7 @@ describe("pm trust", async () => { }); test("some dependencies, non with scripts", async () => { + const { packageDir, packageJson, env } = await setupTest(); await writeFile( packageJson, JSON.stringify({ @@ -6486,59 +6678,63 @@ describe("pm trust", async () => { }); }); -test("it should be able to find binary in node_modules/.bin from parent directory of root package", async () => { - await mkdir(join(packageDir, "node_modules", ".bin"), { recursive: true }); - await mkdir(join(packageDir, "morePackageDir")); - await writeFile( - join(packageDir, "morePackageDir", "package.json"), - JSON.stringify({ - name: "foo", - version: "1.0.0", - scripts: { - install: "missing-bin", - }, - dependencies: { - "what-bin": "1.0.0", - }, - }), - ); +test.concurrent( + "it should be able to find binary in node_modules/.bin from parent directory of root package", + async () => { + const { packageDir, env } = await setupTest(); + await mkdir(join(packageDir, "node_modules", ".bin"), { recursive: true }); + await mkdir(join(packageDir, "morePackageDir")); + await writeFile( + join(packageDir, "morePackageDir", "package.json"), + JSON.stringify({ + name: "foo", + version: "1.0.0", + scripts: { + install: "missing-bin", + }, + dependencies: { + "what-bin": "1.0.0", + }, + }), + ); - await cp(join(packageDir, "bunfig.toml"), join(packageDir, "morePackageDir", "bunfig.toml")); + await cp(join(packageDir, "bunfig.toml"), join(packageDir, "morePackageDir", "bunfig.toml")); - await writeShebangScript( - join(packageDir, "node_modules", ".bin", "missing-bin"), - "node", - `require("fs").writeFileSync("missing-bin.txt", "missing-bin@WHAT");`, - ); + await writeShebangScript( + join(packageDir, "node_modules", ".bin", "missing-bin"), + "node", + `require("fs").writeFileSync("missing-bin.txt", "missing-bin@WHAT");`, + ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: join(packageDir, "morePackageDir"), - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: join(packageDir, "morePackageDir"), + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("not found"); - expect(err).not.toContain("error:"); - const out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - expect.stringContaining("+ what-bin@1.0.0"), - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("not found"); + expect(err).not.toContain("error:"); + const out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + expect.stringContaining("+ what-bin@1.0.0"), + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); - expect(await file(join(packageDir, "morePackageDir", "missing-bin.txt")).text()).toBe("missing-bin@WHAT"); -}); + expect(await file(join(packageDir, "morePackageDir", "missing-bin.txt")).text()).toBe("missing-bin@WHAT"); + }, +); -describe("semver", () => { +describe.concurrent("semver", () => { const taggedVersionTests = [ { title: "tagged version last in range", @@ -6639,6 +6835,7 @@ describe("semver", () => { for (const { title, depVersion, expected } of taggedVersionTests) { test(title, async () => { + const { packageDir, packageJson, env } = await setupTest(); await writeFile( packageJson, JSON.stringify({ @@ -6671,12 +6868,18 @@ describe("semver", () => { "", "1 package installed", ]); + expect(await file(join(packageDir, "node_modules", "dep-with-tags", "package.json")).json()).toEqual({ + name: "dep-with-tags", + version: expected, + dependencies: {}, + }); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); }); } test.todo("only tagged versions in range errors", async () => { + const { packageDir, packageJson, env } = await setupTest(); await writeFile( packageJson, JSON.stringify({ @@ -6707,7 +6910,8 @@ describe("semver", () => { }); }); -test("doesn't error when the migration is out of sync", async () => { +test.concurrent("doesn't error when the migration is out of sync", async () => { + const { env } = await setupTest(); await using cwd = tempDir("out-of-sync-1", { "package.json": JSON.stringify({ "devDependencies": { @@ -6863,9 +7067,10 @@ const prereleaseTests = [ for (let i = 0; i < prereleaseTests.length; i++) { const tests = prereleaseTests[i]; const depName = `prereleases-${i + 1}`; - describe(`${depName} should pass`, () => { + describe.concurrent(`${depName} should pass`, () => { for (const { title, depVersion, expected } of tests) { test(title, async () => { + const { packageDir, packageJson, env } = await setupTest(); await writeFile( packageJson, JSON.stringify({ @@ -7009,9 +7214,10 @@ const prereleaseFailTests = [ for (let i = 0; i < prereleaseFailTests.length; i++) { const tests = prereleaseFailTests[i]; const depName = `prereleases-${i + 1}`; - describe(`${depName} should fail`, () => { + describe.concurrent(`${depName} should fail`, () => { for (const { title, depVersion } of tests) { test(title, async () => { + const { packageDir, packageJson, env } = await setupTest(); await writeFile( packageJson, JSON.stringify({ @@ -7043,8 +7249,9 @@ for (let i = 0; i < prereleaseFailTests.length; i++) { }); } -describe("yarn tests", () => { +describe.concurrent("yarn tests", () => { test("dragon test 1", async () => { + const { packageDir, packageJson, env } = await setupTest(); await writeFile( packageJson, JSON.stringify({ @@ -7109,6 +7316,7 @@ describe("yarn tests", () => { }); test("dragon test 2", async () => { + const { packageDir, packageJson, env } = await setupTest(); await writeFile( packageJson, JSON.stringify({ @@ -7183,6 +7391,7 @@ describe("yarn tests", () => { }); test("dragon test 3", async () => { + const { packageDir, packageJson, env } = await setupTest(); await writeFile( packageJson, JSON.stringify({ @@ -7235,6 +7444,7 @@ describe("yarn tests", () => { }); test("dragon test 4", async () => { + const { packageDir, packageJson, env } = await setupTest(); await writeFile( packageJson, JSON.stringify({ @@ -7297,6 +7507,7 @@ describe("yarn tests", () => { }); test("dragon test 5", async () => { + const { packageDir, packageJson, env } = await setupTest(); await writeFile( packageJson, JSON.stringify({ @@ -7380,6 +7591,7 @@ describe("yarn tests", () => { }); test.todo("dragon test 6", async () => { + const { packageDir, packageJson, env } = await setupTest(); await writeFile( packageJson, JSON.stringify({ @@ -7497,6 +7709,7 @@ describe("yarn tests", () => { }); test.todo("dragon test 7", async () => { + const { packageDir, packageJson, env } = await setupTest(); await writeFile( packageJson, JSON.stringify({ @@ -7580,6 +7793,7 @@ describe("yarn tests", () => { }); test("dragon test 8", async () => { + const { packageDir, packageJson, env } = await setupTest(); await writeFile( packageJson, JSON.stringify({ @@ -7623,6 +7837,7 @@ describe("yarn tests", () => { }); test("dragon test 9", async () => { + const { packageDir, packageJson, env } = await setupTest(); await writeFile( packageJson, JSON.stringify({ @@ -7666,6 +7881,7 @@ describe("yarn tests", () => { }); test.todo("dragon test 10", async () => { + const { packageDir, packageJson, env } = await setupTest(); await writeFile( packageJson, JSON.stringify({ @@ -7741,6 +7957,7 @@ describe("yarn tests", () => { }); test("dragon test 12", async () => { + const { packageDir, packageJson, env } = await setupTest(); await writeFile( packageJson, JSON.stringify({ @@ -7814,6 +8031,7 @@ describe("yarn tests", () => { }); test("it should not warn when the peer dependency resolution is compatible", async () => { + const { packageDir, packageJson, env } = await setupTest(); await writeFile( packageJson, JSON.stringify({ @@ -7855,6 +8073,7 @@ describe("yarn tests", () => { }); test("it should warn when the peer dependency resolution is incompatible", async () => { + const { packageDir, packageJson, env } = await setupTest(); await writeFile( packageJson, JSON.stringify({ @@ -7896,6 +8115,7 @@ describe("yarn tests", () => { }); test("it should install in such a way that two identical packages with different peer dependencies are different instances", async () => { + const { packageDir, packageJson, env } = await setupTest(); await writeFile( packageJson, JSON.stringify({ @@ -8004,6 +8224,7 @@ describe("yarn tests", () => { }); test("it should install in such a way that two identical packages with the same peer dependencies are the same instances (simple)", async () => { + const { packageDir, packageJson, env } = await setupTest(); await writeFile( packageJson, JSON.stringify({ @@ -8068,6 +8289,7 @@ describe("yarn tests", () => { }); test("it should install in such a way that two identical packages with the same peer dependencies are the same instances (complex)", async () => { + const { packageDir, packageJson, env } = await setupTest(); await writeFile( packageJson, JSON.stringify({ @@ -8134,6 +8356,7 @@ describe("yarn tests", () => { }); test("it shouldn't deduplicate two packages with similar peer dependencies but different names", async () => { + const { packageDir, packageJson, env } = await setupTest(); await writeFile( packageJson, JSON.stringify({ @@ -8194,6 +8417,7 @@ describe("yarn tests", () => { }); test("it should reinstall and rebuild dependencies deleted by the user on the next install", async () => { + const { packageDir, packageJson, env } = await setupTest(); await writeFile( packageJson, JSON.stringify({ @@ -8255,7 +8479,8 @@ describe("yarn tests", () => { }); }); -test("tarball `./` prefix, duplicate directory with file, and empty directory", async () => { +test.concurrent("tarball `./` prefix, duplicate directory with file, and empty directory", async () => { + const { packageDir, packageJson, env } = await setupTest(); await write( packageJson, JSON.stringify({ @@ -8331,7 +8556,7 @@ test("tarball `./` prefix, duplicate directory with file, and empty directory", ); }); -describe("outdated", () => { +describe.concurrent("outdated", () => { const edgeCaseTests = [ { description: "normal dep, smaller than column title", @@ -8400,7 +8625,9 @@ describe("outdated", () => { ]; for (const { description, packageJson } of edgeCaseTests) { - test(description, async () => { + // serial: `toMatchSnapshot()` is not available inside concurrent tests + test.serial(description, async () => { + const { packageDir, env } = await setupTest(); await write(join(packageDir, "package.json"), JSON.stringify(packageJson)); await runBunInstall(env, packageDir); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); @@ -8414,20 +8641,19 @@ describe("outdated", () => { env: testEnv, }); - expect(await exited).toBe(0); - - const err = await stderr.text(); + const [out, err, exitCode] = await Promise.all([stdout.text(), stderr.text(), exited]); expect(err).not.toContain("error:"); expect(err).not.toContain("panic:"); - const out = await stdout.text(); const first = out.slice(0, out.indexOf("\n")); expect(first).toEqual(expect.stringContaining("bun outdated ")); expect(first).toEqual(expect.stringContaining("v1.")); const rest = out.slice(out.indexOf("\n") + 1); expect(rest).toMatchSnapshot(); + expect(exitCode).toBe(0); }); } test("in workspace", async () => { + const { packageDir, packageJson, env } = await setupTest(); await Promise.all([ write( packageJson, @@ -8485,7 +8711,8 @@ describe("outdated", () => { expect(await exited).toBe(0); }); - test("NO_COLOR works", async () => { + test.serial("NO_COLOR works", async () => { + const { packageDir, packageJson, env } = await setupTest(); await write( packageJson, JSON.stringify({ @@ -8522,10 +8749,10 @@ describe("outdated", () => { expect(await exited).toBe(0); }); - async function setupWorkspace() { + async function setupWorkspace(packageDir: string) { await Promise.all([ write( - packageJson, + join(packageDir, "package.json"), JSON.stringify({ name: "foo", workspaces: ["packages/*"], @@ -8574,7 +8801,8 @@ describe("outdated", () => { } test("--filter with workspace names and paths", async () => { - await setupWorkspace(); + const { packageDir, env } = await setupTest(); + await setupWorkspace(packageDir); await runBunInstall(env, packageDir); let out = await runBunOutdated(env, packageDir, "--filter", "*"); @@ -8600,18 +8828,19 @@ describe("outdated", () => { }); test("dependency pattern args", async () => { - await setupWorkspace(); + const { packageDir, env } = await setupTest(); + await setupWorkspace(packageDir); await runBunInstall(env, packageDir); let out = await runBunOutdated(env, packageDir, "no-deps", "--filter", "*"); expect(out).toContain("no-deps"); expect(out).not.toContain("a-dep"); - expect(out).not.toContain("prerelease-1"); + expect(out).not.toContain("prereleases-1"); out = await runBunOutdated(env, packageDir, "a-dep"); expect(out).not.toContain("a-dep"); expect(out).not.toContain("no-deps"); - expect(out).not.toContain("prerelease-1"); + expect(out).not.toContain("prereleases-1"); out = await runBunOutdated(env, packageDir, "*", "--filter", "*"); expect(out).toContain("no-deps"); @@ -8620,6 +8849,7 @@ describe("outdated", () => { }); test("scoped workspace names", async () => { + const { packageDir, packageJson, env } = await setupTest(); await Promise.all([ write( packageJson, @@ -8654,6 +8884,7 @@ describe("outdated", () => { }); test("catalog dependencies", async () => { + const { packageDir, packageJson, env } = await setupTest(); await Promise.all([ write( packageJson, @@ -8694,6 +8925,7 @@ describe("outdated", () => { }); test("--recursive flag for outdated", async () => { + const { packageDir, env } = await setupTest(); // First verify the flag appears in help const { stdout: helpOut, @@ -8713,7 +8945,7 @@ describe("outdated", () => { expect(help).toContain("-r"); // Setup workspace - await setupWorkspace(); + await setupWorkspace(packageDir); await runBunInstall(env, packageDir); // Test --recursive shows all workspaces @@ -8724,6 +8956,7 @@ describe("outdated", () => { }); test("catalog grouping with multiple workspaces", async () => { + const { packageDir, packageJson, env } = await setupTest(); await Promise.all([ write( packageJson, @@ -8777,7 +9010,7 @@ describe("outdated", () => { // This test is to verify that BinLinkingShim.zig creates correct shim files as // well as bun_shim_impl.exe works in various edge cases. There are many fast // paths for many many cases. -describe("windows bin linking shim should work", async () => { +describe.concurrent("windows bin linking shim should work", async () => { if (!isWindows) return; const packageDir = tmpdirSync(); @@ -8810,7 +9043,7 @@ describe("windows bin linking shim should work", async () => { stdout: "pipe", stdin: "pipe", stderr: "pipe", - env, + env: bunEnv, }); var err = await stderr.text(); @@ -8840,6 +9073,8 @@ describe("windows bin linking shim should work", async () => { copyFileSync(join(packageDir, "node_modules\\bunx-bins\\native.exe"), join(temp_bin_dir, "native.exe")); const PATH = process.env.PATH + ";" + temp_bin_dir; + // Every test below only executes the bins installed above, so they can all share this directory. + const shimEnv = mergeWindowEnvs([bunEnv, { PATH }]); const bins = [ { bin: "bin1", name: "bin1" }, @@ -8863,7 +9098,7 @@ describe("windows bin linking shim should work", async () => { stdout: "pipe", stdin: "pipe", stderr: "pipe", - env: mergeWindowEnvs([env, { PATH: PATH }]), + env: shimEnv, }); expect(stderr).toBeDefined(); const err = await stderr.text(); @@ -8882,7 +9117,7 @@ describe("windows bin linking shim should work", async () => { stdout: "pipe", stdin: "pipe", stderr: "pipe", - env: mergeWindowEnvs([env, { PATH: PATH }]), + env: shimEnv, }); expect(stderr).toBeDefined(); const err = await stderr.text(); @@ -8901,7 +9136,7 @@ describe("windows bin linking shim should work", async () => { stdout: "pipe", stdin: "pipe", stderr: "pipe", - env: mergeWindowEnvs([env, { PATH: PATH }]), + env: shimEnv, }); expect(stderr).toBeDefined(); const err = await stderr.text(); @@ -8920,7 +9155,7 @@ describe("windows bin linking shim should work", async () => { stdout: "pipe", stdin: "pipe", stderr: "pipe", - env: mergeWindowEnvs([env, { PATH: PATH }]), + env: shimEnv, }); expect(stderr).toBeDefined(); const err = await stderr.text(); @@ -8932,7 +9167,8 @@ describe("windows bin linking shim should work", async () => { } }); -test("rejects dependency aliases containing relative path segments", async () => { +test.concurrent("rejects dependency aliases containing relative path segments", async () => { + const { packageDir, packageJson, env } = await setupTest(); // A dependency alias is used verbatim as a folder name when building install // paths (`node_modules//node_modules/...`). `one-fixed-dep@2.0.0` // depends on `no-deps@2.0.0`, which conflicts with the root `no-deps@1.0.0` @@ -9008,7 +9244,8 @@ test("rejects dependency aliases containing relative path segments", async () => expect(await exited).toBe(0); }); -test("rejects package names containing relative path components in bun.lock", async () => { +test.concurrent("rejects package names containing relative path components in bun.lock", async () => { + const { packageDir, packageJson, env } = await setupTest(); // The package name from a bun.lock `packages` entry is written verbatim into // the cache folder name (`/@@@@@@1`) before the // extracted tarball is renamed into place. A name with a leading `..` @@ -9081,7 +9318,9 @@ test("rejects package names containing relative path components in bun.lock", as expect(await exited).toBe(0); }); -test("rejects npm aliases whose manifest URL resolves to a different host than the registry", async () => { +// serial: registers a registry user (see "whoami") +test.serial("rejects npm aliases whose manifest URL resolves to a different host than the registry", async () => { + const { packageDir, packageJson, env } = await setupTest(); // The manifest URL is built by joining the registry URL with the package // name. WHATWG URL joining treats "\" like "/" for http(s) schemes, so a // name beginning with two backslashes becomes a protocol-relative authority: @@ -9141,144 +9380,149 @@ test("rejects npm aliases whose manifest URL resolves to a different host than t expect(await exited).not.toBe(0); }); -test("registry override from a project .env only keeps the saved token when the host matches and the scheme is not downgraded", async () => { - // `bun install` loads the project's `.env` before computing installer - // options, so a repo-committed `.env` can point BUN_CONFIG_REGISTRY at a - // different registry host. The token configured for the default registry - // scope is host-scoped and must only be attached to requests for that host. - const received: { url: string; authorization: string | null }[] = []; - using otherRegistry = Bun.serve({ - port: 0, - fetch(req) { - received.push({ url: req.url, authorization: req.headers.get("authorization") }); - return new Response("not found", { status: 404 }); - }, - }); - - const token = "default-registry-secret-token"; +test.concurrent( + "registry override from a project .env only keeps the saved token when the host matches and the scheme is not downgraded", + async () => { + const { packageDir, packageJson, env } = await setupTest(); + // `bun install` loads the project's `.env` before computing installer + // options, so a repo-committed `.env` can point BUN_CONFIG_REGISTRY at a + // different registry host. The token configured for the default registry + // scope is host-scoped and must only be attached to requests for that host. + const received: { url: string; authorization: string | null }[] = []; + using otherRegistry = Bun.serve({ + port: 0, + fetch(req) { + received.push({ url: req.url, authorization: req.headers.get("authorization") }); + return new Response("not found", { status: 404 }); + }, + }); - // Case 1: the .env points the registry at a different host. The manifest - // request must reach that host without the default registry's token. - await Promise.all([ - write( - join(packageDir, "bunfig.toml"), - Bun.TOML.stringify({ - install: { - cache: false, - registry: { url: `http://localhost:${port}/`, token }, - }, - }), - ), - write( - packageJson, - JSON.stringify({ - name: "foo", - version: "1.0.0", - dependencies: { - "no-deps": "1.0.0", - }, - }), - ), - write(join(packageDir, ".env"), `BUN_CONFIG_REGISTRY=http://127.0.0.1:${otherRegistry.port}/\n`), - ]); + const token = "default-registry-secret-token"; - { - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - await stderr.text(); - await stdout.text(); + // Case 1: the .env points the registry at a different host. The manifest + // request must reach that host without the default registry's token. + await Promise.all([ + write( + join(packageDir, "bunfig.toml"), + Bun.TOML.stringify({ + install: { + cache: false, + registry: { url: `http://localhost:${port}/`, token }, + }, + }), + ), + write( + packageJson, + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "no-deps": "1.0.0", + }, + }), + ), + write(join(packageDir, ".env"), `BUN_CONFIG_REGISTRY=http://127.0.0.1:${otherRegistry.port}/\n`), + ]); - // The .env override must take effect: the manifest request goes to the - // overridden registry... - expect(received.length).toBeGreaterThan(0); - // ...but the token configured for the localhost registry must not be sent - // to the different host. - expect(received.filter(r => r.authorization !== null)).toEqual([]); - // The overridden registry returned 404, so this install fails. - expect(await exited).not.toBe(0); - } + { + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + await stderr.text(); + await stdout.text(); + + // The .env override must take effect: the manifest request goes to the + // overridden registry... + expect(received.length).toBeGreaterThan(0); + // ...but the token configured for the localhost registry must not be sent + // to the different host. + expect(received.filter(r => r.authorization !== null)).toEqual([]); + // The overridden registry returned 404, so this install fails. + expect(await exited).not.toBe(0); + } - // Case 2: when the override points at the same host the token was - // configured for, the token is still sent. - received.length = 0; - await Promise.all([ - rm(join(packageDir, "bun.lock"), { force: true }), - rm(join(packageDir, "bun.lockb"), { force: true }), - write( - join(packageDir, "bunfig.toml"), - Bun.TOML.stringify({ - install: { - cache: false, - registry: { url: `http://127.0.0.1:${otherRegistry.port}/`, token }, - }, - }), - ), - write(join(packageDir, ".env"), `BUN_CONFIG_REGISTRY=http://127.0.0.1:${otherRegistry.port}/\n`), - ]); + // Case 2: when the override points at the same host the token was + // configured for, the token is still sent. + received.length = 0; + await Promise.all([ + rm(join(packageDir, "bun.lock"), { force: true }), + rm(join(packageDir, "bun.lockb"), { force: true }), + write( + join(packageDir, "bunfig.toml"), + Bun.TOML.stringify({ + install: { + cache: false, + registry: { url: `http://127.0.0.1:${otherRegistry.port}/`, token }, + }, + }), + ), + write(join(packageDir, ".env"), `BUN_CONFIG_REGISTRY=http://127.0.0.1:${otherRegistry.port}/\n`), + ]); - { - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - await stderr.text(); - await stdout.text(); + { + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + await stderr.text(); + await stdout.text(); - expect(received.length).toBeGreaterThan(0); - expect(received.some(r => r.authorization === `Bearer ${token}`)).toBe(true); - expect(await exited).not.toBe(0); - } + expect(received.length).toBeGreaterThan(0); + expect(received.some(r => r.authorization === `Bearer ${token}`)).toBe(true); + expect(await exited).not.toBe(0); + } - // Case 3: the override points at the same host but downgrades https to - // http. The token configured for the https registry must not be sent. - received.length = 0; - await Promise.all([ - rm(join(packageDir, "bun.lock"), { force: true }), - rm(join(packageDir, "bun.lockb"), { force: true }), - write( - join(packageDir, "bunfig.toml"), - Bun.TOML.stringify({ - install: { - cache: false, - registry: { url: `https://127.0.0.1:${otherRegistry.port}/`, token }, - }, - }), - ), - write(join(packageDir, ".env"), `BUN_CONFIG_REGISTRY=http://127.0.0.1:${otherRegistry.port}/\n`), - ]); + // Case 3: the override points at the same host but downgrades https to + // http. The token configured for the https registry must not be sent. + received.length = 0; + await Promise.all([ + rm(join(packageDir, "bun.lock"), { force: true }), + rm(join(packageDir, "bun.lockb"), { force: true }), + write( + join(packageDir, "bunfig.toml"), + Bun.TOML.stringify({ + install: { + cache: false, + registry: { url: `https://127.0.0.1:${otherRegistry.port}/`, token }, + }, + }), + ), + write(join(packageDir, ".env"), `BUN_CONFIG_REGISTRY=http://127.0.0.1:${otherRegistry.port}/\n`), + ]); - { - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: packageDir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - await stderr.text(); - await stdout.text(); + { + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + await stderr.text(); + await stdout.text(); - expect(received.length).toBeGreaterThan(0); - expect(received.filter(r => r.authorization !== null)).toEqual([]); - expect(await exited).not.toBe(0); - } -}); + expect(received.length).toBeGreaterThan(0); + expect(received.filter(r => r.authorization !== null)).toEqual([]); + expect(await exited).not.toBe(0); + } + }, +); -describe("registry/token env var priority", () => { +describe.concurrent("registry/token env var priority", () => { // BUN_CONFIG_* takes precedence over NPM_CONFIG_*, which takes precedence // over npm_config_*. An empty value falls through to the next candidate. async function installAndCaptureAuth(extraEnv: Record) { + const { packageDir, packageJson, env } = await setupTest(); const received: (string | null)[] = []; await using server = Bun.serve({ port: 0, @@ -9335,6 +9579,7 @@ describe("registry/token env var priority", () => { }); test("BUN_CONFIG_REGISTRY wins over NPM_CONFIG_REGISTRY", async () => { + const { packageDir, packageJson, env } = await setupTest(); const hits = { preferred: 0, other: 0 }; await using preferred = Bun.serve({ port: 0, @@ -9374,7 +9619,8 @@ describe("registry/token env var priority", () => { }); }); -test("npm manifest cache entries with invalid package version records are treated as invalid", async () => { +test.concurrent("npm manifest cache entries with invalid package version records are treated as invalid", async () => { + const { packageDir, packageJson, env } = await setupTest(); await write( packageJson, JSON.stringify({ @@ -9436,7 +9682,8 @@ test("npm manifest cache entries with invalid package version records are treate expect(() => parseManifest(corrupted, registryUrl())).toThrow("manifest is invalid"); }); -test("npm manifest cache entries are only reused for the package name they were saved for", async () => { +test.concurrent("npm manifest cache entries are only reused for the package name they were saved for", async () => { + const { packageDir, packageJson, env } = await setupTest(); const { parseManifest } = npm_manifest_test_helpers; const cacheDir = join(packageDir, ".bun-cache"); From de573e415e147af80ff98d04a19265172a9ab075 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 14 Aug 2026 13:43:16 +0000 Subject: [PATCH 2/3] test(install): chain createTestDir() calls and give --bun shim tests their own TEMP Concurrent rm() of the registry's htpasswd file rejects with EFAULT on Windows, and concurrent --bun runs share the fake node directory in TEMP. --- test/cli/install/bun-install-registry.test.ts | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/test/cli/install/bun-install-registry.test.ts b/test/cli/install/bun-install-registry.test.ts index 3ff513328f3c..5e067e5b79a3 100644 --- a/test/cli/install/bun-install-registry.test.ts +++ b/test/cli/install/bun-install-registry.test.ts @@ -51,10 +51,19 @@ afterAll(() => { * `serial`, so nothing else may be shared between tests. The serial ones use `toMatchSnapshot()` * (not available in concurrent tests), register registry users, or need the registry to be idle. */ +let previousSetup: Promise = Promise.resolve(); async function setupTest() { - const { packageDir, packageJson } = await registry.createTestDir({ - bunfigOpts: { saveTextLockfile: false, linker: "hoisted" }, - }); + // `createTestDir()` also deletes the registry's htpasswd file. When a concurrent group starts, + // every test in it calls this at once, and concurrent deletions of the same file fail on Windows + // (EFAULT), so the calls are chained. The directory creation itself takes a few milliseconds. + const created = previousSetup.then(() => + registry.createTestDir({ bunfigOpts: { saveTextLockfile: false, linker: "hoisted" } }), + ); + previousSetup = created.then( + () => {}, + () => {}, + ); + const { packageDir, packageJson } = await created; const tmpDir = join(packageDir, ".bun-tmp"); const env: NodeJS.Dict = { ...bunEnv, @@ -9075,6 +9084,13 @@ describe.concurrent("windows bin linking shim should work", async () => { const PATH = process.env.PATH + ";" + temp_bin_dir; // Every test below only executes the bins installed above, so they can all share this directory. const shimEnv = mergeWindowEnvs([bunEnv, { PATH }]); + // `--bun` creates its fake `node.exe` inside %TEMP% (and debug builds recreate it on every run), + // so each `--bun` test gets its own temp directory instead of racing the others on the shared one. + function shimEnvWithOwnTemp(subdir: string) { + const temp = join(packageDir, subdir); + mkdirSync(temp); + return mergeWindowEnvs([shimEnv, { TMP: temp, TEMP: temp }]); + } const bins = [ { bin: "bin1", name: "bin1" }, @@ -9117,7 +9133,7 @@ describe.concurrent("windows bin linking shim should work", async () => { stdout: "pipe", stdin: "pipe", stderr: "pipe", - env: shimEnv, + env: shimEnvWithOwnTemp(`temp-run-${bin}`), }); expect(stderr).toBeDefined(); const err = await stderr.text(); @@ -9136,7 +9152,7 @@ describe.concurrent("windows bin linking shim should work", async () => { stdout: "pipe", stdin: "pipe", stderr: "pipe", - env: shimEnv, + env: shimEnvWithOwnTemp(`temp-x-${bin}`), }); expect(stderr).toBeDefined(); const err = await stderr.text(); From bb646452b4bbd633753fea8c9ee1e428c30005f1 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 14 Aug 2026 17:27:16 +0000 Subject: [PATCH 3/3] test(install): use a literal token in the alias host-pinning test so it can run concurrently The request is refused before anything is sent, so registering a registry user (which kept the test serial) was not needed. --- test/cli/install/bun-install-registry.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/cli/install/bun-install-registry.test.ts b/test/cli/install/bun-install-registry.test.ts index 5e067e5b79a3..d3913b844655 100644 --- a/test/cli/install/bun-install-registry.test.ts +++ b/test/cli/install/bun-install-registry.test.ts @@ -9334,8 +9334,7 @@ test.concurrent("rejects package names containing relative path components in bu expect(await exited).toBe(0); }); -// serial: registers a registry user (see "whoami") -test.serial("rejects npm aliases whose manifest URL resolves to a different host than the registry", async () => { +test.concurrent("rejects npm aliases whose manifest URL resolves to a different host than the registry", async () => { const { packageDir, packageJson, env } = await setupTest(); // The manifest URL is built by joining the registry URL with the package // name. WHATWG URL joining treats "\" like "/" for http(s) schemes, so a @@ -9353,7 +9352,8 @@ test.serial("rejects npm aliases whose manifest URL resolves to a different host }, }); - const token = await generateRegistryUser("manifest-host-pinning", "manifest-host-pinning"); + // The request is refused before it is sent, so the token only has to exist, not be valid. + const token = "manifest-host-pinning-token"; await Promise.all([ write( join(packageDir, "bunfig.toml"),