Skip to content

test(install): share the starting trees between bun-update-transitive cases and assert every update summary - #39741

Open
robobun wants to merge 4 commits into
mainfrom
farm/67186a3f/speed-up-bun-update-transitive-test
Open

robobun wants to merge 4 commits into
mainfrom
farm/67186a3f/speed-up-bun-update-transitive-test

Conversation

@robobun

@robobun robobun commented Aug 20, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • bun-update-transitive.test.ts takes 19s on the x64 ASAN lane (build 101560). Under ASAN bun test runs 5 cases at a time (context.rs:506), so the file takes its total per-case work divided by 5.
  • Its 174 cases spawned 594 bun processes, 303 of them installs of a starting tree, and most cases start from the same few dozen trees. The harness can not share one: makeTreeSync uses fs.cpSync, which leaves the links bun install writes pointing into the original.
  • About 30 cases did not read the stdout of the bun update under test.

Fix

  • Commit 1, test/harness.ts: copyTreeSync replaces fs.cpSync. A relative link is kept, an absolute link into the source is pointed at the copy. So createTestDir({ files: tree }) is an independent copy of an installed tree, cache included.
  • Commit 2: setup() and the recipes build each tree once and give every case such a copy. Helper signatures do not change. A case writes only to its copy. A tree is never written to after its promise resolves.
  • Commit 3: every bun update run asserts its stdout (list in the notes).
  • Processes: 594 to 410. Interleaved pairs on a loaded host, Linux debug ASAN build: main 27.3s, 35.4s, 28.7s against 24.4s, 29.8s, 25.1s. Windows x64 debug build: 18.0s against 14.7s. 174 pass in every run. Every open PR on this file merges cleanly on top.

Background

  • A recipe such as stale() installs a tree, then re-installs it with a widened package.json, which leaves a transitive row for bun update to move.
  • bun install writes absolute links for workspace members and store entries on Windows, and for the cache's index everywhere. A Windows directory link is recreated as a junction, which needs no privilege, as bun's own fallback does.
  • A real run prints transitive rows in package id order (print_transitive_updates). --dry-run sorts. So expectRowSetAnd compares two such rows as a set.
Notes

Self-review. A review of the first version of this PR (a copier local to this file, which also left the cache behind) made two points that this version takes up. The copier is a harness matter: tempDir(prefix, folder) and createTestDir({ files: folder }) already exist for copying a folder, their fs.cpSync is what made them unfit for an installed tree, and #39742 was about to add a second, different copier to bun-dedupe.test.ts for the same reason. So commit 1 fixes the harness and commit 2 is its first consumer. And the assertion commit conflicted with #38770 in the two cases that PR rewrites ("a sibling whose range rejects the picked version" and "several names in one command"), so those two are left as they are on main. Afterwards git merge-tree merges #38770, #38901, #38919, #39531 and #39742 cleanly onto this branch, both onto commit 2 and onto the whole PR. #39165 conflicts with main itself in mordant-baseline.toml and pack_command.rs, not with this branch. The review also suggested removing expectMoved, expectNoMoves and expectNoChangesLine and giving expectNoop a required count line. Not done: #38770 and #38901 add cases that call them with the current signatures, so that would break those PRs for no gain in the cases this PR touches (expectNoop returns its stdout instead, and the callers here pin the line).

copyTreeSync. The three existing callers of the folder form (migration/migrate.test.ts:728, migration/pnpm-migration.test.ts:185, migration/pnpm-lock-v9.test.ts:35) copy npm-arborist and pnpm fixture folders that hold regular files and directories only (checked with find), and all three pass with the new copier (125 pass and 1 todo, 3 pass and 11 todo as on main, 81 pass; pnpm-lock-v9 also on Windows). copyFileSync keeps the mode, as cpSync did. relative() is taken against realpathSync.native(source) because bun writes resolved targets. A link is recreated with the kind of its target (statSync on the source link), "file" if it dangles. On Windows a probe showed the workspace link of a hoisted tree is absolute and the store links of an isolated tree are relative on a runner with the symlink privilege, and with BUN_FEATURE_FLAG_FORCE_WINDOWS_JUNCTIONS=1 every link is an absolute junction that readdirSync reports as a link. In both modes the copy's links point into the copy and bun install --frozen-lockfile in the copy reports no changes for both linkers, and the whole file passes in both modes (174 pass, twice in normal mode and once with junctions forced, on this version).

Cost of a copy. A stale() tree is 23 entries, 12 of them cache. With the debug runner a copy takes 8.8ms (copyTreeSync) or 12.6ms (createTestDir), with a release runner 0.8ms or 1.6ms, so the 158 copies of a run cost about 0.25s with a release runner and about 1.5s more than the first version of this PR (which skipped the cache: 22.5s to 22.8s against 26.6s to 28.5s on a quieter host) with the debug one. The cache is copied anyway because that is what each case had before: its own installs had just warmed it. The index entries of the cache are absolute links into the tree's own cache and are relocated like the rest. fs.promises.cp and fs.cpSync were measured too: cpSync walks in JS on Linux and took 27ms per 20-entry tree with the debug runner against 7ms for the hand written walk, and the async form was slower end to end.

Timing method. Same machine, same debug ASAN build (bun bd, isASANEnabled() is true), 12 CPUs of quota, reading the Ran 174 tests line. The numbers in the description come from git checkout main -- test/harness.ts test/cli/install/bun-update-transitive.test.ts, a run, git checkout HEAD -- ..., a run, three times, with the host load average between 31 and 43, which is why the pairs differ so much from each other. The fixed cost of the file (verdaccio start plus runner start) is about 3.9s locally and is untouched. Windows: main 17.98s and 17.97s, this version 14.71s and 14.85s (not ASAN, so 20 cases at a time).

CI. Build 101601 (the first version) reported the file at 20.98s on the x64 ASAN lane against 19.20s for main in build 101560. The runner executes changed files first, so the file ran third in its job, during the job's service warm-up: coordinator: redis_unified ready lands in the middle of its output and its first cases were reported done after 8.7s, against 3.4s in build 101560 where it ran eighth. From there on main completed its cases in about 15s (11.5 per second) and the branch in about 12.5s (about 13.5 per second). The same applies to every build of this PR: in builds 101601, 102381 and 102393 the file ran third in its job, its first cases were reported done after 8.7s, 14.6s and 13.7s, and it finished 12.3s, 8.9s and 10.7s after that (20.98s, 23.50s and 24.41s in total, 174 pass each time), against main's 3.4s and then 15.8s in build 101560.

Count line semantics (install_with_manager.rs:1170): installs is the number of node_modules entries found in place, packages is the number of bun.lock rows. So a bundled dependency is a package but not an install (noChanges(3, 5), noChanges(1, 3)), a workspace link is an install (noChanges(3, 4)), and --filter pkg2 checks pkg2's installs only (noChanges(2, 4), the same line bun install --filter pkg2 prints on that tree).

Assertion changes (commit 3).

  • Exact summary (expectSummary) added to: a direct dependency's range left alone, bun up, -L (also pins the 1.0.1 start and the 2.0.0 end in bun.lock), the three staleMemberTransitive cases, the two disjointEdges move cases, from one member re-points a sibling, from a member lets a sibling follow, both runs of "from the root does not re-resolve a member's own entry" (plus where each no-deps copy is installed), the named and bare runs of "leaves the named package's own dependencies", above latest, the three prerelease cases, both alias cases (the row is printed under the alias, and the installed alias is checked), both peerDependencies cases, @types/* with --filter, --filter <other> (and that it saves the lockfile), the row index shift case (also pins + a-dep@1.0.1 (v1.0.10 available)), without a lockfile (+ no-deps@1.1.0 (v2.0.0 available)), and the auto-installed peer cases. The named peer case prints the same row as the bare one, so its planned flag is gone.
  • Exact no-op lines added to the two --latest hold-back cases, the bundled edge cases, and, through expectNoop returning stdout, the peer edge, peer-only, dependency cycle and two ABOVE_LATEST cases.
  • Row list plus count line (expectRowsAnd) replaces the row-only or contains checks in the two override cases, the moved dist-tag cases and the accepted scanner case.
  • expectRowSetAnd in the shared package cases (plus resolvedEdges pinning which dependent got 1.0.2 and which 2.0.2) and the --minimum-release-age case.
  • expectHeaderOnly (moved up from the outdated section) on the twelve update error paths, expectRejected included.
  • The three confirmed interactive runs check their rows and count line through expectPicked, which expectInteractiveDryRun now uses too.
  • expectKeptPatched (commit 2) folds the two identical patched bodies and adds that the lockfile is not reported as saved and that the patched 1.0.0 is still the one installed.
  • noDepsEdges became resolvedEdges(dir, name, ...) so that the shared package cases can use it.
  • expect() calls went from 2601 to 2197: the checks inside a recipe run once per shared tree instead of once per case, and install()'s two checks run 184 fewer times.

Not changed. The 35 cases that serve a registry from memory bake their server's port into bun.lock, so their trees are not shared. frozen() still runs after every update. No case was removed, skipped or reordered. The verdaccio start under the ASAN binary in CI is a fixed cost of every registry test file and is outside this PR.


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

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

Debug/ASAN (expected pass):
$ bun bd test 'test/cli/install/bun-update-transitive.test.ts'
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test test/cli/install/bun-update-transitive.test.ts
bun test v1.4.0 (4199361ed)

test/cli/install/bun-update-transitive.test.ts:
(pass) `bun update` moves a transitive dependency within its dependent's range (text lockfile) [973.93ms]
(pass) `bun update --latest` still moves transitive dependencies only within their ranges [926.05ms]
(pass) `bun update` moves a transitive dependency within its dependent's range (isolated linker) [982.15ms]
(pass) `bun update` moves a transitive dependency within its dependent's range (binary lockfile) [986.19ms]
(pass) `bun update no-deps` reaches a package that is only a transitive dependency [1076.75ms]
(pass) `bun update` with a pattern reaches a package that is only a transitive dependency [388.84ms]
(pass) `bun update` with a bare `*` reaches a package that is only a transitive dependency [353.41ms]
(pass) `bun update` with a negated name reaches a package that is only a transitive dependency [397.00ms]
(pass) `bun update` with a pattern alongside a direct name reaches a package that is only a transitive dependency [374.20ms]
(pass) `bun update --latest no-deps` reaches a package that is only a transitive dependency [551.95ms]
(pass) `bun update <name>` naming a package with nothing newer is the same no-op as a bare rerun [235.00ms]
(pass) without a lockfile, `bun update <undeclared>` is rejected and writes no lockfile [170.67ms]
(pass) `bun update <name>` and a pattern that match nothing in bun.lock name that file [325.57ms]
(pass) `bun update <name>` and a pattern that match nothing in bun.lockb name that file [334.56ms]
(pass) without a lockfile, `bun update <declared>` resolves and saves [412.49ms]
(pass) `bun update --frozen-lockfile` refuses to move the transitive dependency [265.16ms]
(pass) `bun update --lockfile-only` moves the transitive dependency in bun.lock only [420.89ms]
(pass) `bun update --dry-run` (bare) prints the plan as su
... (truncated)
Exit: 0
diff hotspot
test/cli/install/bun-update-transitive.test.ts | 562 +++++++++++++++----------
 test/harness.ts                                |  44 +-
 2 files changed, 382 insertions(+), 224 deletions(-)

gate history · 3 passed · 0 rejected · iteration 2

evidence per changed file
file                                            reads  edits  tests
test/cli/install/bun-update-transitive.test.ts     54     96      0
test/harness.ts                                     5      3      0

@coderabbitai

coderabbitai Bot commented Aug 20, 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: bd54aa0f-32c6-41fb-9b38-bd8c3ea11fb4

📥 Commits

Reviewing files that changed from the base of the PR and between 72ec6e2 and bcf2e17.

📒 Files selected for processing (2)
  • test/cli/install/bun-update-transitive.test.ts
  • test/harness.ts

Included review availability: Your plan provides up to 5 included reviews per hour; 0 remain after this review.


Walkthrough

The pull request expands bun update transitive-resolution tests. It adds cached fixture copying, structured output checks, lockfile edge assertions, workspace and dependency-form coverage, failure checks, and interactive selection coverage.

Changes

Transitive update test coverage

Layer / File(s) Summary
Fixture tree copying
test/harness.ts
makeTreeSync uses copyTreeSync for recursive file, directory, and symbolic-link copying.
Cached fixtures and output assertions
test/cli/install/bun-update-transitive.test.ts
Shared helpers validate update rows, package counts, resolved edges, clean stderr, and header-only failures.
Dependency resolution scenarios
test/cli/install/bun-update-transitive.test.ts
Tests cover patches, overrides, hoisting, peers, bundled entries, tags, release ages, prereleases, and production dependencies.
Workspace update behavior
test/cli/install/bun-update-transitive.test.ts
Workspace tests validate root/member updates, cross-workspace edges, scope rejection, sibling repointing, and locked groups.
Aliases, patterns, and interactive selection
test/cli/install/bun-update-transitive.test.ts
Tests validate alias patterns, peer-entry matching, malformed lockfiles, and interactive package selection.

Suggested reviewers: jarred-sumner, alii, cirospaciari

🚥 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 summarizes the two main changes: sharing starting trees and asserting update summaries.
Description check ✅ Passed The description explains the changes and provides detailed verification results, although it does not use the template headings.

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

@robobun

robobun commented Aug 20, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: ready for review at bcf2e17 (the diff is 33aea72 plus an empty CI rerun commit). Nothing is left to do on this side.

CI: the file itself and the three other callers of the changed harness code passed on every lane in builds 102381 and 102393. Both builds are red only because of test/js/bun/http/bun-server.test.ts on Windows 2019 x64, a heap-count test that fails on main as well and is reported separately, plus flakes in unrelated files that passed on retry (inspector protocol, websocket-server, bun-install-registry, bun-lock, config-precedence). The build before those, 102365, failed its binary size check only because the branch predated #39770, and was rebased.

How the slowness was measured: bun bd test test/cli/install/bun-update-transitive.test.ts on a debug ASAN build runs 5 cases at a time. Interleaved with main on the same machine it took 27.3s, 35.4s and 28.7s on main against 24.4s, 29.8s and 25.1s on this branch. The file spawned 594 bun processes on main and spawns 410 here. All 174 cases pass in every run on Linux and on Windows x64, with and without BUN_FEATURE_FLAG_FORCE_WINDOWS_JUNCTIONS. The CI lane numbers are taken apart in the notes of the description: a changed file runs first in its job, during the service warm-up, so its total there is not comparable with main's, while the cases themselves run faster.

@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. It's a sizable test-infrastructure refactor (a new copyOf/copyTree memoization layer with hand-written cross-platform symlink handling, plus ~30 tightened stdout assertions), so a human look would still be worthwhile.

What was reviewed:

  • copyOf memoization: get/set on templates has no await between them, so concurrent test.concurrent callers can't race; recipes build on a copy from setup(), so shared templates are never written after their promise resolves.
  • copyTree: .bun-cache is skipped and run() already points BUN_INSTALL_CACHE_DIR at the copy; absolute symlinks are re-targeted into the copy via relative(template, target), relative ones kept verbatim.
  • Recipes that read post-build state (staleMemberEdges texts/lockBefore, stalePatched patched-file handle) do so on the per-test copy, not the shared template.
  • resolvedEdges slice arithmetic and expectRowSetAnd copy-before-sort look correct.
Extended reasoning...

Overview

This PR touches a single test file, test/cli/install/bun-update-transitive.test.ts, with two goals: (1) memoize the ~55 distinct starting trees so that 174 concurrent test cases share installs instead of each spawning their own bun install subprocesses (594 → 410 processes), and (2) tighten stdout assertions on ~30 bun update runs that previously only checked stderr or a subset of the output. The mechanism is a new copyOf(key, layout, build) helper backed by a Map<string, Promise<string>> and a hand-written copyTree that recursively copies a built tree, skipping .bun-cache and re-pointing absolute symlinks (Windows workspace/store links) at the copy. Nine recipe helpers (stale, staleSiblings, stalePatched, staleMemberTransitive, staleMemberEdges, staleShared, staleAutoInstalledPeer, staleScoped, staleAlias, stalePeerEntry, staleMemberGroups) are wrapped in copyOf, and setup() itself is memoized. Several assertion helpers are added or generalized (expectRowSetAnd, expectCountLine, expectPicked, expectKeptPatched, resolvedEdges), expectHeaderOnly is moved up and applied to twelve error paths, and two identical patched-test bodies are folded into one helper.

Security risks

None. This is test-only code that spawns bun against a local Verdaccio registry in temp directories. No production code, no network egress, no auth/crypto surface.

Level of scrutiny

Medium-high for a test-only change. The diff is ~400 lines and introduces a new sharing pattern that every subsequent case in this file depends on. The correctness of copyTree (particularly the Windows absolute-symlink retargeting via join(copy, relative(template, target)) and the .bun-cache skip) and of the memoization keying (JSON-stringified [recipeName, ...params, bunfigOpts]) determine whether copies are truly isolated from their templates. I traced each wrapped recipe to confirm its build() closure operates on a fresh copy (via the inner setup() call, which itself returns a copy) and that nothing writes to the memoized directory after the promise resolves. I also confirmed run() already sets BUN_INSTALL_CACHE_DIR to join(dir, ".bun-cache") per copy, so skipping the template's cache is safe. The get-then-set on the templates map is synchronous, so test.concurrent callers can't double-build.

Other factors

The PR description is unusually thorough — it documents timing methodology on both Linux ASAN and Windows, explains why fs.cpSync was rejected (4× slower under debug, and its default symlink resolution would leave a Windows copy pointing into the shared tree), and enumerates every assertion change. The author reports 174/174 passing across 7 Linux and 3 Windows runs. That said, the strengthened assertions encode exact count-line semantics (installed(N), noChanges(installs, packages)) derived from install_with_manager.rs internals, and the new copyTree is bespoke filesystem code with platform branches — both are the kind of thing a maintainer familiar with the install output format and Windows CI behavior should sign off on rather than an automated approval.

@robobun

robobun commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator Author

The two areas the review points a human at are documented in the notes of the description:

  • The count lines come from install_with_manager.rs:1170: installs is the number of node_modules entries found in place, packages is the number of bun.lock rows. The three cases where the two differ in a way that is not obvious (a bundled dependency, a workspace link, --filter) are listed there with the line each one prints.
  • copyTree on Windows was checked with a probe on a Windows x64 machine: the shared tree's workspace link is absolute, the copy's link points at the copy's own packages/pkg1, and bun install --frozen-lockfile in the copy reports no changes for both linkers. The whole file then passed three times there.

@robobun

robobun commented Aug 21, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 12:03 AM PT - Aug 21st, 2026

❌ @robobun, your commit bcf2e17 has 1 failures in Build #102393 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 39741

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

bun-39741 --bun

@robobun

robobun commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator Author

Pushed a reworked version (4835ed0), after a self-review of the first one. What changed:

Re-verified: 174 pass on Linux (interleaved with main: 27.3s, 35.4s, 28.7s against 24.4s, 29.8s, 25.1s on a loaded host) and on Windows x64 (14.7s against 18.0s), also with BUN_FEATURE_FLAG_FORCE_WINDOWS_JUNCTIONS=1. The description is rewritten for this version.

…h fs.cpSync

makeTreeSync, and so tempDir and createTestDir, copied a folder with
fs.cpSync. That keeps an absolute link pointing into the folder it was
copied from, and resolves a relative one against it, so a copy of an
installed project was not independent of the original. copyTreeSync
recreates each link instead: a relative target is kept, and an absolute
target inside the source is pointed at the copy. On Windows a directory
link with an absolute target is recreated as a junction, which needs no
privilege, like the fallback bun install itself uses.

The three existing callers copy fixture folders without links and are
unaffected. This lets a test install a tree once and give every case a
copy of it through createTestDir({ files: tree }).
Most cases of the file start from one of a few dozen trees, and each
case installed its own: 303 of the 594 bun processes the file spawned
were those installs. setup() and the recipes several cases start from
(stale, staleSiblings, staleMemberEdges, staleShared, the patched trees
and the rest) now build their tree once, in the first case that asks
for it, and give every case a copy of it through createTestDir. The
tree is never written to after it is built. Helper signatures and
return shapes do not change.

The file now spawns 410 processes. With a debug ASAN build, which runs
5 cases at a time, it takes about 24s instead of 27s to 28s.
…te-transitive

About 30 cases did not read the stdout of the bun update they ran.
Each run now asserts the exact summary where one row moves, the row set
plus the count line where two packages new to bun.lock move (a real run
prints those in package id order, so the order is not pinned), the
header alone on every error path, and the exact "Checked N installs
across M packages" line on every no-op. Several cases also pin which
version each dependent's edge resolves to and what is installed.

The two cases that #38770 rewrites are left as they are, so that PR and
the other open PRs on this file still merge on top of this one.
@robobun
robobun force-pushed the farm/67186a3f/speed-up-bun-update-transitive-test branch from 4835ed0 to 33aea72 Compare August 21, 2026 06:08
@robobun

robobun commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator Author

Rebased onto main (33aea72, no changes to the diff). The only non-flaky failure in build 102365 was the binary size check: it reported every target 2 MB over the canary, because the canary already has #39770 (the 2 MB trim) and this branch was based on a main from before it. This PR touches test/ only. The file still passes on the new base (174 pass), as does migration/pnpm-lock-v9.test.ts, one of the callers of the changed harness code.

@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. It's a large test refactor (~600 lines) that also changes shared harness behavior — makeTreeSync now uses a hand-written copyTreeSync with symlink/junction relocation instead of fs.cpSync, and the test file adopts a memoized-template-tree pattern under test.concurrent. Given the scope and the shared-infrastructure change, a human look would still be worthwhile.

What was reviewed:

  • copyTreeSync symlink handling: relative links kept, absolute links inside source retargeted via relative(realpathSync.native(source), target); Windows absolute dir links become junctions. The three existing string-path callers of makeTreeSync (the migration tests) copy link-free fixtures, so no behavior change there; patch.test.ts only passes object trees.
  • copyOf memoization under test.concurrent: the promise is stored in the map before any await, so concurrent callers share one build; each test then gets an independent createTestDir copy, and writeBunfig overwrites the copied bunfig.toml so the copy's cache path points at itself. Recipes that call setup() inside their own copyOf write to a copy of the setup template, not the template itself.
  • Assertion changes strengthen coverage (exact summary/count lines, expectHeaderOnly on error paths) and no test is removed, skipped, or has an assertion weakened.
Extended reasoning...

Overview

Two files: test/harness.ts gains copyTreeSync (replaces fs.cpSync inside makeTreeSync's string-path branch) and test/cli/install/bun-update-transitive.test.ts is refactored so each of its ~dozen recipe helpers builds its starting tree once and hands every test case an independent copy. About 30 cases that previously ignored bun update stdout now assert it exactly.

Security risks

None. Test-only; no production code, no network, no auth/crypto surface.

Level of scrutiny

Medium. It's test infrastructure, so a mistake surfaces as a failing or (worse) vacuous test rather than a runtime bug. But two aspects deserve human eyes: (1) copyTreeSync is now the copier for every tempDir(prefix, path) call repo-wide and its Windows junction/symlink relocation logic is platform-sensitive, and (2) the memoized-template pattern is a new idiom that concurrent tests share — the author's argument that templates are never written after their promise resolves checks out, but it's the kind of invariant a maintainer should sign off on before it spreads to other test files.

Other factors

  • The diff is ~380 insertions / ~220 deletions across 174 test cases; not mechanical enough to auto-approve.
  • Dozens of new exact count-line assertions (noChanges(2, 3), noChanges(3, 5), installed(1)) each encode a specific reading of install_with_manager.rs output semantics. The PR notes explain the non-obvious ones (bundled deps, workspace links, --filter), and all 174 pass on Linux and Windows per the author, but a maintainer confirming those readings once is cheap insurance.
  • No prior human review on the thread; the earlier self-review already reshaped the PR (moved the copier into the harness, dropped changes that would conflict with open PRs).

@robobun

robobun commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator Author

On the two points the review leaves for a human: the folder form of makeTreeSync (and so of tempDir and createTestDir) has three callers in the repo besides this file, all listed in the notes of the description with their results on the new copier, and each count line that is not self-evident (bundled dependency, workspace link, --filter) is traced to install_with_manager.rs:1170 there as well. Build 102381 is running on the rebased head.

@robobun

robobun commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator Author

Both automated reviews of the rebased version came back without findings, and no review thread is open. The remaining item is build 102393, which reruns CI after build 102381 failed only on files this PR does not touch (bun-server.test.ts on Windows 2019, which also fails on main and is reported, plus three retry-passing flakes).

@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's a ~600-line refactor that changes shared harness behavior (makeTreeSync now uses a hand-rolled copyTreeSync with symlink relocation and Windows junction handling) and hard-codes many exact count-line values across 174 concurrent tests, a human look would still be worthwhile.

What was reviewed:

  • Template sharing via copyOf: no await between templates.get/set, build resolves before any copy starts, every caller writes only to its own createTestDir copy (including nested setup() inside recipe builds).
  • copyTreeSync: absolute-link relocation uses realpathSync.native(source) as the base, relative links kept as-is, dangling links fall through to "file"; createTestDir overwrites the copied bunfig.toml so the copy's cache path points at itself.
  • No assertions weakened — removed .not.toContain checks are subsumed by exact expectSummary; expectRejected's dropped return value has no consumers; the removed planned flag on the auto-installed-peer case is a strengthening.
Extended reasoning...

Overview

Test-only refactor of test/cli/install/bun-update-transitive.test.ts (174 cases) plus a harness change in test/harness.ts. Three separable pieces: (1) copyTreeSync replaces fs.cpSync inside makeTreeSync, recreating symlinks so an installed tree can be copied independently of its original; (2) a module-level templates map memoizes each starting tree so it's installed once and copied per test via createTestDir; (3) ~30 cases gain exact stdout assertions (expectSummary, expectRowsAnd, expectRowSetAnd, expectHeaderOnly, expectPicked) and several helpers are extracted or generalized (expectKeptPatched, resolvedEdges, stalePatched).

Security risks

None. Test-only; no production code, auth, or network surface touched.

Level of scrutiny

Medium-high for a test change. The harness edit reaches every caller of tempDir/tempDirWithFiles/createTestDir that passes a string path (three existing callers, all verified per the notes), and the symlink-relocation logic has platform-specific branches (junction vs. "dir"/"file" symlinks on Windows). The template-sharing model changes the isolation contract of 174 concurrent tests — correctness depends on every recipe writing only to the copy it receives, which I traced through setup, stale, staleSiblings, stalePatched, staleMemberTransitive, staleMemberEdges, staleShared, staleAutoInstalledPeer, staleScoped, staleAlias, stalePeerEntry, and staleMemberGroups. The many new hard-coded count lines (noChanges(2, 3), noChanges(3, 4), noChanges(2, 4), etc.) are each traceable to install_with_manager.rs:1170 semantics as the description documents, but a maintainer should confirm those match intent.

Other factors

The PR is unusually well-documented (every count line, every assertion change, Windows probe results, timing methodology) and reports 174/174 passing on Linux debug/ASAN and Windows x64 in both symlink modes. No prior human or claude[bot] review on the thread. The bug-hunting pass found nothing. I checked that expectRejected's dropped return is unused, that [...rows].sort() in expectRowSetAnd doesn't mutate the caller's array, and that templates.get/set in copyOf has no await between them so concurrent test.concurrent callers can't double-build. The change is sound as far as I can trace, but its size and the harness-wide reach put it outside the "simple/mechanical" bar for auto-approval.

@robobun

robobun commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator Author

One point on the many exact count lines, since each review pass mentions them: every case builds its line through installed(), noChanges(), nothingToUpdate() or noneSelected(), so a change to the wording of the line is a one-line change in this file, and only a change to what gets counted touches the cases, which is the kind of change these lines are there to surface. Build 102393 is still running.

@robobun

robobun commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator Author

Final state from this side: build 102393 (the rerun) is red for the same reasons as 102381, that is bun-server.test.ts on Windows 2019, which fails on main too and is reported, plus flakes in unrelated files that passed on retry. This file and the three other callers of the changed harness code passed on every lane in both builds (the file: 174 pass, 23.5s and 24.4s on the x64 ASAN lane, both times during the job's warm-up as described in the notes). Nothing else is pending here; the status comment at the top is current.

robobun added a commit that referenced this pull request Aug 27, 2026
…like copyTreeSync

shared() takes the bunfig options of its tree and writes the copy's
bunfig through VerdaccioRegistry.writeBunfig instead of patching the
copied one. packageTree and workspaceTree build the trees the way setup,
setupWithLinker and setupWorkspaces do, from one helper each.

copyTree recreates a relative link with its own kind and only an
absolute directory link as a junction, the same as the harness's
copyTreeSync in #39741, so the two cannot drift apart.

This branch has not been deployed

No deployments
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