Skip to content

test(install): pin both no-deps versions instead of racing a range against a pin - #38858

Open
robobun wants to merge 2 commits into
mainfrom
farm/0d4be2c0/isolated-install-peer-tests-pin-no-deps
Open

test(install): pin both no-deps versions instead of racing a range against a pin#38858
robobun wants to merge 2 commits into
mainfrom
farm/0d4be2c0/isolated-install-peer-tests-pin-no-deps

Conversation

@robobun

@robobun robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator

Problem

  • test/cli/install/isolated-install.test.ts > "ranged peer dependency resolution is stable across installs from bun.lock" fails intermittently on main (2 of 20 runs of the unmodified test with a debug build here; Deterministic dependency resolution: an ordered walk over a live tree #36476 also lists it as failing on the released binary):
    expect(received).toContain(expected)
    Expected to contain: "peer-deps-fixed@1.0.0+7ff199101204a65d"
    Received: [ "@types+is-number@2.0.0", "no-deps@1.0.1", "node_modules", "normal-dep-and-dev-dep@1.0.0",
      "peer-deps-fixed@1.0.0+f8a822eca018d0a1", "two-range-deps@1.0.0" ]
    
  • The test needs two versions of no-deps in the graph and gets them from normal-dep-and-dev-dep (depends on no-deps@1.0.1) plus two-range-deps (depends on no-deps@^1.0.0). Whether the range adds no-deps@1.1.0 or dedupes onto the 1.0.1 pin depends on which of the two manifests the registry answers first: Lockfile::get_package_id (src/install/lockfile.rs:2061) lets a range settle on an already appended exact pin of the same major. When 1.0.1 is appended first there is one no-deps, the peer binds to it, and the store entry is keyed +f8a822eca018d0a1.
  • Forcing each order with a proxy that holds one manifest back (release 1.4.0) reproduces both outcomes every time: normal-dep-and-dev-dep first gives { "no-deps": "no-deps@1.0.1" } and the +f8a8... entry (the failure above); two-range-deps first gives an extra two-range-deps/no-deps: no-deps@1.1.0 and the +7ff1... entry (the pass).
  • The two sibling tests added with it in install: fix stale global-store links on disable and peer edge drift across bun.lock loads #32182 use the same construction. "optional ranged peer keeps its hoisted-tree binding" passes in both orders, but in the first order the graph has a single no-deps, so the case it describes (a higher version present that a version scan would pick) is not exercised. "aliased peer dependency binds to its real package" never has its 1.1.0 decoy in either order: the workspace declares "no-deps": "npm:a-dep@^1.0.2", so two-range-deps' plain no-deps@^1.0.0 is redirected to that alias (known_npm_aliases, src/install/PackageManager/PackageManagerEnqueue.rs:677), and the only real no-deps in its graph is 1.0.1, which does not satisfy ^1.0.2.
  • The same pin-plus-range pair is used once more, in test/cli/install/bun-install-registry.test.ts > "update should update all packages in the current workspace": one-fixed-dep@1.0.0 (depends on no-deps@1.0.0) next to two-range-deps, with the result masked as /(?:20|19) packages installed/ under a comment blaming optional-native. With a debug build here the count was 19 in 7 of 10 runs (bun.lock has no-deps@1.0.0 only) and 20 in 3 (no-deps@1.1.0 as well); the optional-native entries were identical in every run, so the alternation is this race, and the comment is wrong.

Fix

  • In the three isolated-install tests, put the second copy into the graph as a root "nd11": "npm:no-deps@1.1.0" dependency instead of through two-range-deps. This is how the fourth test of the group ("overridden peer dependency keeps the override ...") already builds the same graph.
  • Why this is the right graph: two exact pins of different versions are both appended whatever order the manifests arrive in (an exact version never satisfies a different one, so there is nothing to dedupe onto), and an npm: dependency is not subject to alias redirection. The tree the tests are about keeps its shape: 1.0.1 is still hoisted to the root no-deps path, 1.1.0 sits at nd11, and the peer edge still gets no path of its own, so the fresh resolve binds it to 1.1.0 by version scan (the same +7ff199101204a65d entry as before) while re-deriving it from the saved paths would still give 1.0.1. In the aliased test the decoy a wrong-name lookup would find (no-deps@1.1.0 satisfies ^1.0.2) is now actually present.
  • Each test now asserts that shape from bun.lock (savedTreeCopiesOf) before looking at the store, so a resolution change shows up as a premise failure rather than as a different peer hash.
  • The resolver's own order dependence is being fixed separately in install: resolve a range onto an existing version only when every install has it #38832. These tests are about what the bun.lock loader does with a given tree, so they should not encode either dedupe policy: with install: resolve a range onto an existing version only when every install has it #38832 the range would always add its own copy, with Deterministic dependency resolution: an ordered walk over a live tree #36476's ordered walk it would always dedupe. Pinning both copies keeps them correct under both.
  • In the bun update test, one-fixed-dep becomes ^2.0.0: its no-deps@2.0.0 pin is outside the range's major, so every install has the same two copies of no-deps and the assertion is a plain 20 packages installed. one-fixed-dep has no other role there (it is one of thirteen dependencies bun update leaves at their range), so the five copies of the list and its + one-fixed-dep@2.0.0 line move with it.
  • Verified:
    • bun bd test test/cli/install/isolated-install.test.ts: 66/66 pass; the five ... across installs from bun.lock tests pass in 10 of 10 repeated debug-build runs.
    • The unmodified test fails 2 of 20 debug-build runs here, both with the +f8a822eca018d0a1 listing above.
    • bun bd test test/cli/install/bun-install-registry.test.ts: the update test passes 8 of 8 repeated debug-build runs (the unmodified one printed 19 in 7 of 10); the full file passes apart from one run of the unrelated manifest conditional requests etag test, which passes alone and in its own block and which install: replace an existing manifest cache entry atomically instead of unlinking it first #38916 already references.
    • Forced-order matrix below: the new graphs give the same bun.lock copies and the same store entries in both arrival orders; the old ones do not.

Background

  • Isolated store entries are named <name>@<version>, plus +<hash> of the package's peer bindings when it has peers, so the entry name says which no-deps a peer was bound to: +7ff199101204a65d is no-deps@1.1.0, +f8a822eca018d0a1 is no-deps@1.0.1.
  • bun.lock's packages keys are node_modules paths of the hoisted tree (also with the isolated linker). install: fix stale global-store links on disable and peer edge drift across bun.lock loads #32182 fixed the loader re-deriving peer edges from those paths; a peer edge satisfied by the version at an enclosing path gets no path of its own, which is why that walk lands on the hoisted version.
  • "nd11": "npm:no-deps@1.1.0" installs no-deps@1.1.0 under the name nd11; in the lockfile it is a no-deps package, so the peer's scan over no-deps versions sees it. Separately, a plain dependency whose name matches an alias declared elsewhere in the tree, and whose range accepts the alias's version, is resolved to the alias target instead; that is what two-range-deps' range hit in the aliased test.
Forced arrival orders (release 1.4.0; proxy holds the named manifest until the other parent's no-deps request was served). Columns: copies of no-deps in bun.lock, store entry of the package with the peer; the reinstall from bun.lock produced the same entry in every row.
main: ranged peer     | normal-dep-and-dev-dep resolved first | {"no-deps":"no-deps@1.0.1"}                                             | peer-deps-fixed@1.0.0+f8a822eca018d0a1   <- the flake
main: ranged peer     | two-range-deps resolved first         | {"no-deps":"no-deps@1.0.1","two-range-deps/no-deps":"no-deps@1.1.0"}    | peer-deps-fixed@1.0.0+7ff199101204a65d
main: aliased peer    | normal-dep-and-dev-dep resolved first | {"normal-dep-and-dev-dep/no-deps":"no-deps@1.0.1"}                      | (workspace peer; no decoy in either order)
main: aliased peer    | two-range-deps resolved first         | {"normal-dep-and-dev-dep/no-deps":"no-deps@1.0.1"}                      |
main: optional peer   | normal-dep-and-dev-dep resolved first | {"no-deps":"no-deps@1.0.1"}                                             | one-optional-peer-dep@1.0.2+f8a822eca018d0a1   <- single version, nothing to keep
main: optional peer   | two-range-deps resolved first         | {"no-deps":"no-deps@1.0.1","two-range-deps/no-deps":"no-deps@1.1.0"}    | one-optional-peer-dep@1.0.2+f8a822eca018d0a1

this PR: ranged peer   | no-deps manifest last                 | {"nd11":"no-deps@1.1.0","no-deps":"no-deps@1.0.1"}                      | peer-deps-fixed@1.0.0+7ff199101204a65d
this PR: ranged peer   | normal-dep-and-dev-dep manifest last  | {"nd11":"no-deps@1.1.0","no-deps":"no-deps@1.0.1"}                      | peer-deps-fixed@1.0.0+7ff199101204a65d
this PR: aliased peer  | no-deps manifest last                 | {"nd11":"no-deps@1.1.0","normal-dep-and-dev-dep/no-deps":"no-deps@1.0.1"} |
this PR: aliased peer  | normal-dep-and-dev-dep manifest last  | {"nd11":"no-deps@1.1.0","normal-dep-and-dev-dep/no-deps":"no-deps@1.0.1"} |
this PR: optional peer | no-deps manifest last                 | {"nd11":"no-deps@1.1.0","no-deps":"no-deps@1.0.1"}                      | one-optional-peer-dep@1.0.2+f8a822eca018d0a1
this PR: optional peer | normal-dep-and-dev-dep manifest last  | {"nd11":"no-deps@1.1.0","no-deps":"no-deps@1.0.1"}                      | one-optional-peer-dep@1.0.2+f8a822eca018d0a1

…k tests

The three peer tests added with the bun.lock peer-binding fix built their
two-version no-deps graph from normal-dep-and-dev-dep (no-deps@1.0.1) plus
two-range-deps (no-deps@^1.0.0). Whether the range adds no-deps@1.1.0 or
dedupes onto the 1.0.1 pin depends on which manifest the registry answers
first, so "ranged peer dependency resolution is stable across installs from
bun.lock" failed whenever 1.0.1 was appended first (the store entry is then
keyed +f8a822eca018d0a1 instead of +7ff199101204a65d), the optional peer
test sometimes ran with a single no-deps version, and the aliased peer test
never had its 1.1.0 decoy at all because the plain range was redirected to
the workspace's no-deps -> a-dep alias.

Add the second copy as a root "nd11": "npm:no-deps@1.1.0" alias instead,
the way the overridden peer test in the same group already does. Two exact
pins are both appended whatever the arrival order, and the saved tree keeps
the shape the tests are about: 1.0.1 at the root no-deps path, 1.1.0 under
nd11, no path of its own for the peer edge. Each test now asserts that
shape from bun.lock before looking at the store.
@coderabbitai

coderabbitai Bot commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 1 minute

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 21e43490-32ac-485f-9420-3010659e7077

📥 Commits

Reviewing files that changed from the base of the PR and between 39fb3c1 and 0538111.

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

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

@robobun

robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 9:42 AM PT - Aug 15th, 2026

@robobun, your commit 053811131734e8aa89acdab44ff21c2978f0992e passed in Build #97895! 🎉


🧪   To try this PR locally:

bunx bun-pr 38858

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

bun-38858 --bun

@robobun

robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author

Status: ready for review.

Reproduced on main two ways: the unmodified "ranged peer dependency resolution is stable across installs from bun.lock" test fails 2 of 20 debug-build runs here with the +f8a822eca018d0a1 listing in the PR description, and a proxy that holds two-range-deps' manifest back until no-deps has been requested makes it fail every time (holding normal-dep-and-dev-dep's back instead makes it pass every time). With this change the three isolated-install tests produce the same bun.lock copies and store entries in both forced orders, and the full isolated-install.test.ts passes with a debug build (66/66; the five peer tests 10/10 repeated runs).

Self-review turned up one more test built on the same pin-plus-range pair, bun-install-registry.test.ts > "update should update all packages in the current workspace", which masked the race as /(?:20|19) packages installed/ (19 in 7 of 10 debug runs here, 20 otherwise). 0538111 moves its one-fixed-dep to ^2.0.0 so the count is always 20; that test passes 8/8 repeated debug-build runs and the file passes.

The resolver side of this (a range settling on a sibling's pin depending on arrival order) is #38832; this PR only makes the tests independent of it. The previous build's only hard failures were two darwin 14 aarch64 jobs that expired without getting an agent.

@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 pins both no-deps versions so the graph shape no longer depends on registry response order.

What was reviewed:

  • The three modified tests still assert the same peer-binding invariant (same +7ff1... / +f8a8... store hashes, same resolved versions on fresh vs. bun.lock reinstall) — the property is preserved, not weakened.
  • The nd11: npm:no-deps@1.1.0 construction matches the existing fourth sibling test at line ~1357; no-deps@1.1.0 is present in the Verdaccio fixtures.
  • savedTreeCopiesOf uses Bun.JSONC.parse (correct for bun.lock) and existing file/join imports; the startsWith(${name}@) filter can't over-match here because the graphs contain no other no-deps* package and the toEqual on the full map would fail loudly if it did.
  • Checked that the aliased-peer test's decoy (1.1.0 satisfying ^1.0.2) is now actually present, and the optional-peer test now always has both versions — both are strengthened, not just stabilized.
Extended reasoning...

Overview

Test-only change to test/cli/install/isolated-install.test.ts fixing a flaky test (and two siblings that share the same construction). The three "... across installs from bun.lock" tests previously pulled in the second no-deps version through two-range-deps' no-deps@^1.0.0 range, which the resolver may or may not dedupe onto the exact 1.0.1 pin depending on which manifest the local registry answers first. The fix replaces that range with a direct "nd11": "npm:no-deps@1.1.0" root dependency — two exact pins that cannot dedupe onto each other regardless of arrival order — and adds a small savedTreeCopiesOf helper so each test asserts the bun.lock tree shape it depends on before checking store entries.

Security risks

None. No production code is touched; only test package.json fixtures and assertions change. The tests continue to run against the local Verdaccio registry.

Level of scrutiny

Low-to-moderate. The main REVIEW.md concern for de-flakes is "keep asserting the property the original assertion protected" — verified: the tests still check the exact same store-entry hashes (peer-deps-fixed@1.0.0+7ff199101204a65d, one-optional-peer-dep@...+f8a822...) and the same resolved no-deps versions after reinstalling from bun.lock. The change swaps how the second version enters the graph, not what is asserted about the peer edge. The new premise assertions make the tests strictly stronger.

Other factors

  • The nd11: npm:no-deps@1.1.0 pattern is not novel — the fourth test in the same group ("overridden peer dependency keeps the override ...") already uses it, so this brings three siblings into line with the fourth.
  • The PR description includes a forced-arrival-order matrix demonstrating the old graphs are order-dependent and the new ones are not, plus 10/10 repeated debug-build passes vs. 2/20 failures on main.
  • The description also notes the underlying resolver order-dependence is being fixed separately (#38832) and explains why these tests should be independent of either dedupe policy — the reasoning is sound.
  • The aliased-peer test is actually fixed (its 1.1.0 decoy was never present under the old construction because two-range-deps' plain no-deps dep was captured by the workspace's npm:a-dep alias), and the optional-peer test now always exercises the two-version case it describes.
  • Bun.JSONC.parse and the packages: Record<string, [resolution, ...]> bun.lock shape are both established; file and join are already imported at the top of the file.

"update should update all packages in the current workspace" installs
one-fixed-dep@1.0.0 (pins no-deps@1.0.0) next to two-range-deps
(no-deps@^1.0.0), the same pin-plus-range pair as the isolated peer tests,
and masked the resulting 19-or-20 package count with an alternation whose
comment blamed optional-native. The optional-native entries are identical in
every run; the extra package is no-deps@1.1.0, present only when the range
was resolved before the pin was appended.

Use one-fixed-dep@^2.0.0 instead: its no-deps@2.0.0 pin is outside the
range's major, so every install has the same two copies of no-deps and the
test can assert "20 packages installed" outright.
@robobun robobun changed the title test(install): pin both no-deps versions in the isolated peer bun.lock tests test(install): pin both no-deps versions instead of racing a range against a pin Aug 15, 2026

@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 — well-researched test-only flake fix that pins both no-deps versions so the peer-binding tests no longer depend on manifest arrival order.

What was reviewed:

  • Confirmed one-fixed-dep@2.0.0 (pins no-deps@2.0.0) and no-deps@1.1.0 exist in the local Verdaccio fixtures, so the new graphs resolve.
  • savedTreeCopiesOf correctly parses bun.lock via Bun.JSONC.parse and filters by name@ prefix; the new premise assertions match the described tree shape.
  • The nd11: npm:no-deps@1.1.0 construction mirrors the existing "overridden peer" sibling test; two exact pins can't dedupe onto each other regardless of arrival order.
  • The 19|20 → 20 tightening in the registry test: the prior comment blamed optional-native, but platform-optional counts are deterministic per machine — the dedupe race explains the run-to-run variance, and no-deps@2.0.0 sits outside ^1.0.0 so the range always adds its own copy.
Extended reasoning...

Overview

Test-only change across two files. In isolated-install.test.ts, three peer-resolution stability tests replace two-range-deps (which pulls no-deps@^1.0.0) with a root nd11: npm:no-deps@1.1.0 alias so both no-deps copies are exact pins, and add a savedTreeCopiesOf helper that asserts the lockfile tree shape before checking store entry hashes. In bun-install-registry.test.ts, the workspace-update test bumps one-fixed-dep from ^1.0.0 to ^2.0.0 (moving its transitive no-deps pin from 1.0.0 to 2.0.0, outside two-range-deps' ^1.0.0 range) and tightens the package count from 19|20 to 20.

Security risks

None. No runtime code is touched; only test fixtures and assertions against a local Verdaccio registry.

Level of scrutiny

Low-to-medium. This is a de-flake of existing tests with an unusually thorough root-cause writeup: the PR reproduced both outcomes deterministically by forcing manifest arrival order with a proxy, and verified the new graphs are order-independent in the same harness. The construction copies the pattern already used by the fourth sibling test ("overridden peer dependency keeps the override"). I verified the referenced fixture versions exist in test/cli/install/registry/packages/.

Other factors

  • The new savedTreeCopiesOf premise assertions are a strict improvement per REVIEW.md ("assert that setup created the precondition") — a future resolver change that reshapes the tree now fails on the premise, not on an opaque peer-hash mismatch.
  • The 19|20 → 20 tightening replaces a comment that attributed variance to optional-native. Platform-gated optionals are deterministic per host, so they can't explain intermittent same-machine failures; the dedupe race does. With one-fixed-dep@2.0.0 the only same-major sibling for two-range-deps' range is gone, so the count is stable.
  • The aliased-peer test now actually contains the no-deps@1.1.0 decoy it was written to guard against — previously two-range-deps' range was redirected to the workspace's npm:a-dep alias and the decoy was never present, so the test was weaker than intended.
  • No prior human review comments to address; the resolver-side order dependence is tracked separately in #38832 and this change is neutral to either outcome there.

robobun added a commit that referenced this pull request Aug 22, 2026
…y.createTestDir

createTestDir now returns the env that pins BUN_INSTALL_CACHE_DIR to the
project's own .bun-cache (the hunk #38255 proposes), so the file-local
wrapper goes. install() takes (env, cwd) like runBunInstall.

The ranged peer case pins no-deps@1.1.0 through the nd11 alias instead of
reaching it through two-range-deps' range, as #38858 does.
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