Commit fcfb7ecc0ba for nodejs

commit fcfb7ecc0ba1199eb336877f2e652bcb21e867be
Author: Guilherme Araújo <arauujogui@gmail.com>
Date:   Tue Oct 6 02:55:47 2026 -0300

    test_runner: add mock call assertions to t.assert

    Add t.assert.called(), t.assert.callCount(), t.assert.nthCalledWith()
    and t.assert.lastCalledWith() for asserting on mock function calls.

    Assisted-by: Claude
    Signed-off-by: Guilherme Araújo <arauujogui@gmail.com>
    PR-URL: https://github.com/nodejs/node/pull/66476
    Reviewed-By: Chemi Atlow <chemi@atlow.co.il>
    Reviewed-By: James M Snell <jasnell@gmail.com>

diff --git a/doc/api/test.md b/doc/api/test.md
index 77bf17544df..a2c98388fa4 100644
--- a/doc/api/test.md
+++ b/doc/api/test.md
@@ -4359,6 +4359,46 @@ test('test', (t) => {
 });
 ```

+#### `context.assert.callCount(fn, times[, message])`
+
+<!-- YAML
+added: REPLACEME
+-->
+
+* `fn` {Function} A mock function created by the test runner's mocking API.
+* `times` {integer} The expected number of calls.
+* `message` {string} Optional error message.
+
+Asserts that the mock function `fn` has been called exactly `times` times.
+
+```js
+test('mock was called twice', (t) => {
+  const fn = t.mock.fn();
+  fn();
+  fn();
+  t.assert.callCount(fn, 2);
+});
+```
+
+#### `context.assert.called(fn[, message])`
+
+<!-- YAML
+added: REPLACEME
+-->
+
+* `fn` {Function} A mock function created by the test runner's mocking API.
+* `message` {string} Optional error message.
+
+Asserts that the mock function `fn` has been called at least once.
+
+```js
+test('mock was called', (t) => {
+  const fn = t.mock.fn();
+  fn();
+  t.assert.called(fn);
+});
+```
+
 #### `context.assert.fileSnapshot(value, path[, options])`

 <!-- YAML
@@ -4398,6 +4438,52 @@ This function differs from `context.assert.snapshot()` in the following ways:
 These differences allow snapshot files to better support features such as syntax
 highlighting.

+#### `context.assert.lastCalledWith(fn[, ...args])`
+
+<!-- YAML
+added: REPLACEME
+-->
+
+* `fn` {Function} A mock function created by the test runner's mocking API.
+* `...args` {any} The expected arguments.
+
+Asserts that the most recent call to the mock function `fn` received arguments
+deeply and strictly equal to `args`, using the same comparison as
+[`assert.deepStrictEqual()`][].
+
+```js
+test('mock was last called with arguments', (t) => {
+  const fn = t.mock.fn();
+  fn(1);
+  fn(2);
+  t.assert.lastCalledWith(fn, 2);
+});
+```
+
+#### `context.assert.nthCalledWith(fn, n[, ...args])`
+
+<!-- YAML
+added: REPLACEME
+-->
+
+* `fn` {Function} A mock function created by the test runner's mocking API.
+* `n` {integer} The 1-based index of the call to check.
+* `...args` {any} The expected arguments.
+
+Asserts that the `n`th call to the mock function `fn` received arguments
+deeply and strictly equal to `args`, using the same comparison as
+[`assert.deepStrictEqual()`][]. `n` starts at `1`, so
+`t.assert.nthCalledWith(fn, 1)` checks `fn.mock.calls[0]`.
+
+```js
+test('mock was called with arguments on the second call', (t) => {
+  const fn = t.mock.fn();
+  fn(1);
+  fn(2);
+  t.assert.nthCalledWith(fn, 2, 2);
+});
+```
+
 #### `context.assert.snapshot(value[, options])`

 <!-- YAML
@@ -5036,6 +5122,7 @@ test.describe('my suite', (suite) => {
 [`SuiteContext`]: #class-suitecontext
 [`TestContext`]: #class-testcontext
 [`TracingChannel`]: diagnostics_channel.md#class-tracingchannel
+[`assert.deepStrictEqual()`]: assert.md#assertdeepstrictequalactual-expected-message
 [`assert.throws`]: assert.md#assertthrowsfn-error-message
 [`context.diagnostic`]: #contextdiagnosticmessage
 [`context.log`]: #contextlogmessage-data
diff --git a/lib/internal/test_runner/assert.js b/lib/internal/test_runner/assert.js
index 776c1e25cbf..98cf4f283ff 100644
--- a/lib/internal/test_runner/assert.js
+++ b/lib/internal/test_runner/assert.js
@@ -2,11 +2,19 @@
 const {
   SafeMap,
 } = primordials;
+const {
+  codes: {
+    ERR_INVALID_ARG_TYPE,
+  },
+} = require('internal/errors');
 const {
   validateFunction,
+  validateInteger,
   validateString,
 } = require('internal/validators');
+const { isDeepStrictEqual } = require('internal/util/comparisons');
 const assert = require('assert');
+const { AssertionError } = assert;
 const methodsToCopy = [
   'deepEqual',
   'deepStrictEqual',
@@ -28,6 +36,98 @@ const methodsToCopy = [
 ];
 let assertMap;

+function getMockContext(fn) {
+  const { MockFunctionContext } = require('internal/test_runner/mock/mock');
+  const ctx = fn?.mock;
+
+  if (!(ctx instanceof MockFunctionContext)) {
+    throw new ERR_INVALID_ARG_TYPE('fn', 'mock function', fn);
+  }
+
+  return ctx;
+}
+
+function called(fn, message) {
+  const count = getMockContext(fn).callCount();
+
+  if (count === 0) {
+    throw new AssertionError({
+      __proto__: null,
+      message: message ?? 'Expected mock function to have been called',
+      actual: count,
+      operator: 'called',
+      stackStartFn: called,
+    });
+  }
+}
+
+function callCount(fn, times, message) {
+  validateInteger(times, 'times', 0);
+  const count = getMockContext(fn).callCount();
+
+  if (count !== times) {
+    throw new AssertionError({
+      __proto__: null,
+      message: message ??
+        `Expected mock function to have been called ${times} times, but it was called ${count} times`,
+      actual: count,
+      expected: times,
+      operator: 'callCount',
+      stackStartFn: callCount,
+    });
+  }
+}
+
+function assertCalledWithAt(fn, n, args, stackStartFn) {
+  const calls = getMockContext(fn).calls;
+
+  if (n > calls.length) {
+    throw new AssertionError({
+      __proto__: null,
+      message: `Expected mock function to have been called at least ${n} times, ` +
+        `but it was called ${calls.length} times`,
+      actual: calls.length,
+      expected: n,
+      operator: stackStartFn.name,
+      stackStartFn,
+    });
+  }
+
+  const actual = calls[n - 1].arguments;
+
+  if (!isDeepStrictEqual(actual, args)) {
+    throw new AssertionError({
+      __proto__: null,
+      message: `Expected call ${n} of mock function to have been called with:`,
+      actual,
+      expected: args,
+      operator: 'deepStrictEqual',
+      stackStartFn,
+    });
+  }
+}
+
+function nthCalledWith(fn, n, ...args) {
+  validateInteger(n, 'n', 1);
+  assertCalledWithAt(fn, n, args, nthCalledWith);
+}
+
+function lastCalledWith(fn, ...args) {
+  const count = getMockContext(fn).callCount();
+
+  if (count === 0) {
+    throw new AssertionError({
+      __proto__: null,
+      message: 'Expected mock function to have been called',
+      actual: count,
+      operator: 'lastCalledWith',
+      stackStartFn: lastCalledWith,
+    });
+  }
+
+  assertCalledWithAt(fn, count, args, lastCalledWith);
+}
+
 function getAssertionMap() {
   if (assertMap === undefined) {
     assertMap = new SafeMap();
@@ -35,6 +135,11 @@ function getAssertionMap() {
     for (let i = 0; i < methodsToCopy.length; i++) {
       assertMap.set(methodsToCopy[i], assert[methodsToCopy[i]]);
     }
+
+    assertMap.set('called', called);
+    assertMap.set('callCount', callCount);
+    assertMap.set('lastCalledWith', lastCalledWith);
+    assertMap.set('nthCalledWith', nthCalledWith);
   }

   return assertMap;
diff --git a/lib/internal/test_runner/mock/mock.js b/lib/internal/test_runner/mock/mock.js
index 4ac1e0a7e0b..996c8a7bcca 100644
--- a/lib/internal/test_runner/mock/mock.js
+++ b/lib/internal/test_runner/mock/mock.js
@@ -1118,5 +1118,6 @@ if (!enableModuleMocking) {

 module.exports = {
   ensureNodeScheme,
+  MockFunctionContext,
   MockTracker,
 };
diff --git a/test/parallel/test-runner-assert.js b/test/parallel/test-runner-assert.js
index 236f1851d6d..68c3a8e9cbb 100644
--- a/test/parallel/test-runner-assert.js
+++ b/test/parallel/test-runner-assert.js
@@ -11,10 +11,92 @@ test('expected methods are on t.assert', (t) => {
     'options',
   ];
   const assertKeys = Object.keys(assert).filter((key) => !uncopiedKeys.includes(key));
-  const expectedKeys = ['snapshot', 'fileSnapshot'].concat(assertKeys).sort();
+  const expectedKeys = [
+    'snapshot',
+    'fileSnapshot',
+    'called',
+    'callCount',
+    'lastCalledWith',
+    'nthCalledWith',
+  ].concat(assertKeys).sort();
   assert.deepStrictEqual(Object.keys(t.assert).sort(), expectedKeys);
 });

 test('t.assert.ok correctly parses the stacktrace', (t) => {
   t.assert.throws(() => t.assert.ok(1 === 2), /t\.assert\.ok\(1 === 2\)/);
 });
+
+test('t.assert.called', (t) => {
+  const fn = t.mock.fn();
+  t.assert.throws(() => t.assert.called(fn), {
+    code: 'ERR_ASSERTION',
+    message: 'Expected mock function to have been called',
+  });
+  fn();
+  t.assert.called(fn);
+  t.assert.throws(() => t.assert.called(() => {}), {
+    code: 'ERR_INVALID_ARG_TYPE',
+  });
+});
+
+test('t.assert.callCount', (t) => {
+  const fn = t.mock.fn();
+  t.assert.callCount(fn, 0);
+  fn();
+  fn();
+  t.assert.callCount(fn, 2);
+  t.assert.throws(() => t.assert.callCount(fn, 1), {
+    code: 'ERR_ASSERTION',
+    message: 'Expected mock function to have been called 1 times, but it was called 2 times',
+  });
+  t.assert.throws(() => t.assert.callCount(fn, -1), {
+    code: 'ERR_OUT_OF_RANGE',
+  });
+});
+
+test('t.assert.nthCalledWith', (t) => {
+  const fn = t.mock.fn();
+  fn(1);
+  fn({ a: 2 });
+  t.assert.nthCalledWith(fn, 1, 1);
+  t.assert.nthCalledWith(fn, 2, { a: 2 });
+  t.assert.throws(() => t.assert.nthCalledWith(fn, 2, { a: 3 }), {
+    code: 'ERR_ASSERTION',
+    message: /^Expected call 2 of mock function to have been called with:/,
+    actual: [{ a: 2 }],
+    expected: [{ a: 3 }],
+  });
+  t.assert.throws(() => t.assert.nthCalledWith(fn, 3, 1), {
+    code: 'ERR_ASSERTION',
+    message: 'Expected mock function to have been called at least 3 times, but it was called 2 times',
+  });
+  t.assert.throws(() => t.assert.nthCalledWith(fn, 0, 1), {
+    code: 'ERR_OUT_OF_RANGE',
+  });
+});
+
+test('t.assert.lastCalledWith', (t) => {
+  const fn = t.mock.fn();
+  t.assert.throws(() => t.assert.lastCalledWith(fn), {
+    code: 'ERR_ASSERTION',
+    message: 'Expected mock function to have been called',
+  });
+  fn(1);
+  fn(2);
+  t.assert.lastCalledWith(fn, 2);
+  t.assert.throws(() => t.assert.lastCalledWith(fn, 1), {
+    code: 'ERR_ASSERTION',
+    actual: [2],
+    expected: [1],
+  });
+});
+
+test('mock matchers count towards t.plan()', (t) => {
+  t.plan(4);
+  const fn = t.mock.fn();
+  fn(1);
+  t.assert.called(fn);
+  t.assert.callCount(fn, 1);
+  t.assert.nthCalledWith(fn, 1, 1);
+  t.assert.lastCalledWith(fn, 1);
+});