From 3fb67467fa16ca8932198c8718cd49f95eacafb4 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Thu, 28 May 2026 14:34:06 +0200 Subject: [PATCH] fix(test): hard kill boundary step timeouts --- .../check-extension-package-tsc-boundary.mjs | 5 +- ...eck-extension-package-tsc-boundary.test.ts | 49 +++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/scripts/check-extension-package-tsc-boundary.mjs b/scripts/check-extension-package-tsc-boundary.mjs index b8aaf04263f..675518ffe2c 100644 --- a/scripts/check-extension-package-tsc-boundary.mjs +++ b/scripts/check-extension-package-tsc-boundary.mjs @@ -337,9 +337,10 @@ function abortSiblingSteps(abortController) { export function runNodeStepAsync(label, args, timeoutMs, params = {}) { const abortController = params.abortController; const onFailure = params.onFailure; + const spawnImpl = params.spawnImpl ?? spawn; const startedAt = Date.now(); return new Promise((resolvePromise, rejectPromise) => { - const child = spawn(process.execPath, args, { + const child = spawnImpl(process.execPath, args, { cwd: repoRoot, env: process.env, signal: abortController?.signal, @@ -353,8 +354,8 @@ export function runNodeStepAsync(label, args, timeoutMs, params = {}) { if (settled) { return; } - child.kill("SIGTERM"); settled = true; + child.kill("SIGKILL"); const error = attachStepFailureMetadata( new Error( formatStepFailure(label, { diff --git a/test/scripts/check-extension-package-tsc-boundary.test.ts b/test/scripts/check-extension-package-tsc-boundary.test.ts index ca37ace6d1d..73b84c93d44 100644 --- a/test/scripts/check-extension-package-tsc-boundary.test.ts +++ b/test/scripts/check-extension-package-tsc-boundary.test.ts @@ -35,6 +35,14 @@ function writeCanaryArtifacts(rootDir: string, extensionId = "demo") { return { canaryPath, tsconfigPath }; } +function createMockPipe() { + const pipe = new EventEmitter() as EventEmitter & { + setEncoding: (encoding: string) => void; + }; + pipe.setEncoding = () => {}; + return pipe; +} + afterEach(() => { for (const rootDir of tempRoots) { fs.rmSync(rootDir, { force: true, recursive: true }); @@ -348,6 +356,47 @@ describe("check-extension-package-tsc-boundary", () => { expect(elapsedMs).toBeGreaterThanOrEqual(0); }, 30_000); + it("hard-kills timed out async node steps", async () => { + const signals: Array = []; + const child = new EventEmitter() as EventEmitter & { + kill: (signal?: NodeJS.Signals | number) => boolean; + stderr: ReturnType; + stdout: ReturnType; + }; + child.stdout = createMockPipe(); + child.stderr = createMockPipe(); + child.kill = (signal) => { + signals.push(signal); + return true; + }; + + const failure = await runNodeStepAsync( + "hung-plugin", + ["--eval", "setTimeout(() => {}, 60_000)"], + 5, + { + spawnImpl(command: string, args: string[]) { + expect(command).toBe(process.execPath); + expect(args).toEqual(["--eval", "setTimeout(() => {}, 60_000)"]); + return child; + }, + }, + ).then( + () => { + throw new Error("expected hung-plugin step to time out"); + }, + (error: unknown) => error, + ); + + expect(signals).toEqual(["SIGKILL"]); + expect(failure).toBeInstanceOf(Error); + if (!(failure instanceof Error)) { + throw new Error("expected timeout failure to reject with an Error"); + } + expect(failure.message).toContain("hung-plugin timed out after 5ms"); + expect((failure as { kind?: unknown }).kind).toBe("timeout"); + }); + it("aborts concurrent sibling steps after the first failure", async () => { const startedAt = Date.now(); const slowStepTimeoutMs = 60_000;