After the mocked RPC spawn fails, getQuotaWindows() still calls readCodexToken(). Use an empty mkdtemp directory for CODEX_HOME for the duration of the test so we never read ~/.codex/auth.json or call WHAM.
85 lines
2.7 KiB
TypeScript
85 lines
2.7 KiB
TypeScript
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");
|
|
});
|
|
});
|