From 937d2273252e640784a4cb1ca7e6114b97d679aa Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 23 Oct 2026 10:13:35 +0200 Subject: [PATCH 1/3] test(node): Retry stalled npm installs in integration test setup Co-Authored-By: Claude Opus 5.5 --- .../utils/runner/createEsmAndCjsTests.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/dev-packages/node-integration-tests/utils/runner/createEsmAndCjsTests.ts b/dev-packages/node-integration-tests/utils/runner/createEsmAndCjsTests.ts index 7c6265f05c38..3742d70db24d 100644 --- a/dev-packages/node-integration-tests/utils/runner/createEsmAndCjsTests.ts +++ b/dev-packages/node-integration-tests/utils/runner/createEsmAndCjsTests.ts @@ -161,10 +161,11 @@ function setupAndCleanup( createdRunners: Set>, tmpDirPath: string, ) { - // Create tmp directory and install additionalDependencies (with retries) + // Create tmp directory and install additionalDependencies (with retries). + // Must cover all npm install attempts (see `NPM_INSTALL_TIMEOUT_MS`) plus `afterSetupCommand`. beforeAll(async () => { await createTmpDir(); - }, 120_000); + }, 240_000); // Clean up the tmp directory after both esm and cjs suites have run afterAll(async () => { @@ -332,6 +333,9 @@ function convertEsmToCjs(content: string): string { const NPM_INSTALL_MAX_RETRIES = 3; const NPM_INSTALL_RETRY_DELAY_MS = 2_000; +// npm install can stall on CI without ever failing, which would otherwise use up the whole setup hook timeout +// before a retry gets a chance to run. +const NPM_INSTALL_TIMEOUT_MS = 60_000; async function npmInstallWithRetry(cwd: string, deps: string[]): Promise { for (let attempt = 1; attempt <= NPM_INSTALL_MAX_RETRIES; attempt++) { @@ -343,6 +347,7 @@ async function npmInstallWithRetry(cwd: string, deps: string[]): Promise { { cwd, encoding: 'utf8', + timeout: NPM_INSTALL_TIMEOUT_MS, }, ); From 777a96500fd0cea0354da0110a5fad81cffde459 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 23 Oct 2026 10:17:54 +0200 Subject: [PATCH 2/3] test(node): Lower integration test setup hook timeout to 200s Co-Authored-By: Claude Opus 5.5 --- .../node-integration-tests/utils/runner/createEsmAndCjsTests.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-packages/node-integration-tests/utils/runner/createEsmAndCjsTests.ts b/dev-packages/node-integration-tests/utils/runner/createEsmAndCjsTests.ts index 3742d70db24d..f320faa55c66 100644 --- a/dev-packages/node-integration-tests/utils/runner/createEsmAndCjsTests.ts +++ b/dev-packages/node-integration-tests/utils/runner/createEsmAndCjsTests.ts @@ -165,7 +165,7 @@ function setupAndCleanup( // Must cover all npm install attempts (see `NPM_INSTALL_TIMEOUT_MS`) plus `afterSetupCommand`. beforeAll(async () => { await createTmpDir(); - }, 240_000); + }, 200_000); // Clean up the tmp directory after both esm and cjs suites have run afterAll(async () => { From 9d8e8b6cbf9dbf8bf60daf86681083c6bab9ce09 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 23 Oct 2026 10:53:55 +0200 Subject: [PATCH 3/3] test(node): Clean up partial npm installs before retrying Co-Authored-By: Claude Opus 5.5 --- .../utils/runner/createEsmAndCjsTests.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/dev-packages/node-integration-tests/utils/runner/createEsmAndCjsTests.ts b/dev-packages/node-integration-tests/utils/runner/createEsmAndCjsTests.ts index f320faa55c66..30ae5c888125 100644 --- a/dev-packages/node-integration-tests/utils/runner/createEsmAndCjsTests.ts +++ b/dev-packages/node-integration-tests/utils/runner/createEsmAndCjsTests.ts @@ -1,4 +1,4 @@ -import { exec } from 'child_process'; +import { exec, execFile } from 'child_process'; import { existsSync } from 'fs'; import { cp, mkdir, readFile, rm, writeFile } from 'fs/promises'; import { basename, delimiter, join } from 'path'; @@ -7,6 +7,7 @@ import { afterAll, beforeAll, test, type TestAPI } from 'vitest'; import { CLEANUP_STEPS, createRunner, PACKAGE_ROOT } from './createRunner'; const execPromise = promisify(exec); +const execFilePromise = promisify(execFile); interface ScenarioPaths { cjs: { @@ -340,10 +341,12 @@ const NPM_INSTALL_TIMEOUT_MS = 60_000; async function npmInstallWithRetry(cwd: string, deps: string[]): Promise { for (let attempt = 1; attempt <= NPM_INSTALL_MAX_RETRIES; attempt++) { try { - const { stdout, stderr } = await execPromise( + // Run npm without a shell, so the timeout kills npm itself rather than leaving it running next to the retry + const { stdout, stderr } = await execFilePromise( + 'npm', // We only use --prefer-offline on the first attempt to try to read from cache // in follow ups we just try to install in any way - `npm install ${attempt === 1 ? '--prefer-offline' : ''} --no-audit --no-fund`, + ['install', ...(attempt === 1 ? ['--prefer-offline'] : []), '--no-audit', '--no-fund'], { cwd, encoding: 'utf8', @@ -367,6 +370,9 @@ async function npmInstallWithRetry(cwd: string, deps: string[]): Promise { `npm install attempt ${attempt}/${NPM_INSTALL_MAX_RETRIES} failed, retrying in ${NPM_INSTALL_RETRY_DELAY_MS}ms...`, ); await new Promise(resolve => setTimeout(resolve, NPM_INSTALL_RETRY_DELAY_MS)); + // A killed attempt leaves a partial install behind, which the next attempt would treat as complete + await rm(join(cwd, 'node_modules'), { recursive: true, force: true }); + await rm(join(cwd, 'package-lock.json'), { force: true }); } else { // oxlint-disable-next-line no-console console.error(error);