From 5c6e1c183f246042f8a6f126f0653a7c70d731a0 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 15 Aug 2026 18:38:58 +0000 Subject: [PATCH 1/3] test(install): retry the warm-cache resolve in the unmet peer test until the manifest write has landed The "declared by a registry package" case of "peer no published version satisfies" resolves from scratch a second time and asserts the registry is not contacted, which needs both manifests in the manifest cache. Cache entries are written by a thread pool task that bun install does not wait for, and peer-target's manifest is the last thing the first install fetches, so under load that install sometimes exits before the entry is on disk and the second resolve requests /peer-target again. A resolve that had to refetch the manifest writes the entry again, so repeat the from-scratch resolve (a few attempts at most) until one ran without contacting the registry, and keep asserting the warning and the identical lockfile on every attempt. --- test/cli/install/bun-lock.test.ts | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/test/cli/install/bun-lock.test.ts b/test/cli/install/bun-lock.test.ts index c8ba27d17e5f..cfb0bfd89691 100644 --- a/test/cli/install/bun-lock.test.ts +++ b/test/cli/install/bun-lock.test.ts @@ -1768,15 +1768,25 @@ describe.each(["hoisted", "isolated"] as const)("peer no published version satis expect(err).not.toContain("Ignoring lockfile"); expect(await file(lockfilePath).text()).toBe(lockfile); - // Resolve from scratch again. Both manifests are cached now, so the peer - // is looked up synchronously instead of through a network task. - await rm(lockfilePath); - await rm(join(String(dir), "node_modules"), { recursive: true }); - registry.requests.length = 0; - ({ err } = await install(String(dir))); - expect(err).toContain(unmetPeerWarning); + // Resolve from scratch again with both manifests cached: the peer is then + // looked up synchronously instead of through a network task (the lookup that + // used to loop forever) and the registry is not contacted at all. A cache + // entry is written by a thread pool task that bun install does not wait for + // (#37203), and peer-target's manifest is the last thing the first install + // fetches, so on a heavily loaded machine about one install in ten exits + // before that entry is on disk. A resolve that had to fetch the manifest + // again writes the entry again, with the same small chance of losing it, so + // allow a few attempts. + for (let attempt = 1; ; attempt++) { + await rm(lockfilePath); + await rm(join(String(dir), "node_modules"), { recursive: true }); + registry.requests.length = 0; + ({ err } = await install(String(dir))); + expect(err).toContain(unmetPeerWarning); + expect(await file(lockfilePath).text()).toBe(lockfile); + if (registry.requests.length === 0 || attempt === 5) break; + } expect(registry.requests).toEqual([]); - expect(await file(lockfilePath).text()).toBe(lockfile); }); it.concurrent("declared by the root package and a workspace", async () => { From 3af43b26366c4596aa496efdc24dfd426bbe5e10 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 15 Aug 2026 18:45:45 +0000 Subject: [PATCH 2/3] test(install): name the retry bound of the warm-cache resolve --- test/cli/install/bun-lock.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/cli/install/bun-lock.test.ts b/test/cli/install/bun-lock.test.ts index cfb0bfd89691..5a4581ac4a38 100644 --- a/test/cli/install/bun-lock.test.ts +++ b/test/cli/install/bun-lock.test.ts @@ -1777,14 +1777,15 @@ describe.each(["hoisted", "isolated"] as const)("peer no published version satis // before that entry is on disk. A resolve that had to fetch the manifest // again writes the entry again, with the same small chance of losing it, so // allow a few attempts. - for (let attempt = 1; ; attempt++) { + const maxAttempts = 5; + for (let attempt = 0; attempt < maxAttempts; attempt++) { await rm(lockfilePath); await rm(join(String(dir), "node_modules"), { recursive: true }); registry.requests.length = 0; ({ err } = await install(String(dir))); expect(err).toContain(unmetPeerWarning); expect(await file(lockfilePath).text()).toBe(lockfile); - if (registry.requests.length === 0 || attempt === 5) break; + if (registry.requests.length === 0) break; } expect(registry.requests).toEqual([]); }); From 258f9f78d671e862738494e4113e51121660b621 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 16 Aug 2026 00:42:37 +0000 Subject: [PATCH 3/3] test(install): establish the warm manifest cache before the unmet peer re-resolve instead of retrying it Check the project's cache for both manifest entries before the from-scratch resolve and re-run the install (without the lockfile) until they are there, then run the resolve once with its zero-requests assertion unchanged. The retry now only re-establishes the precondition that the fire-and-forget manifest write can leave unmet, so a refetch during the resolve itself fails the test on the first run. --- test/cli/install/bun-lock.test.ts | 48 +++++++++++++++++++------------ 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/test/cli/install/bun-lock.test.ts b/test/cli/install/bun-lock.test.ts index 5a4581ac4a38..fd31c0e81929 100644 --- a/test/cli/install/bun-lock.test.ts +++ b/test/cli/install/bun-lock.test.ts @@ -1721,7 +1721,7 @@ describe.each(["hoisted", "isolated"] as const)("peer no published version satis // The request assertions below need a cache of their own per project: the // environment's cache dir takes precedence over bunfig, and a package // extracted there by one of the concurrent tests is not downloaded again. - env: { ...env, BUN_INSTALL_CACHE_DIR: join(cwd, ".bun-cache") }, + env: { ...env, BUN_INSTALL_CACHE_DIR: cacheDir(cwd) }, stdout: "pipe", stderr: "pipe", // Only matters if an install never returns. @@ -1732,6 +1732,19 @@ describe.each(["hoisted", "isolated"] as const)("peer no published version satis return { out, err }; } + function cacheDir(cwd: string) { + return join(cwd, ".bun-cache"); + } + + // How many manifests the project's installs have written to its cache. The + // entries are written by a thread pool task that bun install does not wait + // for before exiting (`save_async` in src/install/npm.rs), so the last + // manifest an install fetches is occasionally missing. An install that has + // to fetch it again writes it again. + function cachedManifests(cwd: string) { + return Array.from(new Bun.Glob("*.npm").scanSync(cacheDir(cwd))).length; + } + it.concurrent("declared by a registry package", async () => { using registry = await serveRegistry(); using dir = createProject(registry.url, { @@ -1768,26 +1781,25 @@ describe.each(["hoisted", "isolated"] as const)("peer no published version satis expect(err).not.toContain("Ignoring lockfile"); expect(await file(lockfilePath).text()).toBe(lockfile); - // Resolve from scratch again with both manifests cached: the peer is then - // looked up synchronously instead of through a network task (the lookup that - // used to loop forever) and the registry is not contacted at all. A cache - // entry is written by a thread pool task that bun install does not wait for - // (#37203), and peer-target's manifest is the last thing the first install - // fetches, so on a heavily loaded machine about one install in ten exits - // before that entry is on disk. A resolve that had to fetch the manifest - // again writes the entry again, with the same small chance of losing it, so - // allow a few attempts. - const maxAttempts = 5; - for (let attempt = 0; attempt < maxAttempts; attempt++) { + // The resolve below needs both manifests cached. peer-target's was the last + // thing the first install fetched, and nothing was left to do after it, so + // its entry is the one that is occasionally missing (see cachedManifests); + // resolving without the lockfile fetches and writes it again. + for (let retries = 5; retries > 0 && cachedManifests(String(dir)) < 2; retries--) { await rm(lockfilePath); - await rm(join(String(dir), "node_modules"), { recursive: true }); - registry.requests.length = 0; - ({ err } = await install(String(dir))); - expect(err).toContain(unmetPeerWarning); - expect(await file(lockfilePath).text()).toBe(lockfile); - if (registry.requests.length === 0) break; + await install(String(dir)); } + expect(cachedManifests(String(dir))).toBe(2); + + // Resolve from scratch again. Both manifests are cached now, so the peer + // is looked up synchronously instead of through a network task. + await rm(lockfilePath); + await rm(join(String(dir), "node_modules"), { recursive: true }); + registry.requests.length = 0; + ({ err } = await install(String(dir))); + expect(err).toContain(unmetPeerWarning); expect(registry.requests).toEqual([]); + expect(await file(lockfilePath).text()).toBe(lockfile); }); it.concurrent("declared by the root package and a workspace", async () => {