Repository: nodejs/node
Pull request: test_runner: add `getTestContext()` public API

Exposes `getTestContext()` function to access test context information from within tests and async operations.

My use case was exposing a pino logger that writes to test diagnostics, but there can be many other usecases:

```mjs
import { prettyFactory } from 'pino-pretty';

const pretty = prettyFactory({ colorize: true  });
const asyncId = executionAsyncId();
const t = testResources.get(asyncId);
t.diagnostic(pretty(obj).replace(/\n$/, ''));
```

Changed files:
filename: doc/api/test.md
patch: @@ -3563,6 +3563,44 @@ Emitted when no more tests are queued for execution in watch mode.
 
 Emitted when one or more tests are restarted due to a file change in watch mode.
 
+## `getTestContext()`
+
+<!-- YAML
+added: REPLACEME
+-->
+
+* Returns: {TestContext|SuiteContext|undefined}
+
+Returns the [`TestContext`][] or [`SuiteContext`][] object associated with the
+currently executing test or suite, or `undefined` if called outside of a test or
+suite. This function can be used to access context information from within the
+test or suite function or any async operations within them.
+
+```mjs
+import { getTestContext } from 'node:test';
+
+test('example test', async () => {
+  const ctx = getTestContext();
+  console.log(`Running test: ${ctx.name}`);
+});
+
+describe('example suite', () => {
+  const ctx = getTestContext();
+  console.log(`Running suite: ${ctx.name}`);
+});
+```
+
+When called from a test, returns a [`TestContext`][].
+When called from a suite, returns a [`SuiteContext`][].
+
+If called from outside a test or suite (e.g., at the top level of a module or in
+a setTimeout callback after execution has completed), this function returns
+`undefined`.
+
+When called from within a hook (before, beforeEach, after, afterEach), this
+function returns the context of the test or suite that the hook is associated
+with.
+
 ## Class: `TestContext`
 
 <!-- YAML

filename: lib/internal/test_runner/harness.js
patch: @@ -21,7 +21,7 @@ const {
   },
 } = require('internal/errors');
 const { exitCodes: { kGenericUserError } } = internalBinding('errors');
-const { kCancelledByParent, Test, Suite } = require('internal/test_runner/test');
+const { kCancelledByParent, Test, Suite, TestContext, SuiteContext } = require('internal/test_runner/test');
 const {
   parseCommandLine,
   reporterScope,
@@ -431,8 +431,27 @@ function hook(hook) {
   };
 }
 
+function getTestContext() {
+  const test = testResources.get(executionAsyncId());
+  // Exclude the reporter sentinel
+  if (test === undefined || test === reporterScope) {
+    return undefined;
+  }
+  // For hooks (hookType is set), return the test/suite being hooked (the parent)
+  const actualTest = test.hookType !== undefined ? test.parent : test;
+  if (actualTest === undefined) {
+    return undefined;
+  }
+  // Return SuiteContext for suites, TestContext for tests
+  if (actualTest instanceof Suite) {
+    return new SuiteContext(actualTest);
+  }
+  return new TestContext(actualTest);
+}
+
 module.exports = {
   createTestTree,
+  getTestContext,
   test: runInParentContext(Test),
   suite: runInParentContext(Suite),
   before: hook('before'),

filename: lib/internal/test_runner/test.js
patch: @@ -1656,4 +1656,6 @@ module.exports = {
   kUnwrapErrors,
   Suite,
   Test,
+  TestContext,
+  SuiteContext,
 };

filename: lib/test.js
patch: @@ -5,7 +5,7 @@ const {
   ObjectDefineProperty,
 } = primordials;
 
-const { test, suite, before, after, beforeEach, afterEach } = require('internal/test_runner/harness');
+const { test, suite, before, after, beforeEach, afterEach, getTestContext } = require('internal/test_runner/harness');
 const { run } = require('internal/test_runner/runner');
 
 module.exports = test;
@@ -15,6 +15,7 @@ ObjectAssign(module.exports, {
   before,
   beforeEach,
   describe: suite,
+  getTestContext,
   it: test,
   run,
   suite,

filename: test/parallel/test-runner-get-test-context.js
patch: @@ -0,0 +1,54 @@
+'use strict';
+require('../common');
+const assert = require('node:assert');
+const { test, getTestContext, describe, it } = require('node:test');
+
+// Outside a test — must return undefined
+assert.strictEqual(getTestContext(), undefined);
+
+test('getTestContext returns current context inside test', async () => {
+  const ctx = getTestContext();
+  assert.ok(ctx !== undefined);
+  assert.strictEqual(ctx.name, 'getTestContext returns current context inside test');
+  assert.strictEqual(typeof ctx.signal, 'object');
+  assert.strictEqual(typeof ctx.fullName, 'string');
+});
+
+test('getTestContext works in nested test', async (t) => {
+  await t.test('child', async () => {
+    const ctx = getTestContext();
+    assert.ok(ctx !== undefined);
+    assert.strictEqual(ctx.name, 'child');
+  });
+});
+
+describe('getTestContext works in describe/it', () => {
+  it('has correct name', () => {
+    const ctx = getTestContext();
+    assert.ok(ctx !== undefined);
+    assert.strictEqual(ctx.name, 'has correct name');
+  });
+});
+
+describe('getTestContext returns SuiteContext in suite', () => {
+  it('suite context is available', () => {
+    const ctx = getTestContext();
+    assert.ok(ctx !== undefined);
+    // Suite name appears as parent in nested test context
+    assert.strictEqual(typeof ctx.signal, 'object');
+    assert.strictEqual(typeof ctx.fullName, 'string');
+  });
+});
+
+test('getTestContext works in test body during async operations', async (t) => {
+  const ctx = getTestContext();
+  assert.ok(ctx !== undefined);
+  assert.strictEqual(ctx.name, 'getTestContext works in test body during async operations');
+
+  // Also works in nested async context
+  const ctxInSetImmediate = await new Promise((resolve) => {
+    setImmediate(() => resolve(getTestContext()));
+  });
+  assert.ok(ctxInSetImmediate !== undefined);
+  assert.strictEqual(ctxInSetImmediate.name, 'getTestContext works in test body during async operations');
+});