Skip to content

node:test: pass a done() callback to (t, done) => {} style tests - #32682

Closed
Sanjays2402 wants to merge 2 commits into
oven-sh:mainfrom
Sanjays2402:fix-bun-node-test-done
Closed

node:test: pass a done() callback to (t, done) => {} style tests#32682
Sanjays2402 wants to merge 2 commits into
oven-sh:mainfrom
Sanjays2402:fix-bun-node-test-done

Conversation

@Sanjays2402

@Sanjays2402 Sanjays2402 commented Jun 25, 2026

Copy link
Copy Markdown

What does this PR do?

Closes #32527.

Bun's node:test did not pass the second done callback argument when a test function declares two or more parameters (test('name', (t, done) => { ... })), unlike Node. As a result done resolved to undefined and callback-style tests could never signal completion.

This:

  • Detects callback-style tests via fn.length >= 2 in createTest.
  • Forwards the wrapper's endTest as the user's second done argument, and waits for it to be called instead of auto-completing synchronously (the existing test timeout still applies if done is never called).
  • Makes endTest idempotent so calling done() and also returning a non-Promise (or throwing) from the same function does not complete the test twice.
  • Updates the TestFn type to include the optional done parameter.

How did you verify your code works?

Added test/js/node/test_runner/fixtures/06-callback-tests.js and a runner case in test/js/node/test_runner/node-test.test.ts covering:

  • a sync callback-style test that calls done(),
  • an async callback-style test that calls done() from a setTimeout,
  • the existing (t) => {} sync signature, and
  • the existing async (t) => {} signature,

asserting all four invoke and the suite reports 0 fail. Run with bun test test/js/node/test_runner/node-test.test.ts.

When a user passes a test function with two or more parameters to
`test()` from node:test, Node.js binds a `done` callback to the second
argument and waits for it to be invoked before completing the test.
Previously Bun's node:test wrapper always called the user function with
just `(context)`, so `done` resolved to `undefined` and the user got
`TypeError: done is not a function` (issue oven-sh#32527).

This commit:
- Detects callback-style tests via `fn.length >= 2` in `createTest`.
- Forwards the wrapper's `endTest` (which calls the bun:test `done`
  parameter once it is wired up by the runtime) as the user's second
  argument.
- Makes `endTest` idempotent so calling `done()` and also returning a
  non-Promise from the same function does not complete the test twice.
- Skips the auto-end fall-through for callback-style synchronous returns
  so the runner waits on `done` (or its timeout) instead of marking the
  test passed immediately.
- Extends the `TestFn` type to reflect the optional `done` parameter.

A new fixture (06-callback-tests.js) exercises the sync, async, and
plain (t)/async (t) signatures, and a matching case has been added to
node-test.test.ts.

Fixes oven-sh#32527

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

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

node:test now detects callback-style test functions by arity, passes a done callback when requested, and keeps completion idempotent. Fixtures and integration tests cover deferred completion, existing signatures, and callback errors.

Changes

node:test callback completion

Layer / File(s) Summary
Callback contract and guard
src/js/node/test.ts
TestFn accepts an optional done argument, and endTest becomes idempotent.
Callback execution paths
src/js/node/test.ts
runTest passes endTest to callback-style tests, forwards synchronous throws, and suppresses promise auto-completion for callback tests.
Callback fixture coverage
test/js/node/test_runner/fixtures/06-callback-tests.js
Adds callback, deferred, non-callback, and idempotent completion cases.
Callback error and integration coverage
test/js/node/test_runner/fixtures/07-callback-done-error.js, test/js/node/test_runner/node-test.test.ts
Verifies successful callback completion and failure reporting from done(new Error("boom")).
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly names the node:test done() callback change and matches the main behavior in the PR.
Description check ✅ Passed The description follows the template and covers what changed plus verification, though the verification section is brief.
Linked Issues check ✅ Passed The changes add Node-style done() support for two-parameter node:test callbacks, matching issue #32527's requirements.
Out of Scope Changes check ✅ Passed The diff is focused on callback-style node:test support and related coverage, with no clear unrelated changes.

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: 3

🤖 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/js/node/test.ts`:
- Around line 698-700: The promise-handling in the test runner is incorrectly
completing callback-style tests when useCallback is true, so result resolution
should not call endTest in that mode. Update the logic around the result
instanceof Promise check in the test execution flow so only non-callback tests
are finished by promise resolution, and keep completion for callback-style tests
tied solely to done()/endTest. Refer to the test runner’s promise branch and the
useCallback condition to locate the fix.

In `@test/js/node/test_runner/fixtures/06-callback-tests.js`:
- Around line 13-19: The callback-style async fixture in the test is still
timer-based, so it does not reliably prove that runTest waits for done() before
completing. Update the test in 06-callback-tests.js to use an observable
completion sentinel or teardown assertion tied to the callback path in callback
test resolves with done() asynchronously, so the test fails if completion
happens before done() runs. Remove the setTimeout-based delay and make the
assertion depend directly on the callback ordering instead.

In `@test/js/node/test_runner/node-test.test.ts`:
- Around line 56-61: The current node test runner coverage only exercises the
successful done() path, so add end-to-end tests for the new callback error and
idempotency behavior. Extend the existing node-test suite around runTests and
the 06-callback-tests.js fixture with a case where a test calls done(new
Error("boom")) and asserts the failure output, plus a mixed-completion case that
calls done more than once or combines done with another completion path to
verify the ended guard suppresses duplicate completion handling. Use the
existing node-test.test.ts runTests helper and the callback fixture names to
keep the new cases alongside the current done() callback coverage.
🪄 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: 7eccdad4-332e-4b17-bbaa-573b58899141

📥 Commits

Reviewing files that changed from the base of the PR and between ed700c2 and 173c944.

📒 Files selected for processing (3)
  • src/js/node/test.ts
  • test/js/node/test_runner/fixtures/06-callback-tests.js
  • test/js/node/test_runner/node-test.test.ts

Comment thread src/js/node/test.ts
Comment on lines +13 to +19
// Async-but-callback-style test: schedules done() in a setTimeout.
test("callback test resolves with done() asynchronously", (t, done) => {
setTimeout(() => {
callCount++;
done();
}, 5);
});

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.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Make the async callback case observable instead of timer-based.

This fixture still passes if runTest incorrectly auto-completes the test before done() is called, because the timer fires later and callCount still reaches 4. Please make the async path assert completion ordering directly (for example with a teardown/sentinel that only flips inside the callback) so it actually exercises the new callback-waiting branch.

As per coding guidelines, "Do not use setTimeout in tests; instead, await the actual condition to be met rather than testing time passing."

🤖 Prompt for 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.

In `@test/js/node/test_runner/fixtures/06-callback-tests.js` around lines 13 - 19,
The callback-style async fixture in the test is still timer-based, so it does
not reliably prove that runTest waits for done() before completing. Update the
test in 06-callback-tests.js to use an observable completion sentinel or
teardown assertion tied to the callback path in callback test resolves with
done() asynchronously, so the test fails if completion happens before done()
runs. Remove the setTimeout-based delay and make the assertion depend directly
on the callback ordering instead.

Source: Coding guidelines

Comment on lines +56 to +61
test("should pass a done() callback to tests whose function takes two parameters", async () => {
const { exitCode, stderr } = await runTests(["06-callback-tests.js"]);
expect({ exitCode, stderr }).toMatchObject({
exitCode: 0,
stderr: expect.stringContaining("0 fail"),
});

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.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Add end-to-end coverage for the new callback error/idempotency paths.

This only checks the all-pass case. The runtime change also introduced new callback-specific behavior for done(error) and duplicate completion suppression, but those branches are still untested here. A tiny companion fixture for done(new Error("boom")) and one mixed-completion case would lock down the new ended guard.

🤖 Prompt for 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.

In `@test/js/node/test_runner/node-test.test.ts` around lines 56 - 61, The current
node test runner coverage only exercises the successful done() path, so add
end-to-end tests for the new callback error and idempotency behavior. Extend the
existing node-test suite around runTests and the 06-callback-tests.js fixture
with a case where a test calls done(new Error("boom")) and asserts the failure
output, plus a mixed-completion case that calls done more than once or combines
done with another completion path to verify the ended guard suppresses duplicate
completion handling. Use the existing node-test.test.ts runTests helper and the
callback fixture names to keep the new cases alongside the current done()
callback coverage.

When a (t, done) => {} test also returns a promise (e.g. an async
callback-style body), completing the test on promise resolution could
finish it before done() was ever called, contradicting the callback
contract. Only auto-complete on resolution when the test is NOT
callback-style; a rejected promise is still surfaced as a failure.

Make the async-callback fixture prove ordering instead of relying on a
timer: a follow-up test asserts a sentinel that is only set inside the
deferred done(), so a premature completion would fail. Add a done()
idempotency case and a separate fixture exercising done(error) so the
failure path and the double-completion guard are covered end to end.

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
test/js/node/test_runner/fixtures/06-callback-tests.js (1)

1-61: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing coverage for synchronous throws in callback-style tests.

The PR objectives state that runTest "forwards synchronous throws" for callback-style tests, but no test here exercises a callback-style test body that throws synchronously (rather than calling done(error) or rejecting a promise). Consider adding a case like test("...", (t, done) => { throw new Error("sync"); }) to lock down that path alongside the existing done(error) coverage in 07-callback-done-error.js.

🤖 Prompt for 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.

In `@test/js/node/test_runner/fixtures/06-callback-tests.js` around lines 1 - 61,
Add a callback-style test case in the fixture that throws synchronously from its
`(t, done)` body without calling `done()`, and assert the runner reports the
thrown error through the expected test-failure behavior. Keep the existing
invocation count and exit assertion consistent, and place it alongside the
current callback-style completion tests.
🤖 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 `@test/js/node/test_runner/fixtures/06-callback-tests.js`:
- Around line 19-28: Replace the setTimeout call in the callback test with
setImmediate, preserving the delayed callback body that increments callCount,
sets sawDone, and invokes done(). Keep the promise resolution and macrotask
boundary behavior unchanged without relying on elapsed time.

---

Outside diff comments:
In `@test/js/node/test_runner/fixtures/06-callback-tests.js`:
- Around line 1-61: Add a callback-style test case in the fixture that throws
synchronously from its `(t, done)` body without calling `done()`, and assert the
runner reports the thrown error through the expected test-failure behavior. Keep
the existing invocation count and exit assertion consistent, and place it
alongside the current callback-style completion tests.
🪄 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: 7766eccc-3280-4b77-8b4d-7e19e04bd87b

📥 Commits

Reviewing files that changed from the base of the PR and between 173c944 and afca1a9.

📒 Files selected for processing (4)
  • src/js/node/test.ts
  • test/js/node/test_runner/fixtures/06-callback-tests.js
  • test/js/node/test_runner/fixtures/07-callback-done-error.js
  • test/js/node/test_runner/node-test.test.ts

Comment on lines +19 to +28
test("callback test waits for done() even when the body returns a promise", (t, done) => {
// Returning a resolved promise must NOT complete the test; only done() may.
return Promise.resolve().then(() => {
setTimeout(() => {
callCount++;
sawDone = true;
done();
}, 5);
});
});

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.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Replace setTimeout with a timer-free macrotask deferral.

Line 22 uses setTimeout(..., 5) to force an async gap between promise resolution and done(). As per path instructions, test/**: "CRITICAL: Do not use setTimeout in tests... you are not testing the TIME PASSING, you are testing the CONDITION." setImmediate achieves the same macrotask boundary without an arbitrary timer.

♻️ Proposed fix
   return Promise.resolve().then(() => {
-    setTimeout(() => {
+    setImmediate(() => {
       callCount++;
       sawDone = true;
       done();
-    }, 5);
+    });
   });
🤖 Prompt for 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.

In `@test/js/node/test_runner/fixtures/06-callback-tests.js` around lines 19 - 28,
Replace the setTimeout call in the callback test with setImmediate, preserving
the delayed callback body that increments callCount, sets sawDone, and invokes
done(). Keep the promise resolution and macrotask boundary behavior unchanged
without relying on elapsed time.

Source: Path instructions

@robobun

robobun commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Thank you for this contribution!

This was fixed in #32631: invokeWithDoneCallback now passes a done callback when the test function declares two parameters. See src/js/node/test.ts on current main.

Closing as already fixed. Thanks again for the investigation and patch!

@robobun robobun closed this Jul 31, 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.

node:test does not implement done

2 participants