Conversation
All 330 packages/agents test files now run under Bun's native test runner. The workspace `test` and `test:ci` scripts invoke a new workspace-local runner; `test:vitest` is retained as the Vitest fallback. There is no exclusion list — every discovered file must pass. Test-count parity is exact: 3728 test cases across 330 files under both runners, 0 failures and 0 skips. Runner (packages/agents/run-bun-tests.ts): - Runs each file in its own `bun test` process, because Bun's mock.module registry is process-wide and 69 agents files register module mocks. - Passes --timeout 30000 explicitly. Bun 1.3.14 ignores a `[test] timeout` key in bunfig.toml and silently uses its 5s default, which is what made the heavier suites fail; the flag restores the Vitest testTimeout budget. - Uses a sliding worker pool rather than fixed batches, so a slow file cannot idle the other workers behind it. - Defaults to concurrency 4 (LLXPRT_AGENTS_TEST_CONCURRENCY overrides). The src/api/__tests__ suites build a real Agent per test and are far heavier than unit tests; oversubscribing pushes them past the timeout. Test-file fixes (34 files failed under Bun before this change): - Async vi.mock factories raced the shim's placeholder registration, so vi.mocked(binding) captured the real class. Moved the mock instances into vi.hoisted() and converted the factories to sync form. - Bare vi.mock automock deep-cloned ChatSession getters, which threw. Replaced with explicit sync factories. - Rewrote all four resolves.not.toThrow() call sites: Bun evaluates not.toThrow() against the resolved value rather than the settled state. - Corrected a mock that targeted the wrong module for logToolCall. - Added getConfig / getContinueOnFailedApiCall to mock objects whose real collaborators call them. - Adapted fake-timer sequencing where Bun and Vitest drain differently. Assertions are unchanged; every fix is runner-compatibility only. The pretest API-surface guard still runs and passes ahead of the Bun run.
F1: Replace fixed-count drain loops with deterministic eventLoop helpers (flushEventLoop/waitForCondition). Converted ~15 loops across 11 test files.
F2: Remove hardcoded StreamEventType in vi.mock factories across 11 files using dual-mode importOriginal pattern.
F3: Normalize vi.mock('./turn') across 12 client test files - convert async to sync dual-mode, standardize pendingToolCalls type annotation.
F4: Fix turn.idle-timeout permanently stuck fixture with controllable gate promise.
F5: Remove duplicated assertions in executor.termination-conditions.test.ts.
F6: Fix unreachable yield in subagent.runNonInteractive-term.test.ts.
F7: Strip trailing whitespace in bun-probe-failures.txt.
Runner correctness: - Merge each child's Bun JUnit report into the workspace junit.xml instead of emitting one pseudo test case per file. CI publishes packages/*/junit.xml, so the file-level summary was dropping every test name and duration that the Vitest reporter used to provide. The merged report now carries all 3728 cases, which also independently confirms the parity figure. - Settle a file's result from `close` after the wall-clock kill rather than from the timer itself. Settling on the timer freed the worker slot while the killed process was still alive, letting the pool exceed its concurrency cap exactly when the machine was already struggling. - Contain unexpected errors in the worker loop. `spawn` can throw synchronously under OS-level resource exhaustion (EMFILE); that previously became an unhandled rejection that killed the run without writing a report or returning a controlled exit code, discarding every result so far. - Size the worker pool at half the core count, clamped to [2, 4]. The previous flat cap of 4 left no headroom on a 4-vCPU CI runner, where each file is a fresh process that re-executes the whole agents module graph. - Rename SKIPPED_DIRECTORIES to PRUNED_DIRECTORIES and document why the entries exist. They prune build and dependency output (node_modules, dist, coverage, and dot-directories such as .stryker-tmp, whose in-place mutation backup would otherwise double-count every test) — exactly what the Vitest config pruned. They are not a test-exclusion list; the header comment previously implied discovery had no filtering at all. Docs: record the measured concurrency/reliability data and the JUnit merge in dev-docs/bun.md and the issue parity notes.
|
Important Review skippedToo many files! This PR contains 362 files, which is 62 over the limit of 300. To get a review, reduce the PR to 300 files or fewer by splitting it into smaller PRs or changing its base branch. Usage-priced reviews support at most 300 files. ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (10)
📒 Files selected for processing (362)
You can disable this status message by setting the 📝 WalkthroughWalkthroughThe agents workspace now runs tests through Bun with isolated processes, JUnit aggregation, compatibility mocks, and event-loop synchronization. Secure-store code now checks platform credential-store availability and uses encrypted-file fallback handling when platform storage is unavailable. ChangesAgents Bun test migration
Secure-store platform availability
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related issues
Possibly related PRs
Suggested labels: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Comment |
WalkthroughBefore this PR, the Tests
Documentation
Chore
Changes
Magnitude🎯 4 (XL) Related
Walkthrough generated by LLxprt PR Review. Planner issue: #2256 |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/agents/bunfig.toml`:
- Around line 7-15: Update the timeout explanation above the [test]
configuration to state that Bun 1.3.14 supports the [test].timeout setting and
that the command-line --timeout 30000 takes precedence. Remove the inaccurate
claim that Bun ignores the configuration key, while preserving the existing
preload configuration.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: a0396987-9f54-4fbf-bc99-dc41eeac8db2
⛔ Files ignored due to path filters (5)
dev-docs/bun.mdis excluded by!dev-docs/**dev-docs/test-runner-inventory.mdis excluded by!dev-docs/**project-plans/20260803issue2845/bun-probe-failures.txtis excluded by!project-plans/**project-plans/20260803issue2845/parity.mdis excluded by!project-plans/**project-plans/20260803issue2845/plan.mdis excluded by!project-plans/**
📒 Files selected for processing (40)
packages/agents/bunfig.tomlpackages/agents/package.jsonpackages/agents/run-bun-tests.tspackages/agents/src/agents/executor.execution.test.tspackages/agents/src/agents/executor.recovery.test.tspackages/agents/src/agents/executor.stream-idle-timeout.test.tspackages/agents/src/agents/executor.termination-conditions.test.tspackages/agents/src/agents/executor.test.tspackages/agents/src/api/__tests__/providerActivation.behavior.test.tspackages/agents/src/core/MessageStreamOrchestrator.modelinfo.test.tspackages/agents/src/core/MessageStreamOrchestrator.todoPause.test.tspackages/agents/src/core/__tests__/compression-boundary.test.tspackages/agents/src/core/chatSession.contextlimit.test.tspackages/agents/src/core/chatSession.promptEnvelopeEstimation.test.tspackages/agents/src/core/chatSession.runtime.streaming.test.tspackages/agents/src/core/chatSession.runtime.timeout.test.tspackages/agents/src/core/client-test-helpers.tspackages/agents/src/core/client.editor-context.test.tspackages/agents/src/core/client.hooks.test.tspackages/agents/src/core/client.ide-context.test.tspackages/agents/src/core/client.lifecycle.test.tspackages/agents/src/core/client.methods.test.tspackages/agents/src/core/client.model-profile.test.tspackages/agents/src/core/client.sendMessageStream-errors.test.tspackages/agents/src/core/client.sendMessageStream-invalid-stream.test.tspackages/agents/src/core/client.sendMessageStream-overflow-compression.test.tspackages/agents/src/core/client.sendMessageStream-overflow.test.tspackages/agents/src/core/client.sendMessageStream-thinking.test.tspackages/agents/src/core/client.sendMessageStream.test.tspackages/agents/src/core/coreToolScheduler.hookRestrictedTelemetry.test.tspackages/agents/src/core/subagent.buildParts.test.tspackages/agents/src/core/subagent.create.test.tspackages/agents/src/core/subagent.runNonInteractive-execution.test.tspackages/agents/src/core/subagent.runNonInteractive-term.test.tspackages/agents/src/core/subagent.runNonInteractive.test.tspackages/agents/src/core/subagent.stream-idle.test.tspackages/agents/src/core/turn.abort-timeout.test.tspackages/agents/src/core/turn.idle-timeout.test.tspackages/agents/src/core/turn.preRequestTimeout.test.tspackages/agents/src/test-utils/eventLoop.ts
OpenCodeReview — automatic reviews suspendedAutomatic OCR reviews are suspended for this PR after 2 of 2 automatic reviews. To get more reviews you can:
|
The agents shard failed on Linux CI while passing on macOS. Two causes. 1. `flushEventLoop()` yielded through `setImmediate`. On Linux a real `setImmediate` scheduled after the fake clock has already been advanced never fires, so every call placed after `vi.advanceTimersByTimeAsync(...)` hung until the 30s per-test budget expired. The helper now yields through a `MessageChannel` port message: a genuine macrotask that belongs to neither runner's timer subsystem, so it behaves the same on every platform with or without fake timers. 2. `subagent.stream-idle.test.ts` had lost the step that let the run finish. `vi.runAllTimersAsync()` cannot be used there — it also fires the 60-minute `max_time_minutes` watchdog and reports the very TIMEOUT the test exists to rule out — but replacing it with nothing left `runPromise` unsettled. It now yields to the real event loop, which resumes the generator without moving the fake clock. Both failure modes are covered by the existing assertions. Also reverted several single `await Promise.resolve()` calls that had been converted to `flushEventLoop()` without needing to be; a microtask turn was already sufficient and provably worked on both platforms. Runner: report signal-terminated children as "killed by signal <SIG>" rather than "exit code -1". Two files were reaped by a signal on the CI runner and the old message made that look like an ordinary non-zero exit.
Subscribing to 'abort' on a signal that has already aborted never fires, so dropping the explicit `signal.aborted` check left the stalled-stream fixture able to hang whenever the idle timeout won the race to abort before the generator reached addEventListener. Sibling fixtures in subagent.runNonInteractive-term and turn.abort-timeout already guard this way.
…loop Reverts the MessageChannel experiment: a port message is not delivered under Bun on Linux while fake timers are installed either, which turned the previous 30s per-test failure in turn.idle-timeout into a 120s per-file hang. Restores the captured real `setImmediate` and documents the actual constraint learned from CI — a real event-loop yield only works BEFORE the fake clock has been advanced, so anything that needs pending work to settle afterwards must go through the fake-timer API. `subagent.stream-idle` now nudges the clock by one second after resolving the stalled iterator. `runAllTimersAsync` cannot be used there because it also fires the 60-minute max_time_minutes watchdog and reports the very TIMEOUT the test exists to rule out; one second pumps the pending promise chain and stays nowhere near that limit.
Both live outside packages/agents and pre-date this migration; it is just the first thing to exercise them. Records the Linux container reproduction, the evidence for each root cause, what was ruled out, and the options with a recommendation. 1. Bun 1.3.14 segfaults calling into @napi-rs/keyring on a runner with no Secret Service. Two files die mid-run at the first test that resolves a credential. The repo's own Bun native-module smoke already avoids credential I/O for this module. 2. The shared compat shim's advanceTimerChunk crosses a real event-loop boundary per timer firing, making large fake-clock advances ~17x more expensive on Linux than macOS. Five tests that are byte-identical to main exceed the 30s budget as a result.
…ngages
A machine with no Secret Service — headless Linux, container, ssh session,
WSL — reports:
Couldn't access platform storage: PermissionDenied
`classifyError` matches error messages by substring, so that string hit the
`denied`/`permission` test and was classified DENIED. `SecureStore.get()` and
`set()` deliberately swallow UNAVAILABLE, NOT_FOUND and TIMEOUT and degrade to
the encrypted file, but rethrow everything else — so a routine machine with no
keyring backend surfaced a hard SecureStoreError instead of falling back. The
UNAVAILABLE remediation text describes precisely this case: "install a keyring
backend ... or allow encrypted fallback storage".
This is a product bug, not a test artifact. The shipped bin/llxprt is a POSIX
launcher that execs Bun, so a Linux user without a Secret Service reading a
provider key hit the throw rather than the intended degrade.
It surfaced as two agents suites dying on the Linux CI runner during the Bun
migration (#2845) — those tests were the first thing to exercise this surface
on a keyring-less machine, and they were right.
Classify "access platform storage" as UNAVAILABLE ahead of the generic
denied/permission test. Pinned by three behavioral tests covering read of an
existing value, a missing key, and a fresh write; all three verified failing
before the change.
Verified in a keyring-less Linux x86_64 container: capabilityGaps went 17/1 to
18/0 and both affected agents files now report 33 pass / 0 fail, with no test
file modified. packages/storage secure-store suite: 236 pass, 0 fail.
The classifier fix alone was not enough. On the Linux CI runner Bun does not
raise a catchable error from libsecret — it aborts the process:
panic(main thread): Segmentation fault at address 0x88
so there is nothing left to classify or degrade from. Probing availability by
calling the native module and catching the failure is therefore unsound on that
platform.
Check first instead. macOS and Windows ship a credential store as part of the
OS; Linux does not — there it is a D-Bus Secret Service, and a headless server,
container, ssh session or WSL frequently has no session bus at all. When
neither DBUS_SESSION_BUS_ADDRESS nor an XDG_RUNTIME_DIR bus socket is present
there is definitively no credential store, so createDefaultKeyringAdapter now
returns null before importing @napi-rs/keyring. Null is the established
"keyring unavailable" signal and makes SecureStore use its encrypted-file
fallback, which is what the design already intends for this case.
The check is a pure predicate taking platform, env and an existsSync-like
callback, so it is covered on every host rather than only on Linux.
Verified in a keyring-less Linux x86_64 container: the two agents suites that
were aborting the process now report 33 pass / 0 fail. packages/storage
secure-store suite: 243 pass, 0 fail. The SecureStore Backend (keyring) CI job
runs under dbus-run-session, which exports DBUS_SESSION_BUS_ADDRESS, so it
still exercises the native path.
The timer-looking failures were the same credential-store stall wearing a different hat, not a fake-timer or compat-shim defect. Records the measurements that disproved the shim theory (3000 timer firings cost 13ms on macOS and 228ms on Linux, nowhere near the 30s budget) and the evidence that all five previously-failing files now pass on Linux in 13s with no shim change.
A session bus on its own does not mean a credential store exists. CI proved it: GitHub's Ubuntu runners have a systemd user session, so the bus check passed, the native keyring was still loaded, and Bun still aborted the process at the same test. Providers advertise themselves with a D-Bus activation file named org.freedesktop.secrets.service, so its presence is what actually distinguishes "a keyring is installed" from "there is merely a bus". Linux now requires both a session bus and a discoverable activation file, searched across XDG_DATA_HOME (or ~/.local/share) and XDG_DATA_DIRS with the spec's default fallbacks. Verified rather than assumed: `dpkg -c gnome-keyring` ships /usr/share/dbus-1/services/org.freedesktop.secrets.service, so the SecureStore Backend (keyring) job — which apt-installs gnome-keyring and runs under dbus-run-session — still exercises the native path, while the agents shard, which installs no provider, degrades to the encrypted file. That the keyring job has to install gnome-keyring at all is itself evidence the base runner has no provider. A machine whose provider is not discovered degrades to the encrypted fallback rather than crashing, so the failure direction is safe. packages/storage secure-store suite: 246 pass, 0 fail. The five agents files that were failing on Linux: 53 pass, 0 fail.
The premise was wrong and the change broke working coverage. I assumed GitHub's Ubuntu runners had no Secret Service, and that the agents SIGILL was Bun entering libsecret on a machine with no provider. CI disproved it: the four tests in secure-store.native-keyring.test.ts exercise the real OS keyring with fallbackPolicy 'deny' and no skip guard, and they pass on that runner under Node. A working provider is therefore present, and the detection was suppressing the native keyring on machines that genuinely have one — it turned four passing tests red in the `rest` shard. That also relocates the real defect: @napi-rs/keyring works under Node and aborts the process under Bun on the same Linux machine, so this is a Bun/NAPI incompatibility rather than an absent daemon. Detecting absence cannot fix it, and needs a different approach and a decision about how the Bun runtime should treat the OS keyring on Linux. The classifier fix (59f37cf) is kept: it is independently correct, pinned by behavioral tests, and regressed nothing. packages/storage secure-store suite after the revert: 236 pass, 0 fail.
…t hypotheses, and the narrowed suspect list
Captured on a native arm64 container, where the crash reproduces in 1.7s and ptrace works. x86_64 on Apple Silicon runs under emulation, which is both slow and breaks gdb, which is why earlier backtrace attempts failed. The fault is on the main thread inside @napi-rs/keyring's native library, with a corrupted unwind; on x86_64 the faulting address is 0x88, a small struct offset consistent with a null pointer dereference. info sharedlibrary names the three addons behind process_dlopen(3): ast-grep, sharp/libvips and keyring. Also clears two more hypotheses: sharp/libvips interference (imported and exercised in both orders) and keyring thread-safety under concurrent reads (5 rounds of 16 parallel getPassword calls).
@napi-rs/keyring 1.3.0 is already the newest release (2026-04-30) and Bun 1.3.14 is already the newest stable. Bun canary 1.4.0 reproduces the crash identically, so it is not fixed upstream either. No matching issue exists in Brooooooklyn/keyring-node (8 issues, none mention Bun) or oven-sh/bun (no napi segfault or keyring reports). The crash is unreported on both sides.
The important negative: the synchronous Entry API crashes too, in the agents context. That was the most attractive quick fix (it schedules no libuv async work) and it does not help, so this cannot be resolved by choosing a different entry point on the addon. Also cleared: multiple independent adapters (sequential, concurrent and re-entrant, matching the tool-keys plus machine-secret pairing the real code uses), AsyncLocalStorage context tracking, all three native addons loaded and initialised together, and a duplicate or mismatched glib/gio/libsecret - a /proc/self/maps diff shows no such libraries mapped at all. Importing the agents graph is harmless and importing the keyring is harmless; it is the first credential call afterwards that dies. That state has resisted every attempt to synthesise it from parts.
@napi-rs/keyring uses dbus-secret-service on Linux, which binds the libdbus C library. libdbus's mainloop uses select(), whose fd_set holds only FD_SETSIZE (1024) descriptors. When the D-Bus connection gets a descriptor >= 1024, FD_SET writes past the end of the fd_set and corrupts memory. None of this is our code. It only looked Bun-specific because Bun has more descriptors open than Node when the keyring is first used. Minimal reproducer, no llxprt code involved: open 1200 descriptors, then call AsyncEntry.getPassword(). Sweep holding everything else constant - 100 fds survives, 800 survives, 1200 segfaults, 4000 segfaults - which brackets 1024. Backend A/B built from source at keyring-node@3e7bcc4: dbus-secret-service segfaults, keyutils-only passes 18/0, and restoring the libdbus path brings the crash back. Rebuilding with strip="none" and debug=true shows a branch through a NULL pointer with the frame chain running into unmapped memory - a smashed stack, consistent with the out-of-bounds FD_SET. Also ruled out: missing NAPI symbols, keyring_core::set_default_store, GC pressure, and concurrent-read thread safety. Suggested upstream fix documented: keyring-node already declares the pure-Rust secret-service v5 crate (zbus, poll-based, no FD_SETSIZE ceiling) but does not use it.
The bug is not in keyring-node and not in Bun. keyring-node enables the
"vendored" feature, which chains to libdbus-sys and compiles libdbus from
source. libdbus picks its polling implementation at compile time:
#if defined(HAVE_POLL) && !defined(BROKEN_POLL) // dbus-sysdeps-unix.c:3146
and otherwise falls back to a select() path that calls FD_SET, which has
undefined behaviour for descriptors >= FD_SETSIZE (1024).
libdbus-sys/build_vendored.rs enables HAVE_EPOLL and DBUS_HAVE_LINUX_EPOLL but
never enables HAVE_POLL - grep for enable("HAVE_POLL") returns zero matches. So
every vendored build takes the select() path and corrupts memory whenever the
D-Bus connection lands on a high descriptor.
Adding one line next to the existing HAVE_EPOLL call fixes it. Verified by
rebuilding with vendored still enabled and libdbus-sys redirected through
[patch.crates-io]: the reproducer now survives at 100, 1200 and 4000
descriptors, capabilityGaps passes 18/0 and subagentOrchestrator-runtime passes
15/0. Both previously crashed.
Storage roots are already redirected for tests, but the OS credential store lives outside them and was never covered. Any agents suite that built a real Agent and touched tool-key storage therefore performed a genuine read against the developer's actual keychain - poor hygiene independent of any crash. Add LLXPRT_TEST_DISABLE_OS_KEYRING, honoured by createDefaultKeyringAdapter before it imports @napi-rs/keyring, and set it from the agents Bun test preload. SecureStore then uses its encrypted-file fallback inside the already isolated storage root. The marker is deliberately separate from LLXPRT_TEST_STORAGE_ISOLATED because the storage workspace's own suites isolate their roots while still needing the genuine keyring, so one flag cannot serve both. This also sidesteps the libdbus FD_SETSIZE memory corruption documented in project-plans/20260803issue2845/keyring-root-cause.md, which was aborting capabilityGaps.integration.spec.ts and subagentOrchestrator-runtime.test.ts on Linux. Verified in a Linux container against the UNPATCHED upstream keyring binary: both suites pass (18 and 15 tests) where they previously segfaulted. No test is skipped or dropped - the same assertions run, against the encrypted-file backend. The real keyring remains covered by the dedicated secure_store_backend CI job, which runs vitest.config.native-keyring.ts with gnome-keyring installed. packages/storage secure-store suite: 238 pass, 4 skipped, 0 fail.
# Conflicts: # dev-docs/test-runner-inventory.md # packages/agents/bunfig.toml
Merging origin/main brought in the providers migration and one new agents test file, src/core/CompressionProfileResolver.proxyKeyStorage.test.ts, which main introduced as Bun-only - it is in the Bun manifest and excluded from the Vitest selection. The agents workspace is therefore 331 files under Bun and 330 under Vitest, with Bun running a strict superset. Resolved both merge conflicts: - packages/agents/bunfig.toml: main added a shim-only preload; kept this branch's superset, which also preloads the storage-isolation setup the migration needs. - dev-docs/test-runner-inventory.md: took main's rows (it migrated providers and updated cli) and kept the agents row, now 331/331 Bun-native. Restored the agents section under "Fully migrated workspaces", which the merge had dropped, and corrected the manifest section to describe why three agents entries stay there. Verified after the merge: npm run test --workspace packages/agents runs the pretest API-surface guard (PASS) and then Passed 331/331 test files, exit 0. Merged JUnit root element reads tests="3730". Vitest fallback: 330 files, 3728 passed, 0 failed, 0 skipped. Build exits 0.
Summary
Migrates the entire
packages/agentsworkspace to Bun's native test runner, and removes Vitest from the workspace's test sources entirely. All 331 test files run under Bun; nothing is excluded, skipped, or deferred.Baseline note: the issue quotes 348 files from
dev-docs/test-runner-inventory.md. The real count at the branch point was 330; the inventory figure predated later consolidation. Mergingmainadded one more agents test file, so the workspace is now 331. The inventory is corrected in this PR.Acceptance criteria
Test (ubuntu-latest) [agents]→Passed 331/331 test filestest/test:ciuse Bunpackages/agents/package.json.test:vitestis gone — see "Vitest removal" belowpretestAPI-surface guard still runs and passesPASS: agents API-surface report matches expected snapshot.precedes the Bun runmainresolves.not.toThrow()rewrittengrep -rn "resolves.not.toThrow" packages/agents→ no matchesagentsshard invokes the workspacetest:ciscript, which is now Bunproject-plans/20260803issue2845/parity.mdThe runner
packages/agents/run-bun-tests.tsdiscovers everysrc/**/*.{test,spec}.{ts,tsx}file and runs each in its ownbun testprocess. Per-file processes are required, not merely preferred: Bun'smock.moduleregistry is process-wide, and 69 agents files register module mocks.Two Bun behaviours had to be worked around, both verified empirically:
[test] timeoutinbunfig.toml. Verified with a probe test that sleeps 8s: it fails viabunfig.tomland passes with--timeout 30000on the command line. The runner passes it explicitly so the workspace keeps the 30s budget it had under Vitest.LLXPRT_AGENTS_TEST_CONCURRENCY. It is a sliding worker pool rather than fixed batches.Each child writes its own Bun JUnit report and the runner merges them, so CI keeps per-test names and durations.
Vitest removal
All 345 test and helper files now import the test API from
packages/agents/src/testApi.tsinstead of fromvitest.test:vitestis removed andvitest/globalsis out of the agents tsconfig.Importing
bun:testdirectly produced 484 lint errors, because Bun's declarations describe Bun's API while these suites target the augmented API thattest-setup/augment-bun-vi.tsinstalls at runtime. The facade corrects exactly two things, both matching real behaviour:vi.mockreturnsvoid— the shim registers synchronously and no caller consumes the result (fixed 323no-floating-promises).rejects/.resolvesmatchers return promises — they are awaitable and every call site awaits them (fixed 159await-thenable)Both are expressed as mapped types over Bun's own types; none of Bun's API is restated. Lint is back to 0.
src/testApi.tssits at the top ofsrc/rather than undersrc/test-utils/so the import specifier stays short enough to fit on one line — the longer path wrapped imports across eight lines and pushed two suites past the 800-line cap. The cap was not raised.Two guards were updated for the new entry point, both deliberately and narrowly:
scripts/check-agents-api-surface.tsre-adds Bun's declarations viafiles(whichexcludedoes not filter), since its temp config replaces the source file list.vitestimport — matched narrowly, not by opening../generally.The public API surface is unchanged at 189 exported names.
Test isolation: the OS keyring
Storage roots were already redirected for tests, but the OS credential store lives outside them, so any agents suite that built a real Agent and touched tool-key storage performed a genuine read against the developer's actual keychain.
LLXPRT_TEST_DISABLE_OS_KEYRING, set by the agents Bun preload and honoured bycreateDefaultKeyringAdapterbefore it imports@napi-rs/keyring, makes SecureStore use its encrypted-file fallback inside the isolated root. It is deliberately separate fromLLXPRT_TEST_STORAGE_ISOLATED, because the storage workspace's own suites isolate their roots while still needing the genuine keyring.This also sidesteps a memory-corrupting crash in the credential stack on Linux, which is root-caused in
project-plans/20260803issue2845/keyring-root-cause.md:@napi-rs/keyringvendors libdbus, whose vendored build omitsHAVE_POLLand so falls back toselect();FD_SETis undefined for descriptors>= FD_SETSIZE(1024) and writes out of bounds. Reported upstream as diwic/dbus-rs#522 with a one-line fix in diwic/dbus-rs#523.The real keyring remains covered by the dedicated
secure_store_backendCI job, which runsvitest.config.native-keyring.tswith gnome-keyring installed.Test-file changes
Every change is runner-compatibility only. No assertion was weakened, and each modified file was verified under Bun.
vi.mockfactories raced the compat shim's placeholder registration. Mock instances moved intovi.hoisted(); factories converted to a sync dual-mode form that preserves the real module.vi.mockautomock deep-clonedChatSessiongetters, which threw. Replaced with explicit sync factories that still spread the real module, so the realStreamEventTypeenum is preserved rather than hardcoded.resolves.not.toThrow()rewritten at all four call sites — Bun evaluatesnot.toThrow()against the resolved value rather than the settled state.logToolCall; mock collaborators were missing methods the real code calls.src/test-utils/eventLoop.ts, documented as a pair:waitForConditionspins event-loop turns for promise-driven work, whilewaitForConditionInRealTimeanddelayRealTimepoll wall-clock time on timers captured at module load, for timer-driven work. Turn-spinning cannot advance a real watchdog, and a baresetTimeoutis not guaranteed to be the real one afteruseRealTimers().The CI-only stream-idle failures
Four tests timed out only on the GitHub runner while passing on macOS, arm64 Linux, and a fully synced x86_64 container on the same Bun build. Temporary trace markers were pushed to make CI report the failing step, which identified two distinct defects:
Fake timers are not active— an earlier test in the file switches to real timers and Bun does not re-arm them inbeforeEach.getTimerCount()threw, which aborted the test before it released its stalled stream; an async generator parked on an unresolvedawaitcannot be returned, so the consumer hung to its 30s budget. That is why it only ever surfaced as a timeout and depended on test ordering.process.stderr.writeperforms I/O, and that tick was what let the pipeline progress. Those yields are now explicit rather than an accident of logging.The stalls are now releasable rather than never-settling promises.
Verification
CI on this head: 40 checks pass, 0 fail, including
Test (ubuntu-latest) [agents]→Passed 331/331 test files, bothSecureStore Backendjobs, all other shards, E2E, CodeQL and the tmux UI test.Local, from the repo root — all exit 0:
npm run format,npm run typecheck,npm run lint,npm run lint:eslint-guard,npm run build.Vitest and Stryker are gone from this workspace
packages/agentsnow has exactly one test runner. Removed with the migration:vitest.config.tsand thevitestdevDependencytest:vitestfallback scriptstryker.conf.json, thetest:mutation:apiscript, and both@stryker-mutator/*devDependenciesThe Stryker gate went because it could not survive the migration: Stryker has no
Bun runner, and its Vitest runner cannot execute suites that import
bun:test.That was verified directly, not assumed — running Vitest against an agents suite
fails to resolve
bun:test. The gate was not wired into CI.No test was deleted. The suite is still 331 files, all running under
bun test.One test was translated rather than left broken:
buildOrder.determinism.spec.tsreadvitest.config.tsand asserted its aliasmapped the public root to
index.tsrather than a stale dist artifact. Under Bunthat guarantee comes from the tsconfig
pathsmapping, so it now asserts that —same intent, same protection against stale dist.
The boundary guard's allow-list was also tightened: it still permitted a bare
vitestimport, which would have let a driver reach a runner the workspace nolonger has. Removing that clause makes the guard stricter.
Fixes #2845