import { EventEmitter } from "node:events"; import { PassThrough } from "node:stream"; import type { ChildProcess } from "node:child_process"; import { describe, expect, it, vi } from "vitest"; import { spawnWithFallback } from "./spawn-utils.js"; function createStubChild() { const child = new EventEmitter() as ChildProcess; child.stdin = new PassThrough() as ChildProcess["stdin"]; child.stdout = new PassThrough() as ChildProcess["stdout"]; child.stderr = new PassThrough() as ChildProcess["stderr"]; child.pid = 1234; child.killed = false; child.kill = vi.fn(() => true) as ChildProcess["kill"]; queueMicrotask(() => { child.emit("spawn"); }); return child; } describe("spawnWithFallback", () => { it("retries on EBADF using fallback options", async () => { const spawnMock = vi .fn() .mockImplementationOnce(() => { const err = new Error("spawn EBADF"); (err as NodeJS.ErrnoException).code = "EBADF"; throw err; }) .mockImplementationOnce(() => createStubChild()); const result = await spawnWithFallback({ argv: ["echo", "ok"], options: { stdio: ["pipe", "pipe", "pipe"] }, fallbacks: [{ label: "safe-stdin", options: { stdio: ["ignore", "pipe", "pipe"] } }], spawnImpl: spawnMock, }); expect(result.usedFallback).toBe(true); expect(result.fallbackLabel).toBe("safe-stdin"); expect(spawnMock).toHaveBeenCalledTimes(2); expect(spawnMock.mock.calls[0]?.[2]?.stdio).toEqual(["pipe", "pipe", "pipe"]); expect(spawnMock.mock.calls[1]?.[2]?.stdio).toEqual(["ignore", "pipe", "pipe"]); }); it("does not retry on non-EBADF errors", async () => { const spawnMock = vi.fn().mockImplementationOnce(() => { const err = new Error("spawn ENOENT"); (err as NodeJS.ErrnoException).code = "ENOENT"; throw err; }); await expect( spawnWithFallback({ argv: ["missing"], options: { stdio: ["pipe", "pipe", "pipe"] }, fallbacks: [{ label: "safe-stdin", options: { stdio: ["ignore", "pipe", "pipe"] } }], spawnImpl: spawnMock, }), ).rejects.toThrow(/ENOENT/); expect(spawnMock).toHaveBeenCalledTimes(1); }); });