Skip to content

[CI] Run Vitest on Bun with Node runtime contracts - #25840

Merged
nwparker merged 6 commits into
mainfrom
nwparker/bun-tests
Oct 6, 2026
Merged

nwparker merged 6 commits into
mainfrom
nwparker/bun-tests

Conversation

@nwparker

@nwparker nwparker commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 20 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​282 $\color{#cf222e}{\Huge{\mathbf{−}}}$​80 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​202
Prod 18 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​382 $\color{#cf222e}{\Huge{\mathbf{−}}}$​36 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​346

ELI5

pnpm test now runs ordinary Vitest suites on Bun. Tests that depend on the application's Node runtime still run in Node, within the same invocation and report.

What Changed

  • Pin Bun 1.4.2 and install it in unit-test CI. Keep existing Vitest imports, discovery, assertions, shard selection, and timing reports.
  • Add a Node worker pool for SQLite publication, native PTYs, sockets, CLI subprocesses, and V8 memory contracts. Use JSON IPC with devalue because Bun and Node's binary serialization formats differ. Application child-process fixtures still launch Node.
  • Adjust fixtures for asynchronous completion, named builtin mocks, and DOM cleanup. Add runtime-boundary and IPC-option coverage; provide pnpm test:node as the full Node fallback. SSH host contracts continue using Node.

Why

This gives a smaller first step than rewriting thousands of tests for Bun's native runner. It preserves runtime contracts where Bun currently differs, including a SQLite close/checkpoint problem.

Benchmarks

Measured on an Apple M4 Max, macOS arm64, Node 24.20.0 and Bun 1.4.2. The merged implementation uses Vitest 4.1.11.

Previous Node configuration versus this PR: same source files and test identities, four workers, one warmup and five measured runs per runner, alternating runner order. Times are median process wall time, including startup, setup, reporting and exit. All 20 measured runs passed with identical case identities and outcomes.

Sample Previous Node config This PR: Bun + Node contracts Time reduction
100 shared-code suites / 1,068 passing cases 4.712s 4.425s 6.1%
40 DOM suites / 301 passing cases + one existing pending case 9.680s 8.719s 9.9%

DOM timings were noisy: Bun ranged from 7.867–14.905s, versus Node's 9.282–11.957s. Background TypeScript compilation was observed during this comparison. These samples do not establish a whole-suite or CI speedup.

A separate comparison using this PR's current Node fallback configuration, with the same samples and measurement method, recorded 5.188s → 4.412s for shared code (15.0%) and 9.916s → 7.742s for DOM (21.9%). Those numbers compare the two current runtime configurations, rather than the previous configuration, so the table above is the relevant before/after result.

Full-suite measurements:

Run Time and measurement Files / cases Result
Merged Bun/Vitest implementation, 16 workers 509.880s, Vitest-reported duration 11,046 / 108,358 Pass; zero unhandled errors
Original Node/Vitest configuration, remeasured later on the expanded source, 16 workers 542.496s, parent-process wall time 11,048 / 108,363 Pass; zero unhandled errors

These full runs differ in source, test count and timing boundaries; they are verification measurements, not a controlled full-suite before/after speedup. The merged run had 107,508 passing cases, one expected failure and 849 existing skips. The later Node baseline had 107,513 passing cases, one expected failure and the same 849 skips.

The five merged Linux ARM CI unit-test steps took 464s, 579s, 477s, 507s and 505s, excluding installation. Their artifacts covered all 11,110 files discovered at the newer merge base exactly once. No comparable pre-migration CI timing was established, so these are not a CI speedup claim.

Further optimization experiments are not included in this PR. The expanded goal is twice the full-suite speed: at most 271.248s against the later 542.496s passing baseline. Faster VM/native-runner experiments were rejected for failed assertions, worker crashes or incomplete execution. The latest interleaved full run collected all 11,048 files / 108,363 cases but took 578.475s and had one failed assertion. The 2× goal has not been achieved; none of those experimental runners has shipped.

Raw sample reports were retained locally in notes/bun-migration/benchmark/before-after/ and notes/bun-migration/benchmark/current-runners/; full-run reports are under notes/bun-migration/ and notes/bun-migration/performance/. These artifacts are not committed in this PR.

Linked Issue

N/A — maintainer-requested test-runner work.

Visual Proof

N/A — test tooling and fixtures only; no application UI or interaction change.

Testing

  • I manually tested these changes locally
  • Automated tests added/updated, or explained why not below

Verified on macOS arm64 with Node 24.20.0, Bun 1.4.2 and Vitest 4.1.11:

  • pnpm tc, changed-code quality gate, and commit-time lint/format checks pass.
  • All 11,046 CI unit suites are discovered exactly once: 10,380 Bun / 666 Node. The existing sequencer assigns every suite exactly once across three shards.
  • Runtime-boundary and child-process tests pass; 138 Node-pool suites pass at 16 workers. A deliberate failing Node-worker probe correctly reports its assertion through Bun.
  • Updated fixtures also pass with the Node runner. Both benchmark cohorts preserve test identities and outcomes across all measured runs.
  • ORCA_BACKGROUND_LAUNCH=1 ORCA_BALANCE_UNIT_SHARDS=1 pnpm test --maxWorkers=16 passes: 10,921 passing suites plus 125 existing skipped suites; 107,508 passing tests, one expected failure and 849 existing skipped cases. No unhandled errors. Test and timing reports cover all 11,046 files.
  • Timing artifacts report the selected Node version and Bun version, and reject mixed runtime evidence. Focused timing tests pass with both runners.
  • All five Linux ARM Bun-backed unit shards pass. Their timing artifacts cover all 11,110 files discovered against the current merge base exactly once, with Node 24.21.0 / Bun 1.4.2 and no unhandled errors. CI has 44 passing checks and seven existing conditional skips, together with platform persistence checks, Linux/Windows packaging, SSH hostile-host checks, static checks, typechecking and compatibility jobs.

Review

Reviewed worker cleanup, IPC serialization, builtin test guards, project exclusions, shard discovery, and failure reporting. Follow-up review adds a bounded shutdown deadline, explicit spawn/kill failure handling, and four real-child lifecycle tests; all CI checks passed again on commit 096b5ea17ade. The child-process wrapper preserves hidden Windows launches and disables shell execution. Native dependencies remain installed for Node.

Agent skill upstream boundary

  • Not applicable, or this change follows docs/reference/agent-skill-sharing-upstream-boundary.md and copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

Production runtimes and remote protocols are unchanged. SSH host, relay integration, mobile, cloud, and specialized runtime jobs retain Node. CI validation passed on Linux, macOS and Windows. The unchanged Node mobile browser job passed after one retry of a Chromium navigation timeout.

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

@coderabbitai

coderabbitai Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

📝 Walkthrough

Walkthrough

Vitest now runs through Bun by default and assigns selected runtime-contract tests to Node workers. A separate command runs Vitest entirely on Node. CI workflows, runtime setup, and timing reports now account for the Bun and Node versions. Tests were also updated for runtime-specific behavior, asynchronous setup, and child-process IPC serialization.

Priority: ⬇️ Low

Merge Risk: 🔵 Low · up to 096b5

The shutdown tests should exercise Node workers before merge. The identified gap affects test confidence rather than demonstrated application behavior.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 8 functions across 30 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ 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 primary change: running Vitest on Bun while retaining Node runtime contracts.
Description check ✅ Passed The description covers the required sections, explains the change and rationale, documents testing, and marks visual proof as not applicable. The maintainer-requested work note is consistent with the …
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
  • Autopilot · 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.

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

Actionable comments posted: 2


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 88bd27a7-a1f7-469f-9dcb-a8cb4c3a1b80
📥 Commits

Reviewing files that changed from the base of the PR and between 4de9f9f and f87da66.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (34)
  • .github/CONTRIBUTING.md
  • .github/workflows/ssh-hostile-hosts.yml
  • .github/workflows/unit-tests.yml
  • config/bun-version
  • config/oxlint-anti-slop.json
  • config/scripts/ci-unit-timing-import.mjs
  • config/scripts/ci-unit-timing-reporter.mjs
  • config/scripts/ci-unit-timing-reporter.test.mjs
  • config/scripts/hang-watchdog-process-metrics.test.mjs
  • config/scripts/run-vitest.mjs
  • config/scripts/ssh-hostile-hosts-workflow.test.mjs
  • config/scripts/vitest-bun-node-builtins.ts
  • config/scripts/vitest-node-runtime-boundary.test.ts
  • config/scripts/vitest-node-runtime-files.mjs
  • config/scripts/vitest-node-runtime-pool.ts
  • config/scripts/vitest-node-runtime-worker.mjs
  • config/scripts/vitest-runtime-boundary.test.ts
  • config/tsconfig.node.json
  • config/vitest.config.ts
  • package.json
  • src/main/daemon/daemon-scope-death-watch.test.ts
  • src/main/native-chat/agent-session-wire/structured-conversation-compaction.test.ts
  • src/main/runtime/runtime-remote-fetch-ref-maintenance.test.ts
  • src/relay/fs-handler-list-files-diagnosis-cancel.test.ts
  • src/relay/fs-search-cancellation.test.ts
  • src/relay/fs-search-diagnosis-cancellation.test.ts
  • src/renderer/src/components/UpdateCard.error-card.test.tsx
  • src/renderer/src/components/activity/activity-auto-mark-read-loop.react185.test.tsx
  • src/renderer/src/components/browser-pane/assemble-chrome/ssh-routed-browser-page-gate.test.tsx
  • src/renderer/src/components/right-sidebar/file-explorer-drag-scroll-marker.test.tsx
  • src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-stream-reconnect.test.ts
  • src/shared/child-process/process-spec.ts
  • src/shared/child-process/run-process.test.ts
  • src/shared/child-process/spawn-resolution.ts

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

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

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 615f7fe7-a54e-41a7-8615-568e7fcbc324
📥 Commits

Reviewing files that changed from the base of the PR and between f87da66 and 62fa8be.

📒 Files selected for processing (1)
  • src/main/ipc/terminal-render-desync-evidence.test.ts

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

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

🧹 Nitpick comments (1)
config/scripts/vitest-node-runtime-pool.test.ts (1)

32-32: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Use the launcher’s Node executable for pool workers.

The test launcher runs Vitest under Bun and passes the Node executable in ORCA_TEST_NODE_EXECUTABLE. The node-runtime project selects nodeRuntimePool, but options() replaces that value with Bun’s process.execPath. The pool then spawns Bun, so the shutdown tests do not exercise a Node worker. Keep the explicit missing-executable argument for the startup-failure test.

🐛 Suggested fix
-function options(executable = process.execPath): PoolOptions {
+function options(
+  executable = process.env.ORCA_TEST_NODE_EXECUTABLE ?? process.execPath
+): PoolOptions {

ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: ef563b14-5324-40d6-8ff9-a1570c23a525
📥 Commits

Reviewing files that changed from the base of the PR and between 0e39b05 and 096b5ea.

📒 Files selected for processing (5)
  • config/scripts/vitest-node-runtime-files.mjs
  • config/scripts/vitest-node-runtime-pool.test.ts
  • config/scripts/vitest-node-runtime-pool.ts
  • src/main/ipc/terminal-render-desync-evidence.test.ts
  • src/main/runtime/runtime-remote-fetch-ref-maintenance.test.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/main/ipc/terminal-render-desync-evidence.test.ts
  • config/scripts/vitest-node-runtime-files.mjs
  • config/scripts/vitest-node-runtime-pool.ts
  • src/main/runtime/runtime-remote-fetch-ref-maintenance.test.ts

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

@nwparker
nwparker merged commit 3ec38b8 into main Oct 6, 2026
51 checks passed
brennanb2025 added a commit that referenced this pull request Oct 6, 2026
Bun formats a method's stack frame without its class ("at step"), so the quit
test's caller check never matched, the step was never held, and both cases timed
out once CI ran Vitest on Bun (#25840). Only the drain step heals owed queue
bookkeeping, so the hold needs no caller check.
brennanb2025 added a commit that referenced this pull request Oct 6, 2026
… held card (#24660)

* fix(native-chat): keep a message accepted before a quit or crash as a held card

A send the host accepted while the agent was still starting, and never handed
over, was rejected unseen at quit or at the next open after a crash. The next
open now keeps a person's message (typed, or a launch's first prompt) as a
waiting card at the head of the queue, held until Resume, Send now, Edit or
Delete; quit no longer rejects it. Each submission records its source so a
restart knows which leftovers to keep. A direct send's replay answers from its
own record, never the queued arm. Cards shown without the queue capability
hide Turn off queueing and the steer chord.

* fix(native-chat): keep an unsent message as a held card at every close, not only a restart

The host now keeps a person's message it accepted and never handed over with one
rule wherever it can no longer hand it over: a quit or crash (settled at the next
open) and a close of the chat (tab close, worktree teardown, orchestration stop).

- The hold is card state: a new per-card hold_reason 'kept', published as the
  existing pausedReason, instead of a fake host_instance value. host_instance
  means the owner again, and the pause clause, adoption filter and /clear carry
  special cases are gone. A kept card holds the cards behind it until the
  person sends, edits or deletes it; a send_failed card still does not.
- A card hand-off rejected by a restart or a close returns as a kept card
  (rejectedDraftSettlement), so a person's next message can never release it.
- One hold function, parameterized by cause (hostRestarted / chatClosed),
  replaces the close path's plain rejection.
- The phone shows published cards and per-card holds whatever the queue
  capability says; only queueing a new send stays gated.
- source gains 'dispatch' for the orchestration preamble (still rejected); an
  unknown source is kept as written and never takes the legacy rule.
- Kept cards from earlier settlements stay ahead of a batch's new ones.

* test(native-chat): the dispatch preamble records its source

* fix(native-chat): skip a kept card like a failed one, and make a quit leave the queue as a crash does

- A kept card is held on its own, as a send_failed one is: the queue sends the
  cards behind it, and the "a message ahead needs attention" caption no longer
  appears behind it (desktop and phone).
- Quit disposes the queue's drain together with delivery, so it mints no
  hand-off that only the next process could settle.
- Only a Send the person asked for (origin client) that a restart or close cut
  short returns kept; the queue's own hand-off returns where it stood, under the
  restart's pause, as on main.
- Comments that said only a capable host gets cards or the card actions now say
  the capability gates only queueing a new send.

* refactor(native-chat): one dispose gate in the queue drain's step

* test(native-chat): the downgrade test names the older build's /clear exception

* fix(native-chat): re-check the drain's quit gate right before it appends a hand-off

A drain step already past its first check when quit begins no longer makes a
hand-off. Adds regression tests for that, for Send now on an ordinary card cut
short by a quit, and for the open-time repair deriving kept from the hand-off's
origin; fixes the pause and settlement comments that called a kept card one the
queue never passes.

* fix(native-chat): a card held on its own starts no restart pause for the others

After a second restart a kept (or send_failed) card is another process's, but it
waits for its own Send, so it no longer pauses every other card under
"Queue paused because Orca restarted".

* fix(native-chat): record on a kept send which card holds it, so no trace survives an Edit or Delete

The rejection row of a send the host kept as a card now names that card
(`keptAsQueuedMessageId`), in the same transaction that writes the card, and the
fold publishes it on the submission. The shared projection draws such a send
only as its card: once the card is sent, edited or deleted, neither the send
nor the sending desktop's local copy of it shows.

* chore(native-chat): leave the unused submission schema as main has it

No client parses published submissions with it (they arrive as typed frames,
and the host's history pages carry the field, as the Edit/Delete test reads);
listing the field put the file over its line budget.

* fix(native-chat): retire a kept send's local copy instead of only hiding it

The outbox reconcile and the send disposition drop an entry whose submission
the host rejected as kept as a card, as they already do for a Stop's
withdrawal, so the copy never comes back as "Not sent / Retry" once the
submission falls out of the loaded page. The projection reads the reconciled
outbox, so its separate filter goes.

* test(native-chat): move the queued-message rig's scripted provider into its own fixture

The rig fixture grew past the 300-line limit once main's changes merged in.

* fix(native-chat): hide a kept send by its own record, not by its card still existing

The transcript hid a rejected send while a queued card held it under its id, so the card's Edit or
Delete brought back a "Not sent" row. It now reads the send's own keptAsQueuedMessageId, which the
host records with the rejection, and the live card list is no longer threaded to the transcript or
the delivery notices.

* refactor(native-chat): move a sent message's row writes into their own journal collaborator

The journal store went past its line limit once main's ledger receipt joined this branch's
transaction hook. The submission and dispatch-transition writes, and what commits in their
transaction, now live in JournalSubmissionWriter; the store's methods delegate to it unchanged.

* fix(native-chat): list a kept send's card id in the submission schema

The schema drops keys it does not list, so a reader that kept a parsed submission would lose
keptAsQueuedMessageId and source, both persisted with the row. The submission schema and the
failure fact it shares with item bodies move to their own modules, with room for both fields.

* test(native-chat): match the transcript and outbox hook signatures main and the swap changed

* test(native-chat): import the journal types once in the queue-delivery test

* fix(mobile): a resend the host kept as a card shows no error and returns no text

The host answers a resend of a message it kept as a card with that
message's rejected submission, marked keptAsQueuedMessageId. The phone read
it as any rejection: "Message not sent" and the text back in the composer,
while the card showed the same text. It now answers like a queued send, as
the desktop's send disposition does: the id is spent, no error, and the card
holds the text.

* test(native-chat): pin that quit's first step stops the queue's hand-off

Quit now stops delivery, the queue drain included, at its first step
(stopDelivery), before teardown drains recovery. The drain-step quit test
runs from that step as well as from the flush.

* test(native-chat): give cards their source and store unknown sources as another build would

#25078 made a card's source required, so the tests that insert a card pass the
person's. The hold's unknown-kind and unreadable-source cases now rewrite the
stored row the way a newer build would leave it, instead of casting a type.

* refactor(native-chat): stop delivery and the queue drain in one line at quit

Main's #25159 left the host at its line limit; quit's stop now disposes both in one
expression instead of a block.

* test(native-chat): hold the drain step without reading the call stack

Bun formats a method's stack frame without its class ("at step"), so the quit
test's caller check never matched, the step was never held, and both cases timed
out once CI ran Vitest on Bun (#25840). Only the drain step heals owed queue
bookkeeping, so the hold needs no caller check.
@nwparker nwparker changed the title Run Vitest on Bun with Node runtime contracts [CI] Run Vitest on Bun with Node runtime contracts Oct 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant