From 6add2bcc1528e9ab90dcb9eb602dadd0e8d0d2b0 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 2 Mar 2026 19:16:18 +0000 Subject: [PATCH] test(process): replace no-output timer subprocess with spawn mock --- src/process/exec.no-output-timer.test.ts | 73 ++++++++++++++++++++++++ src/process/exec.test.ts | 32 ----------- 2 files changed, 73 insertions(+), 32 deletions(-) create mode 100644 src/process/exec.no-output-timer.test.ts diff --git a/src/process/exec.no-output-timer.test.ts b/src/process/exec.no-output-timer.test.ts new file mode 100644 index 00000000000..9c851f1e1a2 --- /dev/null +++ b/src/process/exec.no-output-timer.test.ts @@ -0,0 +1,73 @@ +import type { ChildProcess } from "node:child_process"; +import { EventEmitter } from "node:events"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +const spawnMock = vi.hoisted(() => vi.fn()); + +vi.mock("node:child_process", async () => { + const actual = await vi.importActual("node:child_process"); + return { + ...actual, + spawn: spawnMock, + }; +}); + +import { runCommandWithTimeout } from "./exec.js"; + +function createFakeSpawnedChild() { + const child = new EventEmitter() as EventEmitter & ChildProcess; + const stdout = new EventEmitter(); + const stderr = new EventEmitter(); + let killed = false; + const kill = vi.fn<(signal?: NodeJS.Signals) => boolean>(() => { + killed = true; + return true; + }); + Object.defineProperty(child, "killed", { + get: () => killed, + configurable: true, + }); + Object.defineProperty(child, "pid", { + value: 12345, + configurable: true, + }); + child.stdout = stdout as ChildProcess["stdout"]; + child.stderr = stderr as ChildProcess["stderr"]; + child.stdin = null; + child.kill = kill as ChildProcess["kill"]; + return { child, stdout, stderr, kill }; +} + +describe("runCommandWithTimeout no-output timer", () => { + afterEach(() => { + vi.useRealTimers(); + vi.restoreAllMocks(); + }); + + it("resets no-output timeout when spawned child keeps emitting stdout", async () => { + vi.useFakeTimers(); + const fake = createFakeSpawnedChild(); + spawnMock.mockReturnValue(fake.child); + + const runPromise = runCommandWithTimeout(["node", "-e", "ignored"], { + timeoutMs: 1_000, + noOutputTimeoutMs: 80, + }); + + fake.stdout.emit("data", Buffer.from(".")); + await vi.advanceTimersByTimeAsync(40); + fake.stdout.emit("data", Buffer.from(".")); + await vi.advanceTimersByTimeAsync(40); + fake.stdout.emit("data", Buffer.from(".")); + await vi.advanceTimersByTimeAsync(20); + + fake.child.emit("close", 0, null); + const result = await runPromise; + + expect(result.code ?? 0).toBe(0); + expect(result.termination).toBe("exit"); + expect(result.noOutputTimedOut).toBe(false); + expect(result.stdout).toBe("..."); + expect(fake.kill).not.toHaveBeenCalled(); + }); +}); diff --git a/src/process/exec.test.ts b/src/process/exec.test.ts index 78fc8015279..6f2c3640c11 100644 --- a/src/process/exec.test.ts +++ b/src/process/exec.test.ts @@ -56,38 +56,6 @@ describe("runCommandWithTimeout", () => { expect(result.code).not.toBe(0); }); - it("resets no output timer when command keeps emitting output", async () => { - const result = await runCommandWithTimeout( - [ - process.execPath, - "-e", - [ - "let count = 0;", - "const emit = () => {", - 'process.stdout.write(".");', - "count += 1;", - "if (count >= 4) {", - "process.exit(0);", - "return;", - "}", - "setTimeout(emit, 40);", - "};", - "emit();", - ].join(" "), - ], - { - timeoutMs: 2_000, - // Keep a healthy margin above the emit interval for loaded CI runners. - noOutputTimeoutMs: 400, - }, - ); - - expect(result.code ?? 0).toBe(0); - expect(result.termination).toBe("exit"); - expect(result.noOutputTimedOut).toBe(false); - expect(result.stdout.length).toBeGreaterThanOrEqual(3); - }); - it("reports global timeout termination when overall timeout elapses", async () => { const result = await runCommandWithTimeout( [process.execPath, "-e", "setTimeout(() => {}, 10)"],