node:test: republish run() child events at the child's nesting
RO
3mo ago+104+104+012345678901234567890123456789-11-11-01234567890123456789
Problem
- Under
run({ files })fromnode:test, every republishedtest:pass/test:failevent carries anestingone higher than node's: a file's top-level tests come out at nesting 1 and their subtests at nesting 2. Node v26.3.0 reports 0 and 1, exactly what the file reports when run on its own. - Cause:
republishChildEvent()insrc/js/node/test.tsdiddata.nesting = (data.nesting ?? 0) + 1on every event it forwarded from the child process. Node'sFileTest#handleReportItem(lib/internal/test_runner/runner.js) forwards the child's nesting unchanged. - The file node that run() reports for a file (its
test:enqueue/test:dequeue, and its verdict when the file had nothing else to report, failed, or was cancelled) always carriedtestId: 1andtestNumber: 1, whatever its position in the run. Node gives the second file 2, and so on.
Fix
republishChildEvent()forwards the child'snestingas-is.runFiles()passes each file's 1-based position torunOneFile()/reportCancelledFile(), which use it as the file node'stestIdandtestNumber.- Why this is correct: it matches node. Node's root creates one FileTest per file and numbers them in order; under process isolation it does not treat the file node as the parent of the file's tests, so the child's nesting values are the run's nesting values. Verified against node v26.3.0 on the test's files: the verdict sequence with its nesting column, and
empty.jsreporting astestNumber: 2, testId: 2, are node's output line for line (details below). - Counting is unchanged:
topLeveland the run-leveltest:planstill count one item per file. Node counts the file's nesting-0 verdicts instead, but bun's child does not yet report top-leveldescribe()suites (#39331 adds that), so counting nesting-0 verdicts here would report 0 top-level items for a describe-only file; that recount belongs with #39331. SubtesttestNumber(node forwards the child's value; bun's child sends 0) is also unchanged. - Verified:
test/js/node/test_runner/node-test.test.ts, newnode:test run()block (two cases: a two-file run pinning the nesting column and the file node's number, and a run aborted before it starts, pinning both cancelled file nodes' numbers). Both fail on main (nesting 2/1/1/1, file node number 1) and pass with the fix. The other 45 cases in the file and the vendoredrun()tests (test-runner-todo-skip-tests.js,test-runner-expect-error.js,test-runner-expect-error-but-pass.js,test-runner-filetest-location.js,test-runner-tags-experimental-warning.mjs) still pass. - #34515 rewrites this code path as part of a much larger change and also drops the
+1; this is the standalone fix for the bug on main today.
Background
run()is node:test's programmatic runner. With the defaultisolation: 'process'it runs each file in a child process; the child reports its tests as events and the parent republishes them on theTestsStreamthatrun()returned, then appends the run-leveltest:plan, diagnostics andtest:summary. Bun'srun()does the same with abun testchild that prints one JSON event per line.nestingis a test's depth in the event stream: a file's top-level tests are 0,t.test()subtests inside them are 1, and so on. Reporters indent by it. Under process isolation node keeps the child's values; the file node that the parent synthesizes for each file also sits at nesting 0.- The file node is node's FileTest: a test-shaped entry per file, enqueued and dequeued as the file starts, and given a verdict of its own only when the file had nothing else to report or the process itself failed (
FileTest#skipReporting); a file whose tests merely failed is represented by those tests. ItstestIdandtestNumberare its position among the run's files.
node v26.3.0 vs bun for the files used in the test
nested.js (top with subtest sub, a failing test, a skipped test) and empty.js (no tests), run in that order. Columns: event, name, nesting.
node v26.3.0 bun main bun with this change
test:pass sub 1 test:pass sub 2 test:pass sub 1
test:pass top 0 test:pass top 1 test:pass top 0
test:fail failing 0 test:fail failing 1 test:fail failing 0
test:pass skipped 0 test:pass skipped 1 test:pass skipped 0
test:pass empty.js 0 test:pass empty.js 0 test:pass empty.js 0
(testNumber 2, testId 2) (testNumber 1, testId 1) (testNumber 2, testId 2)
Earlier revision of this PR
The first push also moved topLevel / testNumber onto node's model on the parent side: each nesting-0 verdict counted as a top-level item and was numbered across files, and the file node only counted when reported. That is right once the child reports every top-level item, but today a top-level describe() emits no verdict (#39331 adds it), so a describe-only file would have reported test:plan count 0 and topLevel: 0 where main and node both report 1. That part was dropped from this PR; the counting on main is unchanged.