Skip to content

test(install): don't require the refetched manifest to be cached before install exits - #38580

Open
robobun wants to merge 1 commit into
mainfrom
farm/c35797dc/manifest-name-check-test
Open

test(install): don't require the refetched manifest to be cached before install exits#38580
robobun wants to merge 1 commit into
mainfrom
farm/c35797dc/manifest-name-check-test

Conversation

@robobun

@robobun robobun commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

Problem

  • test/cli/install/bun-install-registry.test.ts > "npm manifest cache entries are only reused for the package name they were saved for" (added in Robustness and input-handling pass across install, shell, TLS/QUIC, HTTP/3, SQL and crypto #37669) fails on every run with a debug build on Windows (Server 2019 x64, 5 of 5 runs; it passes on the release Windows lanes and on Linux):
    error: failed to open manifest file "...\.bun-cache\472ae378b15ad7b8-9eb9626f42b8b153.npm": ENOENT
    
    at the last assertion, parseManifest(byName["no-deps"], ...) after the second install.
  • The second install behaves correctly: the entry holding basic-1's manifest is rejected and deleted (Serializer::load_by_file_id, src/install/npm.rs), no-deps is fetched again, node_modules/no-deps is 1.0.0 and the lockfile points at the no-deps tarball. The assertion additionally requires the refetched manifest to be back on disk when the process exits.
  • That write is a thread pool task (Serializer::save_async) that bun install does not wait for. test(security-scanner-matrix): don't depend on the setup install's manifest cache writes having landed #37203 tried adding a wait and reverted it: the write is fire-and-forget by design, a missing entry only means a refetch. In this test the no-deps tarball is already in the cache from the first install, so nothing remains to do after the manifest arrives and the install exits before the task lands. On a Windows debug build that is not a narrow race: with timestamps added locally, the save task took about 25 ms from start to rename while the main thread went from receiving the manifest to "Saved lockfile" in about 12 ms, so the file was missing on every run.
  • The first install of the same test (and the other tests that read the cache after an install) also rely on the writes landing, but there the tarball downloads and extraction happen after the manifests arrive: on the same machine its two writes had finished about 45 ms before it was done resolving. test(security-scanner-matrix): don't depend on the setup install's manifest cache writes having landed #37203's measurements show that margin can still be lost occasionally under heavy load on release builds; this PR only changes the one install that has no margin at all.

Fix

Background


no test proof · iteration 0 · Platform-specific test-only change; deferring to CI.

@coderabbitai

coderabbitai Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 45d75c0e-a236-4ca9-bd51-c679cfe938bc

📥 Commits

Reviewing files that changed from the base of the PR and between eabb96d and 19ba9d0.

📒 Files selected for processing (1)
  • test/cli/install/bun-install-registry.test.ts

Walkthrough

The manifest-cache test now accounts for asynchronous replacement of invalid cache entries. After reinstalling, it accepts an absent cache file or a refreshed no-deps manifest and rejects the stale basic-1 manifest.

Changes

Manifest cache validation

Layer / File(s) Summary
Refresh-aware cache assertion
test/cli/install/bun-install-registry.test.ts
The test guards the manifest-name assertion when the cache file is absent. If the file exists, it must contain the no-deps manifest instead of basic-1.

Suggested reviewers: jarred-sumner, dylan-conway

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly describes the main test change: it no longer requires the asynchronously refetched manifest to be cached before install exits.
Description check ✅ Passed The description explains the problem, fix, expected behavior, scope, and verification results, covering the template requirements.

Comment @coderabbitai help to get the list of available commands.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — targeted test-only de-flake that stops depending on the fire-and-forget manifest cache write.

What was reviewed:

  • Confirmed exists is already imported from fs/promises (line 5).
  • Checked the conditional assertion is not vacuous: without the #37669 name check the stale entry is never deleted, so the file exists and parses as basic-1, and the earlier lockfile/node_modules assertions fail first — the property is still enforced.
  • No TOCTOU between exists() and parseManifest(): the subprocess has already exited by this point and the write is temp→rename, so the file state is stable.
Extended reasoning...

Overview

Single-file, test-only change to test/cli/install/bun-install-registry.test.ts. The final assertion in the "npm manifest cache entries are only reused for the package name they were saved for" test previously required the refetched no-deps manifest to be back on disk after the second install. That write is a fire-and-forget thread-pool task (Serializer::save_async) that bun install intentionally does not wait for (per #37203), and on Windows debug builds the install exits before it lands 5/5 times. The change wraps the parseManifest call in if (await exists(...)) and adds a comment explaining the race with an issue reference.

Security risks

None. Test-only change; no runtime code paths, no new inputs, no credential/network handling.

Level of scrutiny

Low. This is a 7-line test de-flake with a documented root cause (background write not awaited by design), a cited precedent (#37203 removed the same dependency elsewhere), and before/after verification on the failing platform. The main review concern for a change like this is whether it silently weakens the test — REVIEW.md's "when de-flaking, keep asserting the property the original assertion protected." I traced the regression case: without the #37669 name check, the corrupted entry is neither rejected nor deleted, so (a) the file still exists and the conditional branch is entered, failing on basic-1no-deps, and (b) the earlier node_modules/no-deps/package.json and lockfile-resolution assertions would already have failed. The property is fully covered.

Other factors

  • No TOCTOU risk between exists() and parseManifest(): the subprocess has exited (awaited via proc.exited), so the thread pool is dead and the file state is final; the write path is temp-then-rename so a present file is complete.
  • The added comment is longer than the repo's usual one-liner, but it documents a non-obvious timing behavior that would otherwise make the conditional look vacuous, and it links the governing issue. Acceptable.
  • No prior reviews or comments on the PR; nothing outstanding to address.

@robobun

robobun commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator Author

Reproduced with a Windows x64 debug build (bun bd test test/cli/install/bun-install-registry.test.ts -t "only reused for the package name"): the test failed on 5 of 5 runs before this change with the ENOENT from the PR description, and passes on 5 of 5 runs with it. On Linux (debug+ASAN) it passes before and after, so a before/after run of this file on Linux cannot show the failure; the Windows runs are the evidence.

The underlying timing (from a locally instrumented debug build, not part of this PR): the cache write task needs about 25 ms on that machine, while the second install, whose tarball is already cached, goes from receiving the manifest to exiting in about 12 ms. The first install's writes had finished about 45 ms before it was done resolving, which is the margin the rest of the suite relies on. Making the install wait for the write was tried and reverted in #37203, so the test is what changes here.

Current head is rebased on main after #38333; the only change since the first push is the wording of the comment in the test.

…re install exits

The manifest name-check test asserted that the no-deps manifest fetched
after the mismatched entry was rejected is back in the cache when the
second install exits. That write is a thread pool task the install does
not wait for, and with the tarball already cached the install exits
before it lands on Windows debug builds, failing the test every time.
Assert that the stale entry is gone and, when the write did land, that
it holds the right manifest.
@robobun
robobun force-pushed the farm/c35797dc/manifest-name-check-test branch from 19ba9d0 to 6ca5eca Compare August 14, 2026 19:42

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — test-only de-flake that stops depending on the fire-and-forget manifest cache write having landed before install exits.

What was reviewed:

  • Confirmed exists is already imported from fs/promises in this file, so the new call resolves.
  • Verified the test still fails without the #37669 name check: the stale entry would not be deleted, so the file would still exist and parse as basic-1; the lockfile and node_modules/no-deps/package.json assertions above also independently catch that regression.
  • Checked for a TOCTOU between exists() and parseManifest(): the second install has already exited so the delete-on-load is done, and the async write is temp-file-then-rename, so an existing file is always complete.
Extended reasoning...

Overview

This PR touches a single assertion at the end of the "npm manifest cache entries are only reused for the package name they were saved for" test in test/cli/install/bun-install-registry.test.ts. The final parseManifest(byName["no-deps"], ...) call is now guarded by if (await exists(...)), with a comment explaining that Serializer::save_async is a thread-pool task bun install intentionally does not wait for (per #37203), so on a Windows debug build the second install exits before the refetched manifest is written back to disk.

Security risks

None. Test-only change; no production code, no new inputs, no auth/crypto/permissions surface.

Level of scrutiny

Low. This is a targeted CI de-flake for a known 5/5 failure on Windows debug builds. The key REVIEW.md concern for de-flaking — "keep asserting the property the original assertion protected" — is satisfied: without the #37669 name-check fix, the basic-1 entry would neither be rejected nor deleted, so the file would still exist and the guarded expect(...).toBe("no-deps") would fire and fail. The earlier lockfile-resolution and node_modules/no-deps/package.json assertions also independently catch that regression, so the conditional does not make the test vacuous.

Other factors

  • exists is already imported at the top of the file (line 5, from fs/promises), so no missing-import issue.
  • No TOCTOU: by the time we check, proc.exited has resolved, so the synchronous delete-on-load in Serializer::load_by_file_id has already happened; the only in-flight operation is the async write, which is temp-file + rename and therefore atomic.
  • The added comment is exactly the kind REVIEW.md asks for: it names why no observable signal exists (write is fire-and-forget by design) and links the PR that decided so.
  • Same pattern was already accepted in #37203 for the security-scanner matrix.
  • The bug hunting system found no issues.

@robobun

robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author

Data point for this test from another branch: in https://buildkite.com/bun/bun/builds/97654 (alpine 3.23 x64, release build, passed on retry) the first half of "npm manifest cache entries are only reused for the package name they were saved for" failed as well: expect(Object.keys(byName).sort()).toEqual(["basic-1", "no-deps"]) received [], so both writes of the first install were lost under load even with the tarball downloads after them. The branch it ran on (#38916) only changes the rewrite of an existing entry, which this install does not do.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant