node:test: subtests, plan, mock timers, getTestContext (+11 tests) - #32631
Conversation
|
Updated 1:05 AM PT - Jul 17th, 2026
✅ @cirospaciari, your commit e8ab4ad77b9ecb4fffae8da4c616e56e21e201e6 passed in 🧪 To try this PR locally: bunx bun-pr 32631That installs a local version of the PR into your bun-32631 --bun |
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
|
Found 6 issues this PR may fix:
🤖 Generated with Claude Code |
|
This PR may be a duplicate of:
🤖 Generated with Claude Code |
WalkthroughChangesnode:test harness expansion
DNS IP validation
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 10
🤖 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/internal/test_runner/mock_timers.ts`:
- Around line 745-749: The runAll() method incorrectly uses peekBottom() to find
the maximum runAt time in a min-heap, but peekBottom() does not reliably return
the longest timer in a min-heap structure, causing runAll() to potentially stop
early and miss pending timers. Additionally, this approach can compute a
negative delta when only immediate timers are present, causing tick() to fail
argument validation. Instead of using peekBottom(), find the actual maximum
runAt value from all timers in the execution queue, and ensure the calculated
delta is non-negative before passing it to the tick() method to handle edge
cases like immediate-only queues correctly.
- Around line 107-117: The issue is that after reset() is called, previously
returned Timeout objects retain their stale priorityQueuePosition values. When
those Timeout objects are later disposed via close() or Symbol.dispose, it
causes removeAt() to be invoked with invalid indices that can drive `#size`
negative and corrupt the heap. To fix this, in the reset() method, before
clearing the heap array, iterate through all Timeout handles that are being
removed and clear their priorityQueuePosition property to undefined so that
subsequent close() or dispose() calls won't attempt to remove them from the
heap.
- Around line 684-691: Duplicate entries in the internalOptions.apis array can
cause the `#storeOriginal`*() methods to capture mocked descriptors instead of
original ones, breaking restoration on reset(). Add validation to ensure no
duplicate API names exist in the internalOptions.apis array before assigning it
to this.#timersInContext. This can be done by converting the array to a Set and
comparing sizes, or by tracking seen APIs during the existing validation loop
and throwing an error if a duplicate is encountered.
In `@src/js/node/test.ts`:
- Around line 217-231: The methods in getAccessValue and mockImplementationOnce
directly call Map.prototype methods (.has(), .get(), .delete(), .set()) on the
this.#onceValues Map object, which can be overridden by users and break the
functionality. Replace all Map access and mutation calls on this.#onceValues
with Bun's private Map intrinsics (using the $ prefix) in both the
getAccessValue method (where .has(), .get(), and .delete() are called) and the
mockImplementationOnce method (where .set() is called). Apply the same
tamper-resistant pattern to the other internal registries mentioned in lines
540-578.
- Around line 1147-1149: The createHook function is accepting and returning hook
options like signal and timeout from parseHookArgs, but these options are being
silently dropped by all callers who only extract the fn property, making
unsupported options accepted without any effect or error. Either wire these
options into the actual hook execution logic throughout the codebase to make
them functional, or reject them upfront by checking if options contains signal
or timeout properties and throwing an error indicating these options are not
supported, rather than silently accepting and ignoring them.
- Around line 171-180: The restore logic is spreading an accessor descriptor's
get/set properties along with a value property, which creates an invalid
descriptor that causes Object.defineProperty() to throw. When restoring the
property in the Object.defineProperty() call, check if the original descriptor
is an accessor descriptor by testing for get/set properties. If it is an
accessor descriptor, only include get, set, configurable, and enumerable in the
new descriptor while excluding value and writable. If it is a data descriptor,
include value, writable, configurable, and enumerable. Apply the same fix to all
property restoration points including the ones around lines 238-242.
- Around line 1425-1434: The branch that handles inline subtests created inside
a running test is not incrementing the parent plan count, which causes
inconsistent plan validation between global test() calls and t.test() calls.
Call planCount(runningNode) in this branch (where runningNode is defined and
runningNode.isRunning() is true) before scheduling the subtest with
scheduleSubtest to ensure the parent's plan count is incremented consistently
regardless of which spelling (t.test() or global test()) created the inline
subtest.
- Around line 708-710: The getTestContext() function always returns
node.getCtx() regardless of whether the current node is a suite or a test. When
a suite is the active node, the function should return the SuiteContext via
node.getSuiteCtx() instead of the TestContext. Modify getTestContext() to check
if the current node is a suite node and conditionally return either
node.getSuiteCtx() for suite nodes or node.getCtx() for test nodes.
- Around line 1345-1351: When `runOwnBeforeHooks(suite)` throws an error, the
catch block in the `run` async function is silently swallowing the error without
incrementing `suite.failedSubtests` to track the failure. Inside the catch
block, add logic to increment `suite.failedSubtests` so that before hook
failures are properly recorded and the parent suite can accurately track that
the setup failed.
- Around line 367-399: The restore function in this mocking logic always defines
the descriptor back onto objectOrFunction using Object.defineProperty, but when
the descriptor was originally found on a prototype (inherited property rather
than an own property), this creates an unwanted own property shadow. To fix
this, check whether the descriptor was originally an own property of
objectOrFunction before the while loop, and in the restore function,
conditionally either delete the own property (if it was inherited) or redefine
it (if it was an own property). This ensures the object is restored to its
original shape after restoreAll is called.
🪄 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: 0ec46dab-8954-4fa9-a64e-2d9fe5469646
📒 Files selected for processing (20)
docs/runtime/nodejs-compat.mdxsrc/js/internal/test_runner/mock_timers.tssrc/js/node/net.tssrc/js/node/test.tstest/js/node/test/parallel/test-runner-aftereach-runtime-skip.jstest/js/node/test/parallel/test-runner-aliases.jstest/js/node/test/parallel/test-runner-custom-assertions.jstest/js/node/test/parallel/test-runner-get-test-context.jstest/js/node/test/parallel/test-runner-mock-timers-date.jstest/js/node/test/parallel/test-runner-mock-timers-scheduler.jstest/js/node/test/parallel/test-runner-mock-timers.jstest/js/node/test/parallel/test-runner-option-validation.jstest/js/node/test/parallel/test-runner-tags-inheritance.mjstest/js/node/test/parallel/test-runner-test-filepath.jstest/js/node/test/parallel/test-runner-test-fullname.jstest/js/node/test/parallel/test-runner-typechecking.jstest/js/node/test/parallel/test-runner-wait-for.jstest/js/node/test/parallel/test-set-http-max-http-headers.jstest/js/node/test_runner/fixtures/05-test-in-test.jstest/js/node/test_runner/node-test.test.ts
💤 Files with no reviewable changes (1)
- test/js/node/test/parallel/test-set-http-max-http-headers.js
adfaf69 to
f8f909f
Compare
f8f909f to
830b998
Compare
830b998 to
bfc9104
Compare
bfc9104 to
f07579f
Compare
|
Awesome addition - have been waiting a while for this! ❤️ |
f07579f to
e0a3946
Compare
e0a3946 to
f0d09c7
Compare
Reimplements the node:test shim so that the in-process API matches Node: - t.test()/t.describe() and global test()/describe() called inside a running test now run as inline subtests (previously NotImplementedError); the parent waits for them and fails when a subtest fails. - beforeEach/afterEach are run by the module itself with Node's context arguments and inheritance order; before/after still map to bun:test beforeAll/afterAll but receive the owning suite/root context. - New: getTestContext(), t.plan() (including the wait option), t.waitFor(), runtime t.skip()/t.todo(), test tags (validation, case-insensitive dedup, inheritance, one-shot ExperimentalWarning), SuiteContext, fullName/filePath/passed/attempt, custom assertions via assert.register(), done-callback test functions, and top-level test() returning a promise. - t.mock is now a per-test MockTracker that resets after the test; mock.property() and mock.timers (a port of Node's MockTimers and PriorityQueue, in src/js/internal/test_runner/mock_timers.ts) are added. - Per-test timeouts are enforced by the shim with Node semantics; only timeouts above bun:test's 5s default are forwarded to bun:test. node:net: a custom lookup callback that yields a non-string address now emits ERR_INVALID_IP_ADDRESS like Node instead of connecting to the stringified value. Tests: - Vendors 11 more Node v26.3.0 test_runner tests (18 total now pass) and refreshes test-runner-aliases.js / test-runner-typechecking.js to verbatim upstream copies. - Rewrites the 05-test-in-test.js fixture to assert subtests work instead of asserting they throw NotImplementedError. - Removes test-set-http-max-http-headers.js: it only passed because callback-style node:test functions never waited for their callback; it spawns test-http-max-http-headers.js, which is not vendored, so it cannot pass meaningfully. - Updates the node:test entry in docs/runtime/nodejs-compat.mdx.
f0d09c7 to
9cb1fad
Compare
sosukesuzuki
left a comment
There was a problem hiding this comment.
Runtime t.skip()/t.todo() gets reported as pass under --concurrent:
// repro.js
const test = require("node:test");
test("alpha", (t) => { t.skip("runtime skip"); });
test("beta", (t) => { t.todo("runtime todo"); }); $ bun bd test --config=bunfig.node-test.toml ./repro.js
0 pass / 1 skip / 1 todo
$ bun bd test --config=bunfig.node-test.toml --concurrent ./repro.js
2 pass / 0 fail
Deterministic, and reachable via concurrentTestGlob in bunfig.toml too. Looks like
get_current_state_data() returns entry_data: None for a concurrent group
(bun_test.rs:704), so the mark gets dropped at jest.rs:582 and
createTopLevelTestRunner falls through to done(undefined).
Your own test-runner-aftereach-runtime-skip.js goes 1 pass/2 skip -> 2 pass/1 skip
under --concurrent and still exits 0.
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 `@test/js/node/test/parallel/test-runner-mocking.js`:
- Around line 587-589: Remove the Bun/JSC-specific private-field and
defineProperty assertion changes from the vendored test-runner mocking test,
restoring parity with Node v26.3.0 and its original assertions. Move equivalent
coverage for JSC-specific error behavior into Bun-owned tests instead, leaving
test/js/node/test/parallel/test-runner-mocking.js vendor-aligned.
🪄 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: fdadf401-f868-485d-ad62-34d9f5a617b9
📒 Files selected for processing (3)
src/js/node/test.tstest/expectations.txttest/js/node/test/parallel/test-runner-mocking.js
|
@robobun adopt |
…root, oldRoot.mockTracker reset markCurrentResult's fallback for the 'not yet stamped' DoneCallback window (when .then() fires inside run_test_callback's microtask drain) used get_current_state_data(), which can't name a sequence inside a concurrent group, so the skip/todo mark was dropped and the test was reported pass. Execution now stashes the callback's own EntryData alongside on_stack_entry (set/restored around run_test_callback), and js_node_test_mark_result reads that instead. New --concurrent case in node-test.test.ts pins it. getTestContext() now checks node.parent !== undefined like hookArgFor() does, so a file-level before/after hook (root has isSuite=true, parent=undefined; Node's root is a Test) gets a TestContext. getRootNode()'s isNewFile branch now also resets the outgoing root's own mockTracker (reachable via a file-level before hook's t.mock, distinct from the module-level mock export).
|
@sosukesuzuki good catch, thank you. Fixed in cd40cb3: your diagnosis was exactly right. Verified with your repro:
|
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 `@test/js/node/test_runner/node-test.test.ts`:
- Around line 97-99: Remove the implementation-history comment describing
markCurrentResult's microtask-drain fallback and the dropped skip/todo mark;
leave the test setup, actions, test name, and assertions unchanged.
🪄 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: c0fffd91-cbf0-481d-b81c-5282de1efcde
📒 Files selected for processing (4)
src/js/node/test.tssrc/runtime/test_runner/Execution.rssrc/runtime/test_runner/jest.rstest/js/node/test_runner/node-test.test.ts
sosukesuzuki
left a comment
There was a problem hiding this comment.
LGTM, great work.
But one comment about current limitation I found.
bun:test 5s watchdog is still a floor, so a sync block past it diverges from node even with a timeout set.
test("x", { timeout: 50 }, () => { const e = Date.now()+8000; while (Date.now()<e) {} });
node: pass bun: fail "timed out after 5000ms"
I think this rarely happens. So it's enough to note it in the PR description and follow up later.
|
@sosukesuzuki thanks, added to the Known divergences section of the PR body. You are right about the mechanism: |
What does this PR do?
Brings the
node:testin-process API much closer to Node v26.3.0 so that the upstream test_runner suite can be vendored. 20 of Node v26.3.0's test_runner tests now pass and are vendored, up from 7 (the previously vendored 7 keep passing;test-runner-aliases.jsandtest-runner-typechecking.jsare refreshed to verbatim upstream copies).Runtime changes (
src/js/node/test.ts, newsrc/js/internal/test_runner/mock_timers.ts)t.test()/t.describe()and globaltest()/describe()called inside a running test now execute as inline subtests (previously they threwNotImplementedError). The parent waits for awaited and unawaited subtests, and fails withN subtests failedwhen one fails. Top-leveltest()returns a promise that resolvesundefinedwhen the test completes, like Node.beforeEach/afterEachregistered throughnode:testare now run by the module itself so they receive Node's context arguments (the running test'sTestContext) and follow Node's inheritance order (ancestors first forbeforeEach, nearest scope first forafterEach).before/afterstill register through bun:testbeforeAll/afterAllbut receive the owning suite/root context.t.before/t.after/t.beforeEach/t.afterEachapply to the test's subtests. Hook{ timeout, signal }options are validated like Node (ERR_INVALID_ARG_TYPE/ERR_OUT_OF_RANGE) and enforced when the hook runs (timeout race + abort listener).beforehooks created on an already-running test run exactly once and their outcome is memoized like Node'srunOnce: a failingbeforehook fails the owning test and every later subtest without running their bodies, and a failingbeforehook of an inline suite fails that suite. Test bodies that throw or reject with a nullish value fail (Node does). Inlinedescribe()callbacks are awaited like Node's suite build: an async callback that rejects fails the suite, a synchronous throw still accounts for the children it already registered, and inline-suite children run serially after previously scheduled subtests. Hooks accept Node's(ctx, done)callback signature, and done-style functions follow Node exactly: a callback function that also returns a promise fails (passed a callback but also returned a Promise) after the body settles, adone()invoked twice fails (callback invoked multiple times), and the callback is only passed for an arity of exactly 2.{ timeout: Infinity }is forwarded to bun:test so an explicitly untimed test overrides the runner's per-test default. A runtimet.skip()/t.todo()suppresses a later failure (Node reports SKIP/TODO and the run passes), inlinedescribe.todo()failures do not fail the owning test, collection-phase todo bodies are registered for real sobun test --todoruns them, and the runner's own timers (timeout race,t.planwait,t.waitFor) are captured at module load somock.timerscannot fake them.getTestContext()(tracked withAsyncLocalStorage, so it works acrossawaitandsetImmediate),t.plan()including thewaitoption,t.waitFor(), runtimet.skip()/t.todo(), test tags (validation, case-insensitive dedup, parent-first inheritance, frozen arrays, one-shotExperimentalWarning),SuiteContextfor suite callbacks/hooks,fullName/filePath/passed/attempt, custom assertions viaassert.register()(bound to theTestContext, counted by the plan), done-callback test functions ((t, done) => {}), andtest()/suite()option validation matching Node's error codes (timeout,concurrency,tags).t.assertis built as a prototype-less namespace like Node's, and the internal mock/assertion registries never go through user-patchableMap.prototypemethods.t.mockis now a per-testMockTrackerthat is reset automatically when the test finishes. Addsmock.property()(port of Node'sMockPropertyContext) andmock.timers— a port of Node'sMockTimers+PriorityQueue(src/js/internal/test_runner/mock_timers.ts) covering setTimeout/setInterval/setImmediate,node:timers,timers/promises(including the async-iteratorsetInterval),Date,scheduler.wait, andAbortSignal.timeout.timeoutoption is enforced by the shim with Node's semantics (a 1ms timeout with a synchronous body still passes); only timeouts above bun:test's 5s default are forwarded to bun:test so its watchdog does not kill them early.only:test.only()/describe.only()and{ only: true }register ordinary tests/suites, matching Node's runner semantics (onlydoes nothing without--test-only, which bun has no equivalent for). They are intentionally not routed to bun:test'sonly(), which both skips siblings and throws in CI environments — routing them was what made the verbatimtest-runner-typechecking.jsfail on every CI shard. This meansbun test --onlydoes not selectnode:testonly-marked tests.lookupcallback that yields a non-string address now emitsERR_INVALID_IP_ADDRESSlike Node (typeof ip !== "string"guard from upstreamlib/net.js), instead of connecting to the stringified value. This gap was exposed becausetest-net-connect-custom-lookup-non-string-address.mjsactually waits for itsdonecallback now.Per-file isolation and timeout forwarding
beforeEach/afterEach(andassert.register()/ the module-level mock tracker) no longer leak across files sharing onebun testprocess: the root test node is now per entry file, matching Node's process-per-file isolation (fixtures14-root-hooks-a/b.jsrun as one multi-file invocation).bunTestOptions()forwards every finite timeout asmax(timeout, 5s)so a lowerbun test --timeoutcannot cut a node-style timeout short (fixture11-timeout-overrides.js, run with--timeout 100).internal/timersshim exportsTIMEOUT_MAX(2 ** 31 - 1, same as Node), so the vendoredtest-runner-mock-timers.jscase for the> TIMEOUT_MAXclamp exercises the real threshold.Stop-clock, file boundaries, and outcome reporting
executeTestNodearms a single timeout before the body and races the body, the subtest drain, andplan.check()against it, matching Node'sstopTest. Previously only the body was bounded, sot.plan(n, { wait: true })could never reject and a{ timeout: 100 }test hung until bun:test's watchdog (fixture16b-plan-wait-timeout.js).Bun.main.BunTestRoot::file_generationis bumped inenter_fileand read from JS, so per-file state (root hooks, the module-level mock tracker,assert.register()) resets on--rerun-eachiterations of the same path, whereBun.mainnever changes (fixture17-rerun-mock-reset.mjs). It is a private$newRustFunctionbinding, not a new export on the publicbun:testmodule object; the host_fn goes throughJest::runner_ptr()so it never materializes an exclusive&mut TestRunnerwhile the runner holds a live pointer into it.t.passed/t.errorinafterEach/after.TestNode.errorwas added (initializednull, like Node) and both fields are written before the hook loops.TestContext.errorno longer hardcodesundefined(fixture15-outcome-in-hooks.js).beforeEach.t.assertdestructures the plan at first access and closes over it (Node does), so the{ plan: N }option must exist before a hook can toucht.assertand is only installed for a truthyN—{ plan: 0 }installs no plan (fixtures16-plan-and-late-subtest.js,19-plan-option-order.js).ERR_TEST_FAILURE/failureType: 'parentAlreadyFinished'for the reporter, while the promiset.test()returned resolves withundefined(checked against the v26.3.0 binary; fixture16-plan-and-late-subtest.js). Previously this fell through to bun:test's "Cannot call test() inside a test".t.assert.okis installed outside the generic wrapper and passes itself toinnerOkas thestackStartFn(nodejs/node@028c5864), so the stack starts at the caller; a registeredassert.register("ok", fn)still wins (fixture21-register-ok.js). The message stays generic untilinternal/assert/utils.getErrMessageis implemented — the only remaining blocker for thetest-runner-assert.jsFIXME.__proto__: nullon everyObject.definePropertydescriptor literal intest.ts(matchingmock.jsand this PR's ownmock_timers.ts),TestContext.workerId, and thecontextNodeWeakMap replaced with a plain#nodeprivate field on both contexts.Known divergences from Node (deliberate)
mock.timers.tick(-1)/setTime(-1)reproduce Node's swapped-argumentERR_INVALID_ARG_VALUEmessage verbatim ("The argument 'time' -1. Received 'positive integer'", frommock_timers.js:558).setInterval(fn, 0)undermock.timersre-fires inside a singletick()until the callback clears it, because Node's mock timers clamp only the upper bound. Clamping to1ms(as real timers do) would change a case Node passes: a self-clearing zero-delay interval fires 4x ontick(1)in Node and 2x under a clamp. Pinned by18-mock-timers-interval-zero.js.{timeout: N}is set (bunTestOptions()forwardsmax(N, 5s)). Node only observes the timeout after the body yields, so a synchronous body that runs past 5s passes under Node with anytimeoutvalue but is killed by bun:test's watchdog here (test('x', {timeout: 50}, () => { /* spin 8s */ })-> Node pass, Bun "timed out after 5000ms"). ForwardingInfinityinstead would disable the watchdog for every node-style timed test and let a genuinely hung one block the whole run; deferred to a follow-up.Test changes
test/js/node/test/parallel/:aftereach-runtime-skip,custom-assertions,get-test-context,mock-timers,mock-timers-date,mock-timers-scheduler,mocking,option-validation,tags-inheritance,test-filepath,test-fullname,wait-for, andtest-mock-timers-abortsignal-timeout. All are verbatim upstream copies excepttest-runner-mocking.js, where two assertions on V8-specific engine error text (Cannot read private member,Cannot redefine property) are loosened to match onTypeErrorsince JSC's wording differs; andtest-runner-mock-timers.js, where one upstream comment line is dropped.test/js/node/test_runner/fixtures/05-test-in-test.jspreviously asserted that subtests throwNotImplementedError; it now asserts subtests run inline, return promises, and that globaltest()/describe()inside a running test become subtests.test-set-http-max-http-headers.jsintest/expectations.txt: it only passed because callback-stylenode:testfunctions never waited for their callback under the old shim. It spawnstest-http-max-http-headers.js, which is not vendored, so now that the callbacks actually run it fails 2/4 with "Module not found". The file and itsno-validate-leaksan.txtentry are left as-is.node:testentry indocs/runtime/nodejs-compat.mdxto describe what is and is not supported.Not covered (still missing, unchanged behavior)
run(),node:test/reporters, snapshot testing (t.assert.snapshot),mock.module(), code coverage, watch mode, and Node's--testCLI runner mode. The remaining upstream test_runner tests depend on these and are not vendored. Test-levelsignal,t.signalabort, andconcurrencyare validated but not enforced -- hook-levelsignalis (fixture20-hook-signal-and-assert-ok.js) and subtests always run serially.test.only()/{only: true}are accepted but do not filter (no--test-onlyequivalent). AbeforeEach/afterEachregistered from a--preloadmodule applies only to the first file of a multi-filebun testrun: the preload module is cached and never re-evaluated, and each file's fresh root drops the previous one's hooks (before/afterare unaffected -- they route through bun:test's ownbeforeAll/afterAll, which persist). The fix needs a preload-scope root above every per-file root and is deferred. A globaltest()called from an inline suite'safterhook runs but its failure is dropped: Bun reports1 pass, 0 failwhere node v26.3.0 reportspass 2, fail 1(the late test is counted as its own failure; neither runtime fails the owning test). Surfacing it needs the late subtest registered with bun:test after the file's tests are built, so it is deferred. Hook errors surface as the thrown error rather than Node'sfailureType: 'hookFailed'wrapper, and a collection-phase hooktimeoutlarger than bun:test's per-test watchdog is still bounded by that watchdog.How did you verify your code works?
test-runner-*files pass when run the same way CI runs them (bun test --config=bunfig.node-test.toml <file>with a debug build).test/js/node/test_runner/node-test.test.ts(20 tests) passes, including the rewritten subtest fixture, the existing mock/hook/variation fixtures, and two new fixtures:06-hook-semantics.js(before hooks run once, hook option validation, mock registries immune toMap.prototypetampering),07-failing-hooks.js(nullish body rejections, an async inlinedescriberejection, a failing inline-suitebeforehook, a hook timeout, and a subtest after a failedbeforehook must all fail),08-only-no-op.js(run withCI=1:onlyregisters ordinary tests and never trips bun:test's CI-only guard),09-inline-suites.js(inline-suite ordering and async describe callbacks),10-done-callbacks.js(done-callback tests and hooks),11-timeout-overrides.js(run with--timeout 100: an Infinity timeout must override the runner default),12-runtime-todo-and-mock-timers.js(runtime skip/todo suppression; runner timers stay real under mock timers), and13-todo-bodies.js(run with--todo: todo bodies actually run, both spellings).nodev26.3.0 binary on identical inputs (same files run under both runtimes).node:testwere re-run with the debug build; the only failure modes seen were pre-existing tmpdir collisions when run outside the CI runner (they pass when run individually).test/regression/issue/19412,23077,24338,test/js/node/module/node-module-module.test.js.bun run lintand the ban-words test pass; changed files are formatted with prettier.[review] gate passed · iteration 13 · 50 files touched
fails on main (without fix)
passes on PR (with fix)
diff hotspot
gate history · 8 passed · 2 rejected · iteration 13
evidence per changed file