Skip to content

test: replace the vite install in ctrl-c.test.ts with a local bin - #45542

Open
robobun wants to merge 3 commits into
mainfrom
robobun/cdb5fe39/ctrl-c-test-local-bin
Open

robobun wants to merge 3 commits into
mainfrom
robobun/cdb5fe39/ctrl-c-test-local-bin

Conversation

@robobun

@robobun robobun commented Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • test/regression/issue/ctrl-c.test.ts installs unpinned vite@^6.0.1 from registry.npmjs.org in beforeAll. When the install fails, all six kills on SIGINT in: 'bun ...' cases fail at expect(viteInstallExitCode).toBe(0).
  • Build 125085: error: GET https://registry.npmjs.org/rollup/-/rollup-4.64.6.tgz - 404 on seven Linux lanes, minutes after its publish.
  • Fixes ^C on bun vite #15545 added the install. No commit in bun caused the break.

Fix

  • Replace vite with long-running, a local file: package. Its bin prints one line and idles with no SIGINT handler. bun install still links node_modules/.bin and contacts no registry.
  • On POSIX each case first sends SIGUSR2 and waits for the bin's answer, then sends SIGINT. Modes and assertions stay.
  • Correct because the cases test that bun run dies from the SIGINT that killed its child, not vite. bun 1.1.36, which has the bug they guard (Cannot exit "bun tsc -w" since v1.1.27 #14375), fails bun dev under both fixtures. Verified: bun bd test test/regression/issue/ctrl-c.test.ts, also with the registry unreachable.
  • Self-reviewed: 16 concerns raised, 13 addressed. Not taken: a no-network guard in the test runner.

Background

Behaviour change: none

Notes

Build 125085: npm published rollup@4.64.6 at 09:07:46 UTC. The seven lanes got the 404 from 09:08:42 to 09:11:17 UTC, four attempts each. With the registry unreachable, main fails the six cases in the same way (below).

Other builds with the same cause

  • 2026-10-13, the same rollup@4.64.6 404: builds 125079 and 125080 (macOS aarch64), 125083 (both Windows lanes).
  • 2026-10-08, builds 123938 and 123939 (Windows 2019 x64): Cannot find module @rollup/rollup-win32-x64-msvc, then kill() failed: ESRCH. npm has rollup@4.64.3 from 18:12:28 UTC and @rollup/rollup-win32-x64-msvc@4.64.3 from 18:17:35 UTC. An install between the two gets rollup without its native package.
  • 2026-10-13, builds 124984, 124999 and 125024 (main): the install hung at Resolving dependencies and the beforeAll hook timed out. The retry passed.

Reproduction that does not depend on npm

$ HTTPS_PROXY=http://127.0.0.1:9 HTTP_PROXY=http://127.0.0.1:9 NO_PROXY=localhost,127.0.0.1 \
    bun bd test test/regression/issue/ctrl-c.test.ts

main:         error: ConnectionRefused downloading package manifest vite
              2 pass, 6 fail
this branch:  8 pass, 0 fail

What bun install still does on the network: it starts the DNS prefetch for the default registry host (src/install/PackageManager/install_with_manager.rs:72) and does not wait for the answer. With a resolver that never answers, the install of the fixture takes about 0.2 s on the debug build and sends two DNS queries. It opens no connection to a registry.

Why the cases send SIGUSR2 first

The bug, on main:

  • src/spawn/process.rs:3267-3268 sets Bun__currentSyncPID to 0 and registers the forwarding handlers. Then it spawns, and only after the spawn returns does it store the pid. :3344 calls Bun__sendPendingSignalIfNecessary().
  • A signal that reaches bun run before the pid is stored is only recorded (src/jsc/bindings/c-bindings.cpp:1093-1097) and is forwarded later, outside the signal handler (:1074-1083).
  • :1069-1071 blocks the signal, sends it, and then unblocks the set that was blocked before. Outside a handler that set does not hold the signal, so the signal stays blocked in bun run.
  • The child dies from the signal. src/bun_core/Global.rs:789-790 is raise(sig), then abort(). The raise does nothing for a blocked signal, so bun run dies from SIGABRT.
  • Clear the inherited signal mask at startup and exec children with an empty one #38926 has the patch (SIG_SETMASK of the saved mask). cli: exit 128 + signo when bun cannot die from a re-raised signal (bun as container PID 1) #38877 is a second guard at the raise.

What it meant for this file:

  • From the start of bun <mode> to the first line on stdout, vite takes about 250 ms (193 ms at least) and the local bin about 60 ms (35 ms at least), on a release build. When bun run does not get CPU time in that span, the SIGINT of the test arrives before the pid is stored.
  • Forced, with an LD_PRELOAD shim that holds bun run for 100 ms after the spawn: without the SIGUSR2 step the four cases that spawn a child end with signalCode: "SIGABRT" in every run. With the step all eight tests pass, with a hold of 100 ms and of 300 ms, on a release build and on the debug build.
  • Not forced, release build, 36 busy loops on 12 cores: 4 of 270 runs of the file hit SIGABRT without the step, 0 of 270 with it.
  • The test runner does not retry a failure that prints SIGABRT (scripts/runner.node.ts:1858). CI shows that today in another test: test/cli/run/run_command.test.ts ("a script killed by Ctrl+C stops bun run") failed with Received: "SIGABRT" in builds 124378, 124477, 124545, 124556, 124759, 125044 and 125184.

How the step works:

  • SIGUSR2 is one of the signals that bun run forwards. The bin answers it with a line. The answer can only come after bun run has the pid, on either forwarding path.
  • When SIGUSR2 itself comes too early, it is SIGUSR2 that stays blocked in bun run. The SIGINT that follows takes the normal path.
  • In the two file modes there is no child. bun runs the bin itself and answers.
  • In the dev cases the signal goes to the pid of the shell. bun run uses bash when it is on PATH, and bash execs the script, so the bin has that pid. The CI images install bash (scripts/build/ci-images/spec.ts). dash 0.5.12 does not exec.
  • Windows has no SIGUSR2 and keeps the old order: first line, then kill.

The local fixture finds the same regression as vite

A standalone copy of the test loop, run against two bun versions on Linux x64:

bun fixture result
1.1.36 (has the bug of #14375) vite bun dev and bun --bun dev end with {killed: true, exitCode: 0, signalCode: null}. bun vite prints error: Failed to run "vite" due to signal SIGINT.
1.1.36 (has the bug of #14375) long-running, SIGUSR2 step bun dev ends with the same state. bun long-running prints the same error.
1.4.3 long-running, SIGUSR2 step All six modes end with signalCode: "SIGINT".

What the old fixture did besides the six cases

  • Four of the six modes started vite's dev server under bun (two of four on Windows). Nothing asserted on it, but it failed the file when vite did not start. From 2025-09-19 that was the first CI signal for rollup 4.52.0, which called process.report.getReport() on Windows. feat: Implement process.report.getReport() on Windows #22816 added it the next day.
  • It was unpinned, so CI ran the newest vite, rollup and esbuild code, install scripts included. The local fixture runs no third-party code.
  • A check like that can come back as a pinned test of its own. It does not belong in a signal test.

What each mode runs (exec_with_cfg in src/runtime/cli/run_command.rs)

  • bun long-running: there is no script and no file of that name, so the bin in node_modules/.bin runs as a child (run_binary_without_bunx_path).
  • bun dev: the package.json script runs through the shell (run_package_script_foreground_with_shell_path).
  • bun ./node_modules/.bin/long-running: bun opens the file and runs it in its own process (maybe_open_with_bun_js).
  • --bun puts bun in the place of node for the shebang.
  • The fixture folder is long-running-src because bun long-running looks for that path in the cwd first (src/resolver/resolver.rs:1889).

Other changes in the diff

  • PORT: "9876" is gone from the env. Nothing listens on a port now.
  • The spawn is await using with killSignal: "SIGKILL". Before, a failed case left bun and its child running. The signal is SIGKILL because the dispose waits for the exit. With the default SIGTERM, a process that ignores the signal makes the test time out, and the failed assertion is lost. Measured with a bin that ignores SIGINT and SIGTERM: this test timed out after 5000ms with SIGTERM, the assertion diff after 0.3 s with SIGKILL. The dispose signals only the pid of the spawned bun. A child that outlives it ends with the 60 s timer.
  • The wait for a line stops with an error that shows the state when bun ends first.
  • The file takes 0.5 s on a release build. With the vite install it took 5.8 s.

Windows: I cannot run Windows locally. On Windows, process.kill(pid, "SIGINT") terminates bun, and the child outlives it. The test runner then logs EPERM when it removes the temp dir. Main does the same with vite (build 125085, both Windows lanes). The difference: the vite server did not end, and long-running ends after at most 60 s.

The rest of the class

  • Nine files in test/regression/issue still install from npm on main: 00631, 026039, 07740, 11806, 24131, 24364, 26225, 26657 and 28193. 24364 adds four unpinned packages and is the most exposed.
  • test: stop ten regression tests from contacting npm #37401 has changes for them, but it needs more than a rebase. It conflicts with main in 15276.test.ts and patch-bounds-check.test.ts, which main has since made local, and seven of its files import VerdaccioRegistry, which test: replace verdaccio with an npm registry on Bun.serve #44055 removed from the harness on 2026-10-12.
  • Not in this PR: a guard in the test runner that denies the network to tests. That is a separate change with its own cost.

no test proof · iteration 0 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/regression/issue/ctrl-c.test.ts

@coderabbitai

coderabbitai Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: oven-sh/bun/.coderabbit.yaml
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 2b9a7cb3-1c5d-4daf-9f34-22d237de4390
📥 Commits

Reviewing files that changed from the base of the PR and between b3e6382 and 594b8a2.

📒 Files selected for processing: 1
  • test/regression/issue/ctrl-c.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 2 remain after this review.

Walkthrough

The SIGINT integration tests replace the shared Vite fixture with a local long-running package. They test its binary and package-script entry points, with and without --bun. On non-Windows systems, they also test the .bin path and SIGUSR2 handling before sending SIGINT.

Changes

SIGINT integration tests

Layer / File(s) Summary
Fixture and process entry points
test/regression/issue/ctrl-c.test.ts
A shared project installs a local package with a binary that prints a readiness line, idles, and responds to SIGUSR2 on non-Windows systems. Tests invoke the binary directly, through the package script, and through its .bin path on non-Windows systems. They wait for readiness, check the SIGUSR2 response where applicable, then test SIGINT. Spawned processes use SIGKILL for disposal.

Suggested reviewers: jarred-sumner

Priority: ⬇️ Low

Merge Risk: ⚪ Minimal · up to 594b8

This change makes a SIGINT regression test hermetic by removing its dependency on the registry and Vite. It has no production impact and no actionable merge-blocking risk remains.

Pre-merge checks: Passed 4
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 main change: replacing the Vite installation with a local binary in ctrl-c.test.ts.
Description check Passed The description explains the problem, fix, verification, behavior impact, platform considerations, and known limitations. It does not use the exact template headings, but it provides the required info…
Full details: Description check

Explanation

The description explains the problem, fix, verification, behavior impact, platform considerations, and known limitations. It does not use the exact template headings, but it provides the required information in equivalent sections.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@robobun

robobun commented Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 1:36 PM PT - Oct 18th, 2026

✅ @robobun, your commit 594b8a2d6e8c9ba2f578af0c6aef4d26b31cf505 passed in Build #128422! 🎉


🧪   To try this PR locally:

bunx bun-pr 45542

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

bun-45542 --bun

@robobun

robobun commented Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: ready for review. CI is green on 594b8a2.

Reproduction: point HTTPS_PROXY and HTTP_PROXY at a closed port and run bun bd test test/regression/issue/ctrl-c.test.ts. On main the install fails with error: ConnectionRefused downloading package manifest vite, and the six kills on SIGINT cases fail at expect(viteInstallExitCode).toBe(0). That is the failure of build 125085. On this branch all eight tests pass in the same setup.

CI (build 128422): all 181 jobs passed. ctrl-c.test.ts shows 8 pass on macOS x64 and on macOS aarch64, and 4 pass with 2 skip on Windows 2019 x64 and on Windows 11 aarch64. No Linux lane needed a retry for it.

PR: #45542

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

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Beyond the inline finding, I also checked whether the fixture swap narrows what the six cases exercise: the file: dependency still goes through bun install bin linking, the #!/usr/bin/env node shebang matches vite's own bin (so the node-on-PATH requirement and the --bun substitution path are unchanged), and the 300ms Promise.race grace window at test/regression/issue/ctrl-c.test.ts:155 is pre-existing and untouched by this diff.

Extended reasoning...

The change is confined to one regression test file, replacing a registry install of vite with a local file: package whose bin prints a line and idles; it touches no security-sensitive surface. The launch-mode matrix and the post-SIGINT assertions are unchanged, and the dropped hardcoded PORT is a straightforward improvement. The one inline finding (no cleanup of the spawned process on assertion failure) is a pre-existing shape that the new 60s timer actually bounds, so it is a note rather than a blocker.

Comment thread test/regression/issue/ctrl-c.test.ts Resolved

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

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

LGTM. The second commit addresses the process-leak point from my earlier review: the spawn is now await using registered before the assertions, and the dispose path in subprocess.rs sends kill_signal only when the process has not already exited, so it cannot affect the signalCode: "SIGINT" assertions on the success path. The file: fixture with a bin is a pattern the install suite already exercises, and the cwd-folder-shadowing note in the comment matches the resolve-before-.bin order in run_command.rs.

Extended reasoning...

The diff touches one test file, test/regression/issue/ctrl-c.test.ts, swapping an unpinned registry install of vite for a local file: dependency with a bin and binding the spawned process with await using plus killSignal SIGKILL; it touches no production code and no security-sensitive surface. The six launch modes and their platform-branched assertions are unchanged, the hardcoded PORT env was dropped since nothing listens anymore, and the only reported finding is a pre-existing 300ms race the diff did not introduce. The prior review's one concern was addressed by the follow-up commit, no CODEOWNERS entry covers the path, and the hunt ran dry, so a human look is not needed.

Additional findings (outside the current diff — GitHub can't attach inline comments there):

  • 🟣 test/regression/issue/ctrl-c.test.ts — pre-existing nit: on a slow runner (debug/ASAN CI) all six SIGINT cases can fail with killed: false, exitCode: null, signalCode: null even though bun behaves correctly. The wait at test/regression/issue/ctrl-c.test.ts:159 is Promise.race([proc.exited, Bun.sleep(300)]), a fixed 300ms sleep rather than awaiting the condition, and the comment does not say why no observable signal exists. Fix: await proc.exited (the test runner's own timeout bounds a hang) or poll with a deadline well above 300ms, keeping the same {killed, exitCode, signalCode} assertion; if a fail-fast bound is wanted, the comment should state that exited never resolves on the regression and that is why a sleep is used.

    Why this was flagged

    The trigger is a CI runner where the spawned bun run parent takes more than 300ms to forward SIGINT, reap its child and re-raise the signal, which happens on loaded debug+ASAN lanes. The test at test/regression/issue/ctrl-c.test.ts:156 sends SIGINT and then at :159 races proc.exited against Bun.sleep(300); when the sleep wins, proc.exitCode and proc.signalCode at :163-164 are still null and the toEqual at :161 fails although the runtime is not broken. The base branch has the identical race (it was carried over from the vite version), so this is pre-existing; the diff keeps it and adds await using with SIGKILL at :138-145, so a lost race now also SIGKILLs the parent, leaving the orphaned child until its 60s timer at :112. The repository review rules ask that a literal sleep of 50ms or more outside a bounded poll loop carry a comment naming why no observable signal exists; the comment at :158 only says it is the same grace period as before. A correct wait awaits proc.exited or polls with a deadline, so a slow but correct bun passes.

    Verification: pre-existing. Trigger: a runner where the spawned bun run parent needs more than 300ms to forward SIGINT. test/regression/issue/ctrl-c.test.ts:159 is await Promise.race([proc.exited, Bun.sleep(300)]);, a fixed sleep rather than awaiting the condition. When the sleep wins, the assertion at :161-177 fails even though bun forwarded the signal correctly. The base file has the identical line, so merging makes nothing worse.

@robobun

robobun commented Oct 16, 2026

Copy link
Copy Markdown
Collaborator Author

On the 300 ms grace after SIGINT: it is unchanged from main, on purpose.

  • I found no CI failure that it caused. Every ctrl-c.test.ts failure that I checked came from the install: builds 123938, 123939, 124984, 124999, 125024, 125079, 125080, 125083 and 125085. In 103 builds of main from 2026-09-25 to 2026-10-16, the file shows up once (125024), with the install hang.
  • The bound has a purpose. The regression that these cases guard is a bun that does not die. With await proc.exited, that regression becomes a test timeout for each case (90 s in CI, for each attempt). With the bound, each case fails after 0.3 s and shows the {killed, exitCode, signalCode} diff.

If the grace starts to flake with the lighter fixture, a poll with a longer deadline is the right change, in its own PR.

@robobun
robobun force-pushed the robobun/cdb5fe39/ctrl-c-test-local-bin branch from b3e6382 to 8d0f238 Compare October 17, 2026 06:59
@robobun

robobun commented Oct 17, 2026

Copy link
Copy Markdown
Collaborator Author

The review of this change surfaced a bug in bun that the vite fixture hid. This PR does not change bun. It only keeps the six cases away from the bug.

  • bun run stores the pid of its child after the spawn returns (src/spawn/process.rs:3267-3288). A signal that arrives before that is recorded and forwarded later, outside the signal handler (src/jsc/bindings/c-bindings.cpp:1074-1083).
  • That forward ends with pthread_sigmask(SIG_UNBLOCK, &restore_mask, nullptr) on the set that was blocked before (c-bindings.cpp:1069-1071), so the signal stays blocked in bun run. When the child then dies from the signal, raise() in src/bun_core/Global.rs:789-790 returns and abort() runs. bun run dies from SIGABRT.
  • Vite needs about 250 ms to print its first line. The local bin needs about 60 ms, so a SIGINT sent at the first line could arrive before the pid is stored. I measured SIGABRT in 4 of 270 runs of the file under heavy CPU load, and in every run when bun run is held for 100 ms after the spawn.
  • The cases now wait for a forwarded SIGUSR2 before they send SIGINT. With that, 0 of 270 runs hit it, and the forced runs pass.

The bug has a patch in #38926 (restore the saved mask with SIG_SETMASK) and a second guard in #38877. It is also what makes test/cli/run/run_command.test.ts fail with Received: "SIGABRT" now and then (builds 124378, 124477, 124545, 124556, 124759, 125044, 125184). The test runner does not retry a failure that prints SIGABRT.

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

The six "kills on SIGINT" cases installed unpinned vite from
registry.npmjs.org in beforeAll. When npm could not serve one of the
dependencies, the install exited 1 and all six cases failed.

The cases now install a local file: package. Its bin prints a line and
then idles with no SIGINT handler. bun install still links
node_modules/.bin, and the test contacts no registry.
A failed case left bun and its child running. The cases now bind the
process with `await using`. The kill signal is SIGKILL, because a bun
that ignores SIGINT can ignore SIGTERM too. The wait for its exit would
then turn the failed assertion into a test timeout.
`bun run` has the pid of its child only when the spawn returns. A
SIGINT that comes earlier takes the pending-signal path. That path has
a bug that ends in SIGABRT, and the local bin prints soon enough after
the spawn for the cases to reach it.

Each POSIX case now sends SIGUSR2 first and waits for the answer of the
bin. The answer proves that `bun run` has the pid. The wait also stops
with a clear error when bun ends before the expected line.
@robobun
robobun force-pushed the robobun/cdb5fe39/ctrl-c-test-local-bin branch from 8d0f238 to 594b8a2 Compare October 18, 2026 15:42

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