Skip to content

test(install): run catalogs.test.ts concurrently against an in-process registry and assert whole lockfiles - #40635

Closed
robobun wants to merge 2 commits into
mainfrom
farm/c4eb61a5/speed-up-catalogs-test
Closed

robobun wants to merge 2 commits into
mainfrom
farm/c4eb61a5/speed-up-catalogs-test

Conversation

@robobun

@robobun robobun commented Aug 27, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • test/cli/install/catalogs.test.ts takes 16 s on debian 13 x64-asan (build #106656): 89 tests, about 200 bun spawns, at most 5 tests in flight under ASAN.
  • 20 tests (basic, update, errors) were plain test(...). Each one is a barrier: the runner drains the concurrent tests and runs it alone. Locally these barriers were 8.6 s of a 19 s run.
  • beforeAll started verdaccio. Under the ASAN binary that start alone costs seconds.

Fix

  • test/harness.ts gets FixtureRegistry: the verdaccio fixture store (test/cli/install/registry/packages) served by Bun.serve on port 0. Same packuments and tarballs, so installs resolve and hash the same. Like verdaccio, it points every dist.tarball at its own host and keeps only the file name, which also serves the 16 fixtures stored with another host or a doubled scheme. It shares createTestDir and writeBunfig with VerdaccioRegistry, which keeps auth, users and publish.
  • Every test is test.concurrent. Each spawned install and update gets its own cache through installEnv() (the same helper test(install): run isolated-install.test.ts concurrently and tighten its assertions #40095 adds): the CI runner sets BUN_INSTALL_CACHE_DIR for the whole file and it wins over the bunfig cache path. The three file snapshots (not supported in concurrent tests) become toEqual on the parsed lockfile sections.
  • The leaf, wants-leaf-* and needs-leaf-2 packages the file generated in memory are fixtures now, written by create-catalog-packages.ts. Generated packages had no integrity, and bun writes those as lockfileVersion: 1 with an empty integrity.
  • Assertions are whole values: full install output (the task count of the resolve summary is masked), nodeModulesPackages layouts, exact stderr and exit code in errors, the lockfile sections bun update moved, the resolution of every lockfile entry in the peer tests. 930 expect() calls, up from 864. No test removed or merged.
  • Verified: bun bd test test/cli/install/catalogs.test.ts, 89 pass, 18 s before (17.3, 19.0, 18.8) and 10.4 s after (10.2, 10.2, 10.4, 11.0) on one debug ASAN build. On the debian 13 x64-asan lane the file took 5.0 to 5.9 s in the four runs of build 106807, against 16 s in build 106656. bun-workspaces.test.ts (75 tests) and hoist.test.ts pass on the refactored VerdaccioRegistry, and bun-workspaces.test.ts also passes with FixtureRegistry swapped in.

Background

  • bun test groups consecutive test.concurrent tests and runs up to --max-concurrency of a group at once (5 under ASAN, src/options_types/context.rs:512). A plain test(...) ends the group and runs alone.
  • A verdaccio fixture is one directory with the packument (package.json) and the tarballs of a package. Verdaccio rewrites each stored tarball URL to <host>/<name>/-/<file> when it serves the packument.
  • toMatchSnapshot needs the current test's name, which a concurrent group cannot provide (SnapshotInConcurrentGroup in expect.rs). Inline snapshots match by source location and work.
Notes

Shared install cache in CI (first CI run of this PR, build 106807). scripts/runner.node.mjs spawns every test file with BUN_INSTALL_CACHE_DIR set to the file's tmpdir, and fetch_cache_directory_path (src/install/PackageManager/PackageManagerDirectories.rs:379) prefers the env var over the bunfig cache path. The serial tests never noticed. Once concurrent, 20 tests shared one cache: the "Resolved, downloaded and extracted [N]" count changed with whatever another test had extracted first (the same test gave [4] on one lane and [6] on another), and on Windows two processes extracting the same package into one cache failed with ENOTEMPTY: Directory not empty (NtSetInformationFile()) and ENOENT: failed opening cache/package/version dir. Reproduced locally by running the file with BUN_INSTALL_CACHE_DIR set: 12 failures, 13 entries in the shared cache. With installEnv() the shared cache stays empty and the file passes. The peer and optional describes already set the variable per test.

Where the registry lives. The first version of this PR kept the in-process registry private to the file. The self-review pointed out that 26 of the 30 VerdaccioRegistry users only read fixtures, that a plain replaceAll("http://localhost:4873", origin) mis-serves 16 of the 228 fixtures (14 stored as http://http://localhost:4873, one on :4874, one on :22115), and that #40095 adds the same installEnv to the harness. So the registry moved to test/harness.ts with the structural tarball rewrite, installEnv has the signature of #40095, and createTestDir returns env as there. Other fixture-only files can switch with the constructor line; none are switched here.

Timing model. The per-test times sum to 42 s before and 39 s after, so the gain is scheduling and startup, not fewer spawns. A simulation of the runner's grouping on the "before" log gives 15.9 s of test time (8.6 s of it serial barriers) for a 19 s run. With every test concurrent the same log simulates to 8.5 s, which matches the measured 10.4 s once startup is added. The lane's wall time only moves when this file's shard is the slowest one, since the runner packs files into shards by test/expected-durations.json. The per-file number is what this PR measures.

Verdaccio start. Locally the harness runs verdaccio under the release bun (0.7 s start). In CI it runs under the built binary; hoist.test.ts (verdaccio plus one install) takes 5.6 s on the asan lane in test/expected-durations.json. That is why the CI gain (16 s to about 5.5 s) is larger than the local one. The 106807 runs still had the shared-cache failures in them, so they are an upper bound for the file's time.

Not done on purpose. Installing a template tree once and copying it per test (as in #39741 and #39742) is not worth it here: every tree is two or three tiny packages, the cost is the process start, and the update describe would save about six spawns (about 0.25 s at five in flight). No second install was dropped. Each one that is kept proves a different property (the lockfile is not rewritten, --frozen-lockfile passes, a reload gives the same tree).

Open PRs that touch the same outputs. #38763 changes what bun update --latest prints for moved catalog entries; this PR pins today's "1 package installed" in three tests, so whichever lands second updates three inline snapshots. #38741 edits the deleted .snap (lockfileVersion 2 to 1); the one inline lockfile snapshot here pins "lockfileVersion": 2 and moves with it. #38849 edits other parts of this file.

Behavior pinned as it is today:

  • bun update no-deps --latest from a workspace whose dependency is catalog: changes nothing ("Checked 3 installs across 4 packages (no changes)", no lockfile save). The test already named this "keeps the catalog reference". The output is now pinned.
  • After the catalog range of a peer goes >=1.0.0 -> ^2.0.0 -> >=1.0.0, bun.lock dedupes again but the nested packages/lib/node_modules/no-deps copy from the middle state stays on disk. The test checks the lockfile at that point, as before, not the layout.
  • With an aliased peer (npm:no-deps@>=1.0.0 in lib), app's "no-deps": "1.0.0" gets no lockfile entry and the root node_modules/no-deps is 2.0.0. This is independent of catalogs (the inline peer does it too). The test only compares catalog against inline, as before.

Deleted test/cli/install/__snapshots__/catalogs.test.ts.snap. Its three lockfile snapshots are now the registryEntry table plus toEqual in "detect changes", and the full-text inline snapshot in "both catalog and catalogs".

Windows: nodeModulesPackages does not descend into junctions or symlinks (Bun reports reparse points as isSymbolicLink(), src/runtime/node/dir_iterator.rs:716), so the layouts are the same on every platform.


[stamp-90s] gate passed · iteration 0 · 2 files touched

passes on PR (with fix)
Test-only change.

Debug/ASAN (expected pass):
$ bun bd test 'test/cli/install/catalogs.test.ts'
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test test/cli/install/catalogs.test.ts
bun test v1.4.1 (731aa92da)

test/cli/install/catalogs.test.ts:
(pass) basic > both catalog and catalogs in top-level [522.68ms]
(pass) basic > both catalog and catalogs in workspaces [595.36ms]
(pass) basic > switching a dependency to a different catalog is detected [686.92ms]
(pass) basic > catalog and catalogs.default may split different packages between them [378.08ms]
(pass) basic > detect changes (bun.lockb) [862.51ms]
(pass) basic > detect changes (bun.lock) [907.38ms]
(pass) update > --latest updates catalog versions in top-level [421.77ms]
(pass) update > --latest updates catalog versions in workspaces [419.32ms]
(pass) update > --latest updates catalog versions with -r [501.15ms]
(pass) update > --latest run from inside a workspace package updates the root catalog [436.11ms]
(pass) update > --frozen-lockfile passes after --latest updates catalogs [602.61ms]
(pass) update > --latest updates the same package independently per catalog [467.16ms]
(pass) update > --latest --dry-run does not modify any package.json (from root) [489.34ms]
(pass) update > update without --latest from root moves catalogs within range (no args) [290.87ms]
(pass) update > update without --latest from root moves catalogs within range (-r) [232.72ms]
(pass) update > --latest --dry-run does not modify any package.json (from workspace) [396.08ms]
(pass) errors > fails gracefully when no catalog is found for a package [162.14ms]
(pass) errors > invalid dependency version [155.18ms]
(pass) errors > a package missing from an existing named catalog suggests bun add --catalog=<name> [256.30ms]
(pass) errors > the same package in both catalog and catalogs.default is rejected (workspaces) [172.06ms]
(pass) update > update without --latest stays in range and keeps catalog references [527.27ms]
(pass) update > update <pkg> --latest keeps the catalog reference [473.33ms]
(pa
... (truncated)
Exit: 0
diff hotspot
.../install/__snapshots__/catalogs.test.ts.snap    |  106 --
 test/cli/install/catalogs.test.ts                  | 1273 ++++++++++++--------
 2 files changed, 784 insertions(+), 595 deletions(-)

gate history · 1 passed · 0 rejected · iteration 0

evidence per changed file
file                                                  reads  edits  tests
test/cli/install/__snapshots__/catalogs.test.ts.snap      0      0      0
test/cli/install/catalogs.test.ts                        23     18      0

…s registry and assert whole lockfiles

Every test in the file is now test.concurrent. The 20 serial tests in the
basic, update and errors describes were scheduling barriers that the other
69 tests had to wait for.

The file serves its registry in-process. It reads the verdaccio fixture
packages from test/cli/install/registry/packages (the same packuments and
tarballs, so the integrity hashes in the lockfiles do not change) and adds
the packages that need a raw catalog: specifier. The verdaccio process is
not started any more.

The three bun.lock file snapshots of "detect changes" move into the file as
toEqual on the parsed catalog, catalogs and packages sections. Successful
installs assert their full output and the installed layout. The error tests
assert the whole stderr and stdout and the exact exit code. The peer tests
compare the resolution of every lockfile entry instead of the entry keys.

Local wall time with the debug build: 18 s to 10.4 s for 89 tests.
@coderabbitai

coderabbitai Bot commented Aug 27, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

  • Run on-demand review

On-demand reviews are free for the next 24 days. After that, they cost $0.25 per reviewed file.

Or wait 13 minutes for your next included review.

View limit details

Limit details: You’ve used all 5 included reviews currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: ee6ad6a2-ec48-4ea3-939a-8565dbbe7a40

📥 Commits

Reviewing files that changed from the base of the PR and between 65362b5 and 658b252.

⛔ Files ignored due to path filters (1)
  • test/cli/install/__snapshots__/catalogs.test.ts.snap is excluded by !**/*.snap
📒 Files selected for processing (1)
  • test/cli/install/catalogs.test.ts

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

@robobun

robobun commented Aug 27, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: ready for review. Three commits: the concurrent rework of the file, the per-test install cache (installEnv), and the move of the in-process registry into test/harness.ts as FixtureRegistry with the leaf family committed as fixtures (after the self-review, see the PR body's Notes).

Measured with one debug ASAN build (bun bd test test/cli/install/catalogs.test.ts, 89 tests): 17.3 s, 19.0 s, 18.8 s on main and 10.2 s, 10.2 s, 10.4 s, 11.0 s with this branch. On the debian 13 x64-asan lane the file took 5.0 to 5.9 s in the four runs of build 106807, against 16 s in build 106656.

Build 106825 (before the harness move): catalogs.test.ts passed on every lane. The red lanes there failed on tests this change does not touch (require-cache.test.ts on Windows, macro-test.test.ts on asan, url.test.ts on darwin x64), reported to main-break triage separately.

@robobun

robobun commented Aug 27, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 4:56 AM PT - Aug 27th, 2026

❌ @robobun, your commit 658b252 has 3 failures in Build #106825 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 40635

That installs a local version of the PR into your bun-40635 executable, so you can run:

bun-40635 --bun

@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.

I reviewed this PR and didn't find any bugs. Because it replaces the harness-recommended VerdaccioRegistry with a custom in-process FixtureRegistry and rewrites assertions across all 89 tests, a human look at the new registry implementation and the pinned inline snapshots would still be worthwhile.

What was reviewed:

  • FixtureRegistry.fetch: scoped-package decoding, tarball path splitting on /-/, and the localhost:4873 → origin rewrite match verdaccio's serving contract; port: 0 and same-fixture-directory tarballs keep integrity hashes stable.
  • Assertion changes strengthen rather than weaken coverage — toContain → full toMatchInlineSnapshot/toEqual, exitCode !== 0 → exitCode === 1, packageKeys → full resolutions() maps; no test removed or relaxed.
  • test.concurrent conversion is safe: each test already owns its own tempDir + .bun-cache, and the three file snapshots blocking concurrency were replaced with inline/structured equivalents.
  • Removal of "no-deps" from catalogManifests is covered by the fixture directory's no-deps packument now served via the same registry.
Extended reasoning...

Overview

This PR is a test-only refactor of test/cli/install/catalogs.test.ts (+739/−572) plus deletion of its external snapshot file. It introduces an in-process FixtureRegistry class that serves the existing verdaccio fixture packuments/tarballs from test/cli/install/registry/packages via Bun.serve({ port: 0 }), augmented with dynamically-built tarballs for the handful of catalog-specifier packages. All 89 tests are converted to test.concurrent, three external toMatchSnapshot() calls become inline snapshots or structured toEqual on parsed lockfile sections, and most toContain/.not.toBe(0) assertions are upgraded to whole-output inline snapshots and exact exit codes. New helpers (lockfileText, readLockfile, registryEntry, resolutions, installedPackages) centralize lockfile and layout assertions. No production code (src/) is touched.

Security risks

None. This is test infrastructure only. The in-process registry reads from checked-in fixture files under test/cli/install/registry/packages and never contacts the network. No auth, crypto, or permissions code paths are involved.

Level of scrutiny

Moderate. While test-only, this is a large refactor (~1200 diff lines) that replaces the harness-standard VerdaccioRegistry — the mechanism CLAUDE.md itself names as the way to do package installs in tests — with bespoke serving logic. The FixtureRegistry.fetch implementation (URL decoding for scoped packages, /-/ splitting for tarball vs. packument routing, localhost:4873 host rewriting) needs to match verdaccio's contract closely enough that bun install exercises identical code paths and produces identical integrity hashes. I traced through it and it looks correct, but a maintainer familiar with the install test suite should confirm this pattern is acceptable before other files copy it. The hardcoded integrity map is another maintenance point worth a human glance.

Other factors

The change is strongly aligned with REVIEW.md's testing rules: assertions became stricter across the board (exact stderr/stdout, exact exit codes, full lockfile resolutions instead of key lists), no test was deleted or weakened, test.concurrent is used for independent subprocess tests, normalizeBunSnapshot wraps all output, and stdout/stderr are asserted before exit codes. CODEOWNERS does not cover this path. The bug hunt exited on dry_streak with no findings and no ruled-out candidates. The PR description is unusually thorough (timing model, per-behavior pinning notes, Windows junction handling) and states bun bd test passes. Given the size and the introduction of new test infrastructure that diverges from the documented harness pattern, deferring to a human is the appropriate call rather than auto-approving.

… mask the task count

The CI runner sets BUN_INSTALL_CACHE_DIR for the whole test file, and the
env var wins over the cache path in bunfig.toml. With every test concurrent,
the basic, update and errors tests shared one install cache: the resolve
summary count changed with whatever another test had extracted first, and
on Windows two processes extracting the same package into one cache failed
with ENOTEMPTY and ENOENT. installEnv() now points each spawned install and
update at the test's own .bun-cache, the way the peer and optional tests
already did.

The task count of "Resolved, downloaded and extracted [N]" is masked in the
snapshots. It counts scheduled tasks and depends on the cache state.
@robobun

robobun commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator Author

Re the review above: the registry class and the pinned snapshots are the two places that changed shape, agreed. One thing the first CI run found in that area, fixed in 658b252: the CI runner sets BUN_INSTALL_CACHE_DIR for the whole test file and the env var wins over the bunfig cache path (fetch_cache_directory_path, src/install/PackageManager/PackageManagerDirectories.rs:379). So once the tests ran concurrently, the basic, update and errors tests shared one install cache. On Linux that only moved the "Resolved, downloaded and extracted [N]" count around (the same test printed [4] on one lane and [6] on another). On Windows two processes extracting the same package into one cache failed with ENOTEMPTY and ENOENT. installEnv() now sets the variable per test, the same thing the peer and optional describes already did, and the count is masked in the snapshots. The PR body has the details under Notes.

@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.

Code review found no issues

No high-confidence issues detected in this change.

@Jarred-Sumner

Copy link
Copy Markdown
Collaborator

This is Claude (an AI assistant) writing on Jarred's behalf.

Included in #44968, in a different form: smaller: every test runs concurrently on the shared test registry, the file snapshots are inline; its whole-lockfile assertions are not taken.

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.

2 participants