21 Commits
Author SHA1 Message Date
Chris Tate 391b9ac00e Merge main 2026-10-20 02:15:13 -05:00
Chris Tate 3ae4172f4d Keep thrown arrays' identity and inspect thrown values in test failures
A thrown array reached its catch binding as a type-erased native array:
`console.log(e)` printed `{}` where Node prints the array, `String(e)`
gave "[object Object]", `Array.isArray(e)` was false and a typed view
such as `(e as number[]).length` threw a TypeError. Thrown records
already crossed the catch boundary as checked-dynamic values that keep
their identity; arrays, and unions over records and arrays, now take the
same conversion, so the catch binding is the thrown object.

node:test printed a failing test's thrown non-Error value with String():
`throw { code: 42 }` reported `[object Object]` where Node prints the
value as util.inspect renders it. The failing-tests section now inspects
thrown numbers, booleans and checked-dynamic values (records, arrays,
null and undefined); a thrown string is quoted as before. A thrown class
instance that is not an Error still prints `[object Object]` (the
limitations page says so).

Adds corpus 6341, extends the thrown-values node:test fixture, and
records 6341's cost.
2026-10-19 21:06:20 -05:00
Chris Tate 0e02fc28ca Accept mock.fn() without an original
node:test changes, matching Node 24.22:

- mock.fn() and t.mock.fn() without an original return undefined and
  record the arguments of each call, whatever the callers pass. The
  original and the initial implementation are a function that returns
  undefined, so restore() brings that function back.
- mock.fn<F>() with an explicit function type records F's parameters
  when F's return type is void or includes undefined. A return type
  without undefined, such as mock.fn<() => number>(), throws SC2020.

mock.fn()'s default type, (...args: any[]) => undefined, maps to the
checked-dynamic rest ABI used for program-declared (...args: unknown[])
functions: the calls log records the argument array, and the mock's
wrapper spreads it into the implementation.

A function whose fixed parameters are all unknown or any, or that has
none, now converts to a checked-dynamic rest signature without fixed
parameters, such as (...args: unknown[]) => void: each call passes the
arguments in order, undefined for the ones the caller left out. This is
how () => {} reaches mockImplementation and mockImplementationOnce, and
it applies to any such slot in TypeScript programs, which threw SC1090
before.

The mock-fn-default fixture and corpus program 5816 compare with Node,
plain and with SCRIPTC_SAN=1.
2026-10-19 16:59:05 -05:00
Chris Tate e82686eab7 Add getTestContext to node:test
node:test changes, matching Node 24.22:

- getTestContext() returns the context of the test, suite or hook whose
  code is running: undefined at module top level, the root's context in
  module-level hooks, the suite's in a suite body and in the suite's
  hooks, and the test's in its body. In the afterEach and t.after hooks
  of a subtest cancelled by its parent's timeout, it returns each hook's
  owner.
- The context rides the AsyncLocalStorage snapshot under a reserved id,
  so it follows a test across its awaits, into the async functions,
  promise callbacks, timers and other callbacks it schedules, and into
  concurrent subtests, which each see their own.
- The root's fullName is "<root>", as its name is.
- Optional chains over a test context, such as getTestContext()?.name,
  lower.

AsyncLocalStorage enter and restore move into the always-linked async
core so the test runner can use them. The snapshot holds the node's
index in a table of live nodes, so a snapshot that outlives its node
answers undefined.

The get-test-context and get-test-context-cancelled fixtures compare
the contexts seen at each point with Node, plain and with SCRIPTC_SAN=1.
2026-10-19 06:29:28 -05:00
Chris Tate f75b7d8aab Use a computed test name as given when the function's name is unknown
A test name computed at run time took the function's name when it was
empty, which needs that name at compile time. For a function the
program received as a parameter, such as a helper that registers
`test(\`case ${label}\`, body)` for its callers, the call was refused,
where it compiled before. The name is now used as given in that case,
and the limitations page notes that an empty one stays empty, where
Node uses the function's name.

The test-name-forms fixture registers tests through such a helper and
compares them with Node, plain and with SCRIPTC_SAN=1. Without this
change it fails to compile.
2026-10-19 03:22:05 -05:00
Chris Tate 60535fd09c Accept test and describe calls without a name, and settle skip and todo as Node does
node:test changes, matching Node 24.22:

- test, it, describe, suite, their skip/todo/only twins and t.test take
  Node's (name?, options?, fn?) forms: test(fn), test(options, fn),
  test(options) and test() compile, as does describe(fn) and
  describe(options, fn). The fallback declarations add the overloads.
- A missing or empty name is the function's name, or "<anonymous>", as
  Node's Test constructor completes it. The name is the function
  expression's or declaration's own name, or the binding name of a
  const arrow or anonymous function; a name that is empty at run time
  takes the same default.
- With both skip and todo, the test is skipped, and the directive message
  is the skip string, else the todo string. The skip and todo method
  twins override the option of the same name, so test.skip("x",
  { skip: "msg" }) reports a bare SKIP and it.skip("x", { todo: "msg" })
  reports "msg". Before, the last option set the mode.

The limitations page notes that a thrown object or array that is not an
Error reports [object Object] in the failing section.

The test-name-forms fixture compares the names, directives and counts
with Node, plain and with SCRIPTC_SAN=1.
2026-10-19 00:21:03 -05:00
Chris Tate e8e432b6dd Clean up subtests cancelled by their parent's timeout
node:test changes, matching Node 24.22:

- A subtest cancelled because its parent timed out has its mocks
  restored and its mock timers reset when it is cancelled, as Node's
  postRun resets the test's mock tracker. Before, a mocked method stayed
  mocked for the tests that ran after it.
- Its afterEach chain and t.after hooks run at the next turn, once the
  runner has moved on to the next test, as Node runs them when the
  cancelled test's own run resumes. Before, they never ran. The chain is
  copied when the test is cancelled, since its ancestors drop their hooks
  when they finish.
- Its hooks are then dropped, so closures that capture its context do not
  keep it alive. The registration parent is still not restored when the
  cancelled body settles: it belongs to the test that runs then.

The timeout-cancel-cleanup fixture records the mocked method and the hook
order against the next test's body and compares them with Node, plain and
with SCRIPTC_SAN=1. It fails without this change.
2026-10-18 20:58:20 -05:00
Chris Tate 3de2fb56a0 Cancel running subtests when their parent test times out
node:test changes, matching Node 24.22:

- When a test's { timeout } elapses, its subtests that are still running
  are cancelled, the innermost first. Each reports its duration so far and
  fails with "test did not finish before its parent and was cancelled",
  before the parent's result.
- A cancelled subtest's body keeps running unobserved: when it settles it
  reports nothing and leaves the runner's state alone. Before, it reported
  its result after the run's summary.

The timeout-subtests fixture compares the results, counts and failing
section with Node, plain and with SCRIPTC_SAN=1.
2026-10-18 19:28:55 -05:00
Chris Tate d3dba4c07f Run module-level before and after hooks
node:test changes, matching Node 24.22:

- before and after hooks outside any suite run, with the root's context,
  whose name is "<root>". They did not run before.
- A module-level before hook runs at its before() call, as Node runs the
  hooks of its already-started root at once: its synchronous part runs
  before the code after the call, and the first top-level test waits for
  an async hook's promise. Every module-level before hook runs, and an
  async hook's rejection is not reported as an unhandled rejection.
- Module-level after hooks run after the last top-level test or suite.
- A throwing root before hook fails each top-level test with the first
  failure; the test's body and beforeEach hooks do not run, and its
  afterEach hooks do. Each top-level suite fails with the error, and the
  tests and suites in it are cancelled before they start: they report
  without a duration and fail with "test did not finish before its parent
  and was cancelled", listed in the failing section in completion order.
- A throwing root after hook reports the entry file as a failed result,
  after the top-level results, and lists it in the failing section at the
  hook's location. It is not counted, and the exit code is unchanged.
- Hooks record the "file:line:col" of their call for that location.

The root-hooks, root-before-throws and root-after-throws fixtures compare
the reporter output, the order of the hooks against the module code, and
the exit codes with Node, plain and with SCRIPTC_SAN=1.
2026-10-18 10:59:27 -05:00
Chris Tate c73f363906 Pass the SuiteContext and hook contexts
node:test changes, matching Node 24.22:

- A describe or suite body may take its SuiteContext, with name,
  fullName, filePath, attempt, passed and diagnostic. Its passed is false
  until the suite has finished, after its after hooks.
- before and after hooks may take the suite's context, and beforeEach
  and afterEach hooks the test's. A test's t.before, t.after,
  t.beforeEach and t.afterEach hooks may take the test's context.
- SuiteContext maps to the runner node that TestContext maps to, so the
  TestContext members that SuiteContext shares lower the same way.

The suite-context fixture compares the reporter output and the order of
the bodies and hooks with Node, plain and with SCRIPTC_SAN=1.
2026-10-18 10:31:11 -05:00
Chris Tate acc920ab85 Add the TestContext filePath, attempt, passed, workerId and tags values
node:test changes, matching Node 24.22:

- t.filePath is the entry file's path, as Node's entryFile, fixed when the
  program is compiled.
- t.attempt is 0, since a test runs once.
- t.passed is false while the body runs and the test's outcome once the
  body ends, so afterEach and t.after hooks see it, as Node's pass() and
  fail() set it before them.
- t.workerId is Number(process.env.NODE_TEST_WORKER_ID) || undefined.
- t.tags is [], since the tags option has no lowering.
- A test's t.before, t.after, t.beforeEach and t.afterEach hooks are
  released when the test ends. Hooks whose closures capture the test's
  context no longer keep it alive in a cycle, which the SCRIPTC_SAN=1
  audit reported as live closures at exit.

The ctx-members fixture compares the reporter output with Node, plain and
with SCRIPTC_SAN=1.
2026-10-18 09:57:26 -05:00
Chris Tate 89b5baedd7 Add t.assert.snapshot and t.assert.fileSnapshot
node:test changes, matching Node 24.22:

- t.assert.snapshot(value[, options]) serializes the value with Node's
  default serializer, JSON.stringify(value, null, 2), or with an
  options.serializers array literal applied in order, and compares it
  with assert.strictEqual against the snapshot stored under
  "<test full name> <n>" in "<entry file>.snapshot".
- t.assert.fileSnapshot(value, path[, options]) compares the serialized
  value with the contents of path.
- Snapshot files are read in the form Node writes them, exports[`key`] =
  `value`; statements, with template escapes and CRLF line endings
  cooked as in a template literal.
- Missing snapshots, unreadable snapshot files and throwing serializers
  throw Node's ERR_INVALID_STATE errors, with Node's messages.
- Running the binary with Node's --test-update-snapshots flag as its
  first argument records the values and writes the snapshot files after
  the last test, keys sorted as Node sorts them. The flag is kept out of
  process.argv and appears in process.execArgv, which is otherwise [].
- Snapshot assertions count toward the test's plan.

The snapshot fixture compares the reporter output with Node, plain and
with SCRIPTC_SAN=1, against a snapshot file Node wrote. The update-mode
harness test runs Node and the binary with --test-update-snapshots and
compares the files they write. The node:test normalization also drops
the "... N lines matching cause stack trace ..." line Node prints for an
error cause, with the stack frames it summarizes.
2026-10-18 05:31:40 -05:00
Chris Tate a5c7c09ad3 Add mock.timers to node:test 2026-10-17 20:55:17 -05:00
Chris Tate 591994217c Run every subtest at once with concurrency: true
Node sets a test's concurrency to Infinity for `true` whenever the test
has a parent, and to os.availableParallelism() - 1 only for the root
test. Every test and suite a program declares has a parent, so `true`
now runs all of its subtests at once instead of capping them at the CPU
count minus one.

The concurrency fixture adds a suite of 64 concurrent tests that checks
all of them run at once.
2026-10-17 08:39:10 -05:00
Chris Tate 05cb9bd609 Add the concurrency test option
node:test changes, matching Node 24.22:

- { concurrency } on describe, suite, test, it and t.test sets how many
  subtests run at once: a number, true for os.availableParallelism() - 1,
  or false for 1. Subtests inherit the parent's value.
- Above 1, each subtest runs on its own fiber. Subtests start in
  declaration order as slots free up, and t.test and test() calls inside a
  concurrent test start the subtest at once; t.test's promise settles when
  it finishes. The parent finishes after all of them.
- Results, diagnostics and failures are reported in declaration order.
- A concurrent subtest waits after its beforeEach hooks, and a concurrent
  suite before its children, for as many turns as Node's runner does, so
  sibling bodies interleave as they do there.
- A concurrency that is not an integer from 1 to 4294967295 throws Node's
  ERR_OUT_OF_RANGE RangeError when the test or suite is created.
- Nested test() calls and hooks inside a concurrent subtest attach to the
  subtest that runs on the calling fiber.

The concurrency fixture compares the reporter output, the order of the
bodies' side effects, and the range errors with Node, plain and with
SCRIPTC_SAN=1.
2026-10-17 03:28:15 -05:00
Chris Tate e2c34f381b Add the plan and timeout test options 2026-10-16 20:55:26 -05:00
Chris Tate 42954a3266 Add mock.fn and t.mock.fn with Node's calls log and implementation controls
node:test's mock.fn(original[, implementation]) and t.mock.fn compile.
A Mock<F> is the function-with-properties record { %call, mock }: the
callable is a wrapper closure that shares the mock context with the mock
field. The context records each call's arguments, result, error and this,
and keeps the current implementation, the original and the one-time
implementations by call index. fn.mock supports, as in Node 24.22:

- calls, read as a copy of the log;
- callCount();
- mockImplementation(fn);
- mockImplementationOnce(fn[, onCall]), for the next call by default, a
  later registration for the same index replacing the earlier one;
- resetCalls();
- restore(), which brings back the original.

A throwing implementation records the error and rethrows it. Functions
with fixed parameters or a single rest array parameter are supported; a
union return type needs an undefined arm. mock.fn() without an original,
mock.method, the tracker's reset and restoreAll, and t.mock.method are
refused with SC2020.

The node:test fixture mock-fn compares the behavior with Node, and
tests/diagnostics/mock-fences.ts covers the refusals.
2026-10-16 00:22:22 -05:00
Chris Tate 681f5ceee7 Add TestContext hooks, plan and fullName, and Node's todo and only rules
node:test changes, matching Node 24.22:

- t.before, t.after, t.beforeEach and t.afterEach lower. t.before runs
  when it is called; a throw fails the test and the subtests that follow,
  which skip their body and beforeEach hooks and run their afterEach hooks.
  t.beforeEach and t.afterEach wrap every descendant subtest. t.after runs
  after the test's afterEach chain, also when the body throws, and a throw
  fails the test.
- t.plan(count) counts the test's t.assert calls and subtests. A mismatch
  fails the test with Node's "plan expected N assertions but received M"
  string, unless an assertion failed first.
- t.fullName joins the names from the outermost suite or test with " > ".
- Tests and suites under a todo suite or test are todo, with a bare TODO
  directive, unless they are skipped or carry their own todo message. A
  todo suite reports its directive.
- describe.only, suite.only and { only: true } on a suite mark it. At each
  level, when a child is marked or contains a marked test or suite, only
  those children run.
- The failing-tests section lists failures in completion order, so
  subtests come before the test that ran them, and prints thrown strings
  quoted, as Node inspects them.

The fixtures ctx-hooks, ctx-plan, todo-suites, suite-variants,
only-nested and thrown-values compare the reporter output and exit code
with Node.
2026-10-15 23:16:04 -05:00
Chris Tate dca69db98e Fix pending top-level await exit precedence 2026-07-28 22:40:36 -05:00
Chris Tate 6b6445ab90 Retire the "DOM" nickname in comments, internal docs, and message prose: the checked-dynamic tree / dyn values
- The ratified rename rider (world-unification §7 lane 5): "DOM" collided with the browser DOM and, post-lane-1, the structure is no longer a document tree. Comments and doc prose now say "the checked-dynamic tree" for the noun and "dyn" adjectivally; runtime symbol names (scr_dyn_*), the error.domClone libCall, and the real web DOMException family are unchanged.
- The few user-facing strings carrying the nickname move with it (the structuredClone cyclic message, the dc subscriber/publish fences, two emitter ICE texts); diagnostics snapshots regenerated (96 pass). One private compiler constant (DOM_PROTO_METHOD_NAMES -> DYN_PROTO_METHOD_NAMES) rides along.
- Mechanical sweep, 142 files, comments/prose only — no behavior change.
2026-07-24 11:14:58 -05:00
Chris Tate 23d5918a53 init 2026-07-22 18:08:04 -05:00