diff --git a/src/process/exec.ts b/src/process/exec.ts index 9af3c909a8b..94bb80aa3df 100644 --- a/src/process/exec.ts +++ b/src/process/exec.ts @@ -338,8 +338,8 @@ export async function runCommandWithTimeout( clearTimeout(timer); clearNoOutputTimer(); clearCloseFallbackTimer(); - const resolvedCode = childExitState?.code ?? code; - const resolvedSignal = childExitState?.signal ?? signal; + const resolvedCode = childExitState?.code ?? code ?? child.exitCode ?? null; + const resolvedSignal = childExitState?.signal ?? signal ?? child.signalCode ?? null; const termination = noOutputTimedOut ? "no-output-timeout" : timedOut diff --git a/src/process/exec.windows.test.ts b/src/process/exec.windows.test.ts index b2357858565..4f0d4b290af 100644 --- a/src/process/exec.windows.test.ts +++ b/src/process/exec.windows.test.ts @@ -17,6 +17,8 @@ let runCommandWithTimeout: typeof import("./exec.js").runCommandWithTimeout; let runExec: typeof import("./exec.js").runExec; type MockChild = EventEmitter & { + exitCode?: number | null; + signalCode?: NodeJS.Signals | null; stdout: EventEmitter; stderr: EventEmitter; stdin: { write: ReturnType; end: ReturnType }; @@ -25,10 +27,17 @@ type MockChild = EventEmitter & { killed?: boolean; }; -function createMockChild(params?: { code?: number; signal?: NodeJS.Signals | null }): MockChild { +function createMockChild(params?: { + closeCode?: number | null; + closeSignal?: NodeJS.Signals | null; + exitCode?: number | null; + signal?: NodeJS.Signals | null; +}): MockChild { const child = new EventEmitter() as MockChild; child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); + child.exitCode = params?.exitCode ?? params?.closeCode ?? 0; + child.signalCode = params?.signal ?? null; child.stdin = { write: vi.fn(), end: vi.fn(), @@ -37,7 +46,7 @@ function createMockChild(params?: { code?: number; signal?: NodeJS.Signals | nul child.pid = 1234; child.killed = false; queueMicrotask(() => { - child.emit("close", params?.code ?? 0, params?.signal ?? null); + child.emit("close", params?.closeCode ?? 0, params?.closeSignal ?? params?.signal ?? null); }); return child; } @@ -94,6 +103,20 @@ describe("windows command wrapper behavior", () => { } }); + it("keeps child exitCode when close reports null on Windows npm shims", async () => { + const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32"); + const child = createMockChild({ closeCode: null, exitCode: 0 }); + + spawnMock.mockImplementation(() => child); + + try { + const result = await runCommandWithTimeout(["npm", "--version"], { timeoutMs: 1000 }); + expect(result.code).toBe(0); + } finally { + platformSpy.mockRestore(); + } + }); + it("uses cmd.exe wrapper with windowsVerbatimArguments in runExec for .cmd shims", async () => { const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32"); const expectedComSpec = process.env.ComSpec ?? "cmd.exe";