test runner: fail dependent tests when a hook's done() receives an error - #33089
test runner: fail dependent tests when a hook's done() receives an error#33089robobun wants to merge 6 commits into
Conversation
|
Updated 1:32 PM PT - Jun 29th, 2026
❌ @robobun, your commit 21e6a3f has 1 failures in
🧪 To try this PR locally: bunx bun-pr 33089That installs a local version of the PR into your bun-33089 --bun |
|
Warning Review limit reached
Next review available in: 4 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (2)
WalkthroughAdds an Changesdone() callback error attribution
Docs formatting fixes
Suggested reviewers
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Comment |
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 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 `@src/runtime/test_runner/bun_test.rs`:
- Around line 840-856: The synchronous done(err) fallback in DoneCallback::call
loses the original callback cfg_data when ref_in is None because it uses
get_current_state_data(), which can return entry_data: None during concurrent
sequences. Update the DoneCallback path in bun_test.rs so the callback
phase/cfg_data is preserved for this case—either store the callback phase on
DoneCallback itself or attach the ref before invoking the callback—so the strong
branch uses the same cfg_data as the throw/rejection path.
In `@test/js/bun/test/test-error-code-done-callback.test.ts`:
- Around line 142-146: Shorten the comments in the test file to the invariant
the test is asserting, and remove the bug-history explanation and old-behavior
context. Keep the remaining comment tied to the relevant test helpers (the
done-callback lifecycle hook cases and the node:test before() coverage) and make
it fit within the 3-line limit while preserving only what the test protects.
- Line 196: The ordering test currently relies on a wall-clock timer via the a
setTimeout case, which can make the assertion flaky. Update the test data in
test-error-code-done-callback.test.ts to use an event-loop turn primitive
instead of setTimeout(fire, 5), so the existing ordering checks still exercise
macrotask behavior without depending on elapsed time. Keep the change localized
to the relevant test case and preserve the surrounding ordering semantics.
In `@test/js/node/test_runner/node-test.test.ts`:
- Around line 58-60: Remove the historical behavior context from the comment in
node-test.test.ts so it only states the invariant being verified. Update the
comment near the affected test to describe the current expected behavior without
mentioning Bun’s prior behavior or the “Unhandled error between tests” history;
keep that background out of the test and rely on the test name/assertions plus
the PR description instead.
🪄 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: ASSERTIVE
Plan: Pro
Run ID: b93356fd-8983-4698-ace8-872dd75b46a7
📒 Files selected for processing (7)
docs/guides/util/base64.mdxdocs/runtime/web-apis.mdxsrc/runtime/test_runner/DoneCallback.rssrc/runtime/test_runner/bun_test.rstest/js/bun/test/test-error-code-done-callback.test.tstest/js/node/test_runner/fixtures/06-failing-before-hook.jstest/js/node/test_runner/node-test.test.ts
|
CI status for this PR as of build 66884 (commit 21e6a3f): 281 jobs passed and the diff is green on every lane that ran it. The three red jobs are unrelated to this change:
I already re-ran CI once for the darwin infra failure and do not want to keep pushing empty commits, so this needs a maintainer to retry those lanes or merge over them. All review feedback is addressed and every thread is resolved. |
Drops the bun_test_done_callback change and its tests. It duplicated a subset of #33089 (and collides with #34041, which edits the same block), and on its own it made a hook's done(error) fail the hook only when the callback fired from a macrotask. This PR is the generation check alone; #33089's rerouting of done(error) through the stored ref picks up the retry case once it is rebased on it.
|
Related: #38876 makes Test cases (each is a `bun test` fixture run in a subprocess)Retry ( import { test } from "bun:test";
let firstDone: (err?: unknown) => void;
let attempt = 0;
test("retry", done => {
attempt++;
if (attempt === 1) {
firstDone = done; // times out
return;
}
firstDone(new Error("late error from attempt 1"));
setTimeout(() => {
console.log("attempt 2 body finished");
done();
}, 1);
}, { retry: 2, timeout: 500 });Expected: stdout contains Same thing without retry (the late error currently fails import { test } from "bun:test";
let firstDone: (err?: unknown) => void;
test("first", done => {
firstDone = done;
}, { timeout: 100 });
test("second", done => {
firstDone(new Error("late error from first"));
setTimeout(() => {
console.log("second body finished");
done();
}, 1);
}, { timeout: 500 });Expected: Concurrent (currently import { test } from "bun:test";
test.concurrent("fails", done => {
setTimeout(() => done(new Error("reported through done")), 1);
});
test.concurrent("passes", done => {
setTimeout(() => done(), 1);
});Expected: |
|
Heads-up: #39112 overlaps with this PR. It also routes |
…hooks Adds the beforeAll and afterAll hook kinds to the done(err) fixture (so all four hook kinds are pinned in the serial and both concurrent modes) and a node:test fixture whose suite-level before()/after() hooks throw, which node:test reports through those hooks' done callbacks. These are the cases #33089 was opened for; it is being closed in favor of this PR.
|
Closing in favor of #39112, which carries the same change (reporting Checked against a build of #39112: the four hook cases from this PR ( |
Problem
When a lifecycle hook fails through the
done(error)callback, the error is printed as anUnhandled error between testsand every test that depends on the hook is still counted as a pass.A synchronous
throwin the same hook reports0 pass, 1 fail. Every hook type has the same divergence (2 tests in the describe block):() => { throw }done => done(err)beforeAllbeforeEachafterEachafterAllnode:testwraps every hook (before,beforeEach,after,afterEach) in the done-callback form, so abefore()that throws undernode:testhits this path too. A broken test environment (database down, fixture missing) produces green pass/fail counts in any reporter that consumes them instead of the exit code.Cause
run_test_callbackreports a hook's synchronous throw or promise rejection throughBunTest::on_uncaught_exceptionwith the hook's ownRefDataValue, soExecution::handle_uncaught_exceptionfinds the sequence and marks it failed.bun_test_done_callbackinstead routeddone(error)through the VM's genericuncaught_exception. Underbun testthat lands injest::on_unhandled_rejection, which deliberately demotes any error observed while a hook is running toRefDataValue::Start("unhandled error between tests"), because a stray exception that happens to fire mid-hook may belong to anything. That demotion is wrong fordone(error): the user explicitly handed the error to the hook's own completion callback, but the failure never reached the hook's sequence.Fix
Report
done(error)throughBunTest::on_uncaught_exceptionwith the done callback's ownRefDataValue, the same way the promise-catch path (bun_test_then_or_catch) does. A staleRefDataValue(adone()called after the runner moved on) is still rejected by the existing validity checks inhandle_uncaught_exceptionand falls back to the unhandled-error report.run_test_callbackattaches the ref only after the callback returns, so a firstdone(error)can arrive without one in two ways: it was called synchronously inside the callback, or the callback was orphaned because its body threw (the throw returns before the attach).run_test_callbacknow marks the orphan in the same exhaustive block that otherwise attaches the ref, and an orphaneddone(error)falls back to the generic path so it is never blamed on whatever entry is active when it finally fires. An event-loop heuristic cannot make that distinction: a throwing body never reaches its own microtask drain, so a microtask orphan is drained inside the NEXT entry's callback, still inside the same runner step.This also removes a synchronous re-entry: the old path advanced the runner from inside the
done()call, while the non-errordone()path (and the comment documenting the intent) defers the advance to the next tick. Two lines change in the existingtest-error-code-done-callback.test.tssnapshot as a result: both were stack frames leaked from the previous test'sdone()call site into the next test's error stack, because the next test's body used to run from inside the previous test'sdone()frame.One known pre-existing gap is intentionally left alone: a synchronous
done(error)inside adescribe.concurrentgroup with more than one sequence still resolves to anentry_data: Nonephase and is reported as an unhandled error, exactly as before. Closing that needs the ref attached before the callback runs, a reordering ofrun_test_callbackbetter done on its own.Verification
test/js/bun/test/test-error-code-done-callback.test.ts: spawnsbun teston adone(err)variant of each hook type and asserts the pass/fail counts match the synchronous-throw variant; a separate test pins that an orphaned latedone(err)(a body that throws after handingdoneaway) is not blamed on an unrelated test, for both asetTimeoutand a microtask scheduling.test/js/node/test_runner/node-test.test.ts+fixtures/06-failing-before-hook.js: anode:testsuite whosebefore()throws reports0 pass, 1 fail.The hook tests fail on the unfixed build with the
2 pass, 0 fail, N error(s)counts above.jest-hooks.test.ts,done-async.test.ts,bun_test.test.ts,bun-test.test.ts(both),test-timeout-behavior.test.ts, and the concurrent suites all pass with the fix.