Commit 0ded159ee15 for nodejs

commit 0ded159ee158f259c007c7afd340db4b593d6d17
Author: Filip Skokan <panva.ip@gmail.com>
Date:   Sat Sep 26 13:22:26 2026 +0200

    test: honor allowed WPT promise rejections

    Honor allow_uncaught_exception for unhandled promise rejections in both
    WPT worker paths. Apply setup settings before nested callbacks, preserve
    settings after results start, and apply promise_setup settings when its
    callback runs. Keep rejection failures in single_test mode.

    Cover both runner backends and Web Workers, and remove the flaky
    expectation for the Web Locks test that deliberately rejects.

    Refs: https://github.com/nodejs/node/issues/59142
    Signed-off-by: Filip Skokan <panva.ip@gmail.com>
    Assisted-by: Claude, Codex
    PR-URL: https://github.com/nodejs/node/pull/66322
    Reviewed-By: Daeyeon Jeong <daeyeon.dev@gmail.com>

diff --git a/test/common/wpt/rejections.js b/test/common/wpt/rejections.js
new file mode 100644
index 00000000000..f0cda7a4c60
--- /dev/null
+++ b/test/common/wpt/rejections.js
@@ -0,0 +1,65 @@
+'use strict';
+
+// Node.js does not dispatch the global unhandledrejection event used by
+// testharness.js. Ignore rejections only when the harness settings allow them.
+module.exports = function honorAllowedRejections() {
+  const { setup, promise_setup, add_result_callback, add_completion_callback } = globalThis;
+  let configurable = true;
+  let allowed = false;
+  let singleTest = false;
+  const ignoreRejection = () => {};
+
+  // The harness ignores configuration changes once results have started.
+  add_result_callback(() => { configurable = false; });
+  add_completion_callback(() => { configurable = false; });
+
+  function apply(properties) {
+    if (properties) {
+      if (Object.prototype.propertyIsEnumerable.call(properties, 'allow_uncaught_exception')) {
+        allowed = properties.allow_uncaught_exception;
+      }
+      if (Object.prototype.propertyIsEnumerable.call(properties, 'single_test') && properties.single_test) {
+        singleTest = true;
+      }
+    }
+    process.removeListener('unhandledRejection', ignoreRejection);
+    // In single_test mode the harness still fails the implicit test when
+    // uncaught errors are allowed. Retain Node's failure path for that mode.
+    if (allowed && !singleTest) {
+      process.on('unhandledRejection', ignoreRejection);
+    }
+  }
+
+  globalThis.setup = function(funcOrProperties, maybeProperties) {
+    let func;
+    let properties;
+    if (arguments.length === 2) {
+      func = funcOrProperties;
+      properties = maybeProperties;
+    } else if (funcOrProperties instanceof Function) {
+      func = funcOrProperties;
+      properties = {};
+    } else {
+      properties = funcOrProperties;
+    }
+
+    // This callback runs only when setup was accepted, before any nested
+    // setup calls made by the test's callback can override its settings.
+    return setup.call(this, () => {
+      apply(properties);
+      if (func) func();
+    }, properties);
+  };
+
+  globalThis.promise_setup = function(func, properties) {
+    if (typeof func !== 'function') {
+      return promise_setup.call(this, func, properties);
+    }
+    return promise_setup.call(this, () => {
+      // promise_setup applies settings when its queued callback executes.
+      // Unlike setup, it still invokes that callback after results start.
+      if (configurable) apply(properties);
+      return func();
+    }, properties);
+  };
+};
diff --git a/test/common/wpt/webworker.js b/test/common/wpt/webworker.js
index e9a8ebe53d7..8a335c80869 100644
--- a/test/common/wpt/webworker.js
+++ b/test/common/wpt/webworker.js
@@ -4,6 +4,7 @@
 // Refs: https://web-platform-tests.org/writing-tests/testharness.html

 const { pathToFileURL } = require('url');
+const honorAllowedRejections = require('./rejections');
 const {
   runInThisContext,
   constants: { USE_MAIN_CONTEXT_DEFAULT_LOADER },
@@ -88,6 +89,7 @@ globalThis.onmessage = ({ data }) => {
     const result = realImportScripts.apply(this, mapped);
     if (mapped.includes(testharnessPath)) {
       applySkips();
+      honorAllowedRejections();
     }
     return result;
   };
diff --git a/test/common/wpt/worker.js b/test/common/wpt/worker.js
index ca4f4706f89..7d84f2ac1a3 100644
--- a/test/common/wpt/worker.js
+++ b/test/common/wpt/worker.js
@@ -9,6 +9,7 @@ const {
 } = require('vm');
 const { setFlagsFromString } = require('v8');
 const { inspect } = require('util');
+const honorAllowedRejections = require('./rejections');
 const {
   isMainThread,
   parentPort,
@@ -79,6 +80,7 @@ function run(workerData) {
     filename: workerData.harness.filename,
     importModuleDynamically: USE_MAIN_CONTEXT_DEFAULT_LOADER,
   });
+  honorAllowedRejections();

   // If there are skip patterns, wrap test functions to prevent execution of
   // matching tests. This must happen after testharness.js is loaded but before
diff --git a/test/parallel/test-common-wpt-allowed-rejections.js b/test/parallel/test-common-wpt-allowed-rejections.js
new file mode 100644
index 00000000000..e8f8ed113f3
--- /dev/null
+++ b/test/parallel/test-common-wpt-allowed-rejections.js
@@ -0,0 +1,189 @@
+'use strict';
+
+const common = require('../common');
+const fixtures = require('../common/fixtures');
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+const { backends } = require('../common/wpt');
+
+const harnessPath = fixtures.path('wpt', 'resources', 'testharness.js');
+const harness = {
+  code: fs.readFileSync(harnessPath, 'utf8'),
+  filename: harnessPath,
+};
+
+async function check(backend, webWorker, {
+  name,
+  code,
+  allowed,
+  passes = 1,
+  singleTest = false,
+  message = /uncaught rejection probe/,
+}) {
+  const label = `${backend}, ${webWorker}, ${name}`;
+  const script = {
+    filename: fixtures.path('wpt-rejection-probe.js'),
+    // Use a direct worker script for single_test: .any.js wrappers call
+    // done() automatically, completing the implicit test before its timers.
+    code: `${webWorker && singleTest ? 'importScripts("/resources/testharness.js");' : ''}\n${code}`,
+  };
+  const workerData = {
+    testRelativePath: 'rejection-probe.any.js',
+    wptRunner: path.join(__dirname, '../common/wpt.js'),
+    wptPath: 'compression',
+    harness,
+    scriptsToRun: webWorker ? [] : [script],
+    webWorker: webWorker ? {
+      path: script.filename,
+      modifiedScript: script,
+      isAnyTest: !singleTest,
+      variant: '',
+      scripts: [],
+    } : undefined,
+  };
+
+  let result;
+  const statuses = [];
+  const handle = backends[backend](['--experimental-web-worker'], workerData, {
+    message(message) {
+      if (message.type === 'result') {
+        statuses.push(message.result.status);
+      } else if (message.type === 'completion') {
+        // The process backend can complete before its uncaught-error handler
+        // exits. Preserve any failure it already reported.
+        result ||= message.status;
+        handle.kill();
+      } else {
+        assert.fail(`Unexpected message type: ${message.type}`);
+      }
+    },
+    failure(error) {
+      if (result) return false;
+      result = { status: 1, message: error.message };
+      return true;
+    },
+  });
+  const timeout = setTimeout(() => {
+    handle.kill();
+    assert.fail(`WPT rejection probe did not finish: ${label}`);
+  }, common.platformTimeout(10_000));
+
+  try {
+    await handle.finished;
+  } finally {
+    clearTimeout(timeout);
+  }
+
+  assert(result, label);
+  assert.strictEqual(result.status, allowed ? 0 : 1, label);
+  if (allowed) {
+    assert.deepStrictEqual(statuses, Array(passes).fill(0), label);
+  } else {
+    assert.match(result.message, message, label);
+  }
+}
+
+(async () => {
+  for (const backend of ['thread', 'process']) {
+    for (const webWorker of [false, true]) {
+      for (const [setup, allowed] of [
+        ['', false],
+        ['setup({ allow_uncaught_exception: false });', false],
+        ['setup({ allow_uncaught_exception: true });', true],
+        ['setup(() => {}, { allow_uncaught_exception: true });', true],
+        ['setup(null, { allow_uncaught_exception: true });', true],
+        ['setup({ allow_uncaught_exception: true }); setup({});', true],
+        ['setup({ allow_uncaught_exception: true }); setup({ allow_uncaught_exception: false });', false],
+        ['setup(Object.defineProperty({}, "allow_uncaught_exception", { value: true }));', false],
+      ]) {
+        await check(backend, webWorker, {
+          name: setup || 'default settings',
+          code: `${setup}
+            const t = async_test('rejection probe');
+            Promise.reject(new Error('uncaught rejection probe'));
+            setTimeout(() => t.done(), 0);`,
+          allowed,
+        });
+      }
+
+      for (const allowed of [false, true]) {
+        await check(backend, webWorker, {
+          name: `nested setup allows rejection: ${allowed}`,
+          code: `setup(() => setup({ allow_uncaught_exception: ${allowed} }),
+                      { allow_uncaught_exception: ${!allowed} });
+            const t = async_test('rejection probe');
+            Promise.reject(new Error('uncaught rejection probe'));
+            setTimeout(() => t.done(), 0);`,
+          allowed,
+        });
+
+        await check(backend, webWorker, {
+          name: `late setup preserves allowance: ${allowed}`,
+          code: `setup({ allow_uncaught_exception: ${allowed} });
+            const t = async_test('rejection probe');
+            test(() => {}, 'first result');
+            setup(() => assert_unreached('late setup callback'),
+                  { allow_uncaught_exception: ${!allowed} });
+            Promise.reject(new Error('uncaught rejection probe'));
+            setTimeout(() => t.done(), 0);`,
+          allowed,
+          passes: 2,
+        });
+
+        await check(backend, webWorker, {
+          name: `promise_setup applies deferred allowance: ${allowed}`,
+          code: `promise_setup(async () => {}, { allow_uncaught_exception: ${allowed} });
+            setup({ allow_uncaught_exception: ${!allowed} });
+            promise_test(async () => {
+              Promise.reject(new Error('uncaught rejection probe'));
+              await new Promise(resolve => setTimeout(resolve, 0));
+            }, 'rejection probe');`,
+          allowed,
+        });
+
+        await check(backend, webWorker, {
+          name: `late promise_setup preserves allowance: ${allowed}`,
+          code: `setup({ allow_uncaught_exception: ${allowed} });
+            const t = async_test('rejection probe');
+            test(() => {}, 'first result');
+            promise_setup(async () => {
+              Promise.reject(new Error('uncaught rejection probe'));
+              setTimeout(() => t.done(), 0);
+            }, { allow_uncaught_exception: ${!allowed} });`,
+          allowed,
+          passes: 2,
+        });
+
+        await check(backend, webWorker, {
+          name: `single_test still fails with allowance: ${allowed}`,
+          code: `setup({ single_test: true, allow_uncaught_exception: ${allowed} });
+            setup({ single_test: false });
+            Promise.reject(new Error('uncaught rejection probe'));
+            setTimeout(done, 0);`,
+          allowed: false,
+          singleTest: true,
+        });
+      }
+
+      for (const setup of [
+        'setup(() => { throw new Error("setup failure"); }, { allow_uncaught_exception: true });',
+        'promise_setup(async () => { throw new Error("setup failure"); }, { allow_uncaught_exception: true });',
+      ]) {
+        await check(backend, webWorker, {
+          name: setup,
+          code: `${setup}\npromise_test(async () => {}, 'setup must succeed first');`,
+          allowed: false,
+          message: /setup failure/,
+        });
+      }
+
+      await check(backend, webWorker, {
+        name: 'unexpected rejection fails even with a permanently pending test',
+        code: `async_test('pending forever');
+          Promise.reject(new Error('uncaught rejection probe'));`,
+        allowed: false,
+      });
+    }
+  }
+})().then(common.mustCall());
diff --git a/test/wpt/status/web-locks.json b/test/wpt/status/web-locks.json
index 97a0a7900d6..f7774a38fa7 100644
--- a/test/wpt/status/web-locks.json
+++ b/test/wpt/status/web-locks.json
@@ -1,12 +1,4 @@
 {
-  "held.https.any.js": {
-    "fail": {
-      "note": "Flaky on ppc, linux x64 and s390x #59142",
-      "flaky": [
-        "Error: this uncaught rejection is expected"
-      ]
-    }
-  },
   "idlharness.https.any.js": {
     "fail": {
       "expected": [