Spaces:
Paused
Paused
| 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<typeof import("node:child_process")>(); | |
| return { | |
| ...cp, | |
| spawn: (...args: Parameters<typeof cp.spawn>) => mockSpawn(...args) as ReturnType<typeof cp.spawn>, | |
| }; | |
| }); | |
| 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"); | |
| }); | |
| }); | |