import { EventEmitter } from "node:events"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import type { ChildProcess } from "node:child_process"; import { describe, expect, it, vi, beforeEach, afterEach } from "vitest"; const { mockSpawn } = vi.hoisted(() => ({ mockSpawn: vi.fn(), })); vi.mock("node:child_process", async (importOriginal) => { const cp = await importOriginal(); return { ...cp, spawn: (...args: Parameters) => mockSpawn(...args) as ReturnType, }; }); import { getQuotaWindows } from "./quota.js"; function createChildThatErrorsOnMicrotask(err: Error): ChildProcess { const child = new EventEmitter() as ChildProcess; const stream = Object.assign(new EventEmitter(), { setEncoding: () => {}, }); Object.assign(child, { stdout: stream, stderr: Object.assign(new EventEmitter(), { setEncoding: () => {} }), stdin: { write: vi.fn(), end: vi.fn() }, kill: vi.fn(), }); queueMicrotask(() => { child.emit("error", err); }); return child; } describe("CodexRpcClient spawn failures", () => { let previousCodexHome: string | undefined; let isolatedCodexHome: string | undefined; beforeEach(() => { mockSpawn.mockReset(); // After the RPC path fails, getQuotaWindows() calls readCodexToken() which // reads $CODEX_HOME/auth.json (default ~/.codex). Point CODEX_HOME at an // empty temp directory so we never hit real host auth or the WHAM network. previousCodexHome = process.env.CODEX_HOME; isolatedCodexHome = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-codex-spawn-test-")); process.env.CODEX_HOME = isolatedCodexHome; }); afterEach(() => { if (isolatedCodexHome) { try { fs.rmSync(isolatedCodexHome, { recursive: true, force: true }); } catch { /* ignore */ } isolatedCodexHome = undefined; } if (previousCodexHome === undefined) { delete process.env.CODEX_HOME; } else { process.env.CODEX_HOME = previousCodexHome; } }); it("does not crash the process when codex is missing; getQuotaWindows returns ok: false", async () => { const enoent = Object.assign(new Error("spawn codex ENOENT"), { code: "ENOENT", errno: -2, syscall: "spawn codex", path: "codex", }); mockSpawn.mockImplementation(() => createChildThatErrorsOnMicrotask(enoent)); const result = await getQuotaWindows(); expect(result.ok).toBe(false); expect(result.windows).toEqual([]); expect(result.error).toContain("Codex app-server"); expect(result.error).toContain("spawn codex ENOENT"); }); });