From 78a54e1fb883ae0111532abd46cbcbae28a192d9 Mon Sep 17 00:00:00 2001 From: Shelley Vohr Date: Fri, 4 Sep 2026 07:56:22 +0000 Subject: [PATCH 1/5] test: accept EPIPE for the second send in fork-closed-channel test test-child-process-fork-closed-channel-segfault sends a second handle to a worker that exits on the first message and already ignores the errors that race produces (ERR_IPC_CHANNEL_CLOSED, ECONNRESET, ECONNREFUSED, EMFILE). When the worker is gone before the write reaches the IPC pipe, Linux and AIX report the write itself as EPIPE instead, which the callback rethrew. Treat it like the other "worker already exited" errors. Signed-off-by: Shelley Vohr --- .../test-child-process-fork-closed-channel-segfault.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/parallel/test-child-process-fork-closed-channel-segfault.js b/test/parallel/test-child-process-fork-closed-channel-segfault.js index 73bd118ddce..2f57ab8984d 100644 --- a/test/parallel/test-child-process-fork-closed-channel-segfault.js +++ b/test/parallel/test-child-process-fork-closed-channel-segfault.js @@ -78,7 +78,8 @@ const server = net if (err && err.code !== 'ERR_IPC_CHANNEL_CLOSED' && err.code !== 'ECONNRESET' && err.code !== 'ECONNREFUSED' && - err.code !== 'EMFILE') { + err.code !== 'EMFILE' && + err.code !== 'EPIPE') { throw err; } }); From eb20cfeaf9518293373a8620d2f9c3b4e06a143f Mon Sep 17 00:00:00 2001 From: Shelley Vohr Date: Fri, 4 Sep 2026 07:56:29 +0000 Subject: [PATCH 2/5] test: ignore tunnel resets in proxy invalid-char-in-url test test-https-proxy-request-invalid-char-in-url is the only client-proxy test that asserts the proxy logged no socket errors at all. Once the last response has been read the client destroys its tunnel, and if the proxy is still relaying the upstream's TLS close_notify at that point the client answers with a reset, which the proxy records as ECONNRESET on the CONNECT socket. That has been failing the test on macOS even though every request was routed to the sanitized URL. Keep asserting on other errors but leave connection resets out. Signed-off-by: Shelley Vohr --- .../test-https-proxy-request-invalid-char-in-url.mjs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/client-proxy/test-https-proxy-request-invalid-char-in-url.mjs b/test/client-proxy/test-https-proxy-request-invalid-char-in-url.mjs index 888137b9a5a..6f27f0775ae 100644 --- a/test/client-proxy/test-https-proxy-request-invalid-char-in-url.mjs +++ b/test/client-proxy/test-https-proxy-request-invalid-char-in-url.mjs @@ -83,7 +83,9 @@ for (const testCase of testCases) { server.close(); assert.deepStrictEqual(requests, expectedUrls); const requestLogs = logs.filter((log) => !('error' in log)); - const errors = logs.filter((log) => 'error' in log); + // The client may reset a tunnel while the proxy is still relaying + // the upstream's TLS shutdown; that says nothing about the URLs. + const errors = logs.filter((log) => 'error' in log && log.error.code !== 'ECONNRESET'); assert.deepStrictEqual(new Set(requestLogs), expectedProxyLogs); assert.deepStrictEqual(errors, []); })); From fd03de03dd05a489564e47112413e5d721e315a8 Mon Sep 17 00:00:00 2001 From: Shelley Vohr Date: Fri, 4 Sep 2026 08:07:27 +0000 Subject: [PATCH 3/5] test: do not dump core in external memory limit test test-external-memory-reasonable-size makes a child allocate 1.2 GB of external memory so that V8's --external-memory-max-reasonable-size check fires and the process aborts. The abort raises SIGABRT with all of that memory resident, and on hosts that write core files (the SmartOS CI machines in particular) the dump takes longer than the test timeout, so the test has been timing out there since it was added. Run the child under `ulimit -c 0` on POSIX, the same way test-abort-fatal-error and common.childShouldThrowAndAbort() handle their aborting children. Refs: https://github.com/nodejs/node/pull/65589 Signed-off-by: Shelley Vohr --- .../test-external-memory-reasonable-size.js | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/test/parallel/test-external-memory-reasonable-size.js b/test/parallel/test-external-memory-reasonable-size.js index 14e2573328b..f9ff1af8e5a 100644 --- a/test/parallel/test-external-memory-reasonable-size.js +++ b/test/parallel/test-external-memory-reasonable-size.js @@ -8,7 +8,7 @@ const common = require('../common'); const assert = require('assert'); -const { spawnSync } = require('child_process'); +const { execSync } = require('child_process'); const { totalmem } = require('os'); // The smallest limit V8 accepts is 1 GB, so the child has to allocate more @@ -20,14 +20,15 @@ for (const flag of [ '--external-memory-max-reasonable-size=1', '--external_memory_max_reasonable_size=1', ]) { - const child = spawnSync(process.execPath, [ - flag, '-e', 'new Float64Array(150_000_000)', - ]); - - assert.notStrictEqual( - child.status, - 0, - `${flag} was not honored, the child exited cleanly`, + // The child aborts with over a gigabyte resident, so keep it from writing a + // core file; on some hosts that dump alone outlasts the test timeout. + const [cmd, opts] = common.escapePOSIXShell`"${process.execPath}" ${flag} -e "new Float64Array(150_000_000)"`; + assert.throws( + () => execSync(common.isWindows ? cmd : `ulimit -c 0; ${cmd}`, { ...opts, stdio: 'pipe' }), + (err) => { + assert.notStrictEqual(err.status, 0, `${flag} was not honored, the child exited cleanly`); + assert.match(err.stderr.toString(), /kMaxReasonableBytes/); + return true; + }, ); - assert.match(child.stderr.toString(), /kMaxReasonableBytes/); } From 0de7f6f0acf898f115eb44d88bdec6e669c61063 Mon Sep 17 00:00:00 2001 From: Shelley Vohr Date: Fri, 4 Sep 2026 08:22:22 +0000 Subject: [PATCH 4/5] test: only count restarts after the write in watch emit-restarted test test-run-watch-emit-restarted expected exactly one test:watch:restarted event, but it starts run({ watch: true }) right after writing the fixtures into the watched directory. Watch backends that deliver events with some latency, FSEvents on macOS most visibly, can still report those setup writes once the first run is under way, which restarts it and makes the later, intentional write the second restart. The test has been marked flaky on macOS x64 for that reason. Wait for the first drain, then require that the write is followed by a restart and a drain, ignoring whatever the setup produced before it, and drop the flaky marker. Refs: https://github.com/nodejs/node/issues/54534 Signed-off-by: Shelley Vohr --- .../test-run-watch-emit-restarted.mjs | 28 ++++++++----------- test/test-runner/test-runner.status | 1 - 2 files changed, 12 insertions(+), 17 deletions(-) diff --git a/test/test-runner/test-run-watch-emit-restarted.mjs b/test/test-runner/test-run-watch-emit-restarted.mjs index 200231c6904..fd596bd818c 100644 --- a/test/test-runner/test-run-watch-emit-restarted.mjs +++ b/test/test-runner/test-run-watch-emit-restarted.mjs @@ -1,5 +1,5 @@ // Test run({ watch: true }) emits test:watch:restarted when file is updated -import * as common from '../common/index.mjs'; +import '../common/index.mjs'; import { run } from 'node:test'; import assert from 'node:assert'; import { writeFileSync } from 'node:fs'; @@ -11,9 +11,8 @@ import { refreshForTestRunnerWatch, skipIfNoWatch, fixtureContent } from '../com skipIfNoWatch(); refreshForTestRunnerWatch(); -let alreadyDrained = false; const events = []; -const testWatchRestarted = common.mustCall(1); +let written = false; const controller = new AbortController(); const stream = run({ @@ -21,27 +20,24 @@ const stream = run({ watch: true, signal: controller.signal, }).on('data', function({ type }) { - events.push(type); - if (type === 'test:watch:restarted') { - testWatchRestarted(); + if (type !== 'test:watch:restarted' && type !== 'test:watch:drained') { + return; } - if (type === 'test:watch:drained') { - if (alreadyDrained) { - controller.abort(); - } - alreadyDrained = true; + events.push(type); + // Watchers with latency (FSEvents) can still report the fixture setup after + // the first run has started, so only a restart after the write below counts. + if (written && type === 'test:watch:drained' && events.at(-2) === 'test:watch:restarted') { + controller.abort(); } }); await once(stream, 'test:watch:drained'); +events.length = 0; +written = true; writeFileSync(join(tmpdir.path, 'test.js'), fixtureContent['test.js']); // eslint-disable-next-line no-unused-vars for await (const _ of stream); -assert.partialDeepStrictEqual(events, [ - 'test:watch:drained', - 'test:watch:restarted', - 'test:watch:drained', -]); +assert.deepStrictEqual(events.slice(-2), ['test:watch:restarted', 'test:watch:drained']); diff --git a/test/test-runner/test-runner.status b/test/test-runner/test-runner.status index 149d6ab689c..7d2d59d3540 100644 --- a/test/test-runner/test-runner.status +++ b/test/test-runner/test-runner.status @@ -14,4 +14,3 @@ test-watch-create-isolation-none: SKIP # https://github.com/nodejs/node/issues/54534#issuecomment-5423551021 test-run-watch-cwd-isolation-none: PASS, FLAKY test-run-watch-cwd-isolation-none-argv: PASS, FLAKY -test-run-watch-emit-restarted: PASS, FLAKY From ce80b32daa3ee8a56a5a29c20fe374858097badb Mon Sep 17 00:00:00 2001 From: Shelley Vohr Date: Fri, 4 Sep 2026 08:24:27 +0000 Subject: [PATCH 5/5] test: fix the thread-spawn handshake in the WASI threads fixture test-wasi-pthread fails now and then on every platform with "Assertion failed: r == 0 (c/pthread.c: main: 17)", i.e. pthread_create() itself reporting an error. The fixture implements `thread-spawn` by starting a Worker and blocking in Atomics.wait(result, 0, 0, 1000) until the worker signals that it has instantiated the module. Two things go wrong there: the worker signals success by storing 0, the value the main thread is already waiting on, so when the worker is quicker than the main thread its notify is lost and the wait runs into the timeout; and one second is not always enough for a Worker to start and instantiate a threads build on the slower CI hosts (arm debug, Windows, macOS). Either way spawn() returns -6 and wasi-libc turns that into a pthread_create() failure. Wait on a sentinel value that neither outcome writes, and give the worker a platform-scaled 30 seconds. Drop the flaky markers. Fixes: https://github.com/nodejs/node/issues/64226 Refs: https://github.com/nodejs/node/issues/59146 Signed-off-by: Shelley Vohr --- test/fixtures/wasi-preview-1.js | 5 ++++- test/wasi/wasi.status | 7 ------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/test/fixtures/wasi-preview-1.js b/test/fixtures/wasi-preview-1.js index 535bc3e5ec1..3fde5a30b75 100644 --- a/test/fixtures/wasi-preview-1.js +++ b/test/fixtures/wasi-preview-1.js @@ -68,6 +68,9 @@ assert.strictEqual(wasiPreview1.wasiImport, const name = `pthread-${tid}`; const sab = new SharedArrayBuffer(8 + 8192); const result = new Int32Array(sab); + // The thread stores 0 once it has loaded or 1 with an error; wait on a + // value neither of them writes so an early notify cannot be missed. + Atomics.store(result, 0, -1); const workerData = { name, @@ -106,7 +109,7 @@ assert.strictEqual(wasiPreview1.wasiImport, throw new Error(e); }); - const r = Atomics.wait(result, 0, 0, 1000); + const r = Atomics.wait(result, 0, -1, common.platformTimeout(30_000)); if (r === 'timed-out') { workers[tid].terminate(); delete workers[tid]; diff --git a/test/wasi/wasi.status b/test/wasi/wasi.status index 14b671b6e3b..a26617e0136 100644 --- a/test/wasi/wasi.status +++ b/test/wasi/wasi.status @@ -15,10 +15,3 @@ test-wasi-getrusage: SKIP # Unsupported on Windows and Android test-wasi-readdir: SKIP -[$system==win32 || $system==macos] -# https://github.com/nodejs/node/issues/64226#issuecomment-5423585588 -test-wasi-pthread: PASS, FLAKY - -[$system==linux] -# https://github.com/nodejs/node/issues/59146 -test-wasi-pthread: PASS, FLAKY