test(codex-local): regression for CodexRpcClient spawn ENOENT
Add a Vitest case that mocks `node:child_process.spawn` so the child emits `error` (ENOENT) after the constructor attaches listeners. `getQuotaWindows()` must resolve with `ok: false` instead of leaving an unhandled `error` event on the process. Register `packages/adapters/codex-local` in the root Vitest workspace. Document in DEVELOPING.md that a missing `codex` binary should not take down the API server during quota polling.
This commit is contained in:
parent
01fb97e8da
commit
c98af52590
4 changed files with 74 additions and 1 deletions
|
|
@ -134,6 +134,8 @@ For `codex_local`, Paperclip also manages a per-company Codex home under the ins
|
||||||
|
|
||||||
- `~/.paperclip/instances/default/companies/<company-id>/codex-home`
|
- `~/.paperclip/instances/default/companies/<company-id>/codex-home`
|
||||||
|
|
||||||
|
If the `codex` CLI is not installed or not on `PATH`, `codex_local` agent runs fail at execution time with a clear adapter error. Quota polling uses a short-lived `codex app-server` subprocess: when `codex` cannot be spawned, that provider reports `ok: false` in aggregated quota results and the API server keeps running (it must not exit on a missing binary).
|
||||||
|
|
||||||
## Worktree-local Instances
|
## Worktree-local Instances
|
||||||
|
|
||||||
When developing from multiple git worktrees, do not point two Paperclip servers at the same embedded PostgreSQL data directory.
|
When developing from multiple git worktrees, do not point two Paperclip servers at the same embedded PostgreSQL data directory.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
import { EventEmitter } from "node:events";
|
||||||
|
import type { ChildProcess } from "node:child_process";
|
||||||
|
import { describe, expect, it, vi, beforeEach } 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", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mockSpawn.mockReset();
|
||||||
|
});
|
||||||
|
|
||||||
|
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");
|
||||||
|
});
|
||||||
|
});
|
||||||
7
packages/adapters/codex-local/vitest.config.ts
Normal file
7
packages/adapters/codex-local/vitest.config.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { defineConfig } from "vitest/config";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
environment: "node",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -2,6 +2,13 @@ import { defineConfig } from "vitest/config";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
test: {
|
test: {
|
||||||
projects: ["packages/db", "packages/adapters/opencode-local", "server", "ui", "cli"],
|
projects: [
|
||||||
|
"packages/db",
|
||||||
|
"packages/adapters/codex-local",
|
||||||
|
"packages/adapters/opencode-local",
|
||||||
|
"server",
|
||||||
|
"ui",
|
||||||
|
"cli",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue