fix: serialize claude cli runs

This commit is contained in:
Peter Steinberger
2026-01-09 04:58:21 +00:00
parent aa5e75e853
commit 9114331218
8 changed files with 222 additions and 47 deletions

View File

@@ -4,6 +4,20 @@ import { runClaudeCliAgent } from "./claude-cli-runner.js";
const runCommandWithTimeoutMock = vi.fn();
function createDeferred<T>() {
let resolve: (value: T) => void;
let reject: (error: unknown) => void;
const promise = new Promise<T>((res, rej) => {
resolve = res;
reject = rej;
});
return {
promise,
resolve: resolve as (value: T) => void,
reject: reject as (error: unknown) => void,
};
}
vi.mock("../process/exec.js", () => ({
runCommandWithTimeout: (...args: unknown[]) => runCommandWithTimeoutMock(...args),
}));
@@ -13,7 +27,7 @@ describe("runClaudeCliAgent", () => {
runCommandWithTimeoutMock.mockReset();
});
it("starts a new session without --session-id when no resume id", async () => {
it("starts a new session with --session-id when none is provided", async () => {
runCommandWithTimeoutMock.mockResolvedValueOnce({
stdout: JSON.stringify({ message: "ok", session_id: "sid-1" }),
stderr: "",
@@ -35,11 +49,11 @@ describe("runClaudeCliAgent", () => {
expect(runCommandWithTimeoutMock).toHaveBeenCalledTimes(1);
const argv = runCommandWithTimeoutMock.mock.calls[0]?.[0] as string[];
expect(argv).toContain("claude");
expect(argv).not.toContain("--session-id");
expect(argv).not.toContain("--resume");
expect(argv).toContain("--session-id");
expect(argv).toContain("hi");
});
it("uses --resume when a resume session id is provided", async () => {
it("uses provided --session-id when a claude session id is provided", async () => {
runCommandWithTimeoutMock.mockResolvedValueOnce({
stdout: JSON.stringify({ message: "ok", session_id: "sid-2" }),
stderr: "",
@@ -56,13 +70,78 @@ describe("runClaudeCliAgent", () => {
model: "opus",
timeoutMs: 1_000,
runId: "run-2",
resumeSessionId: "sid-1",
claudeSessionId: "c9d7b831-1c31-4d22-80b9-1e50ca207d4b",
});
expect(runCommandWithTimeoutMock).toHaveBeenCalledTimes(1);
const argv = runCommandWithTimeoutMock.mock.calls[0]?.[0] as string[];
expect(argv).toContain("--resume");
expect(argv).toContain("sid-1");
expect(argv).not.toContain("--session-id");
expect(argv).toContain("--session-id");
expect(argv).toContain("c9d7b831-1c31-4d22-80b9-1e50ca207d4b");
expect(argv).toContain("hi");
});
it("serializes concurrent claude-cli runs", async () => {
const firstDeferred = createDeferred<{
stdout: string;
stderr: string;
code: number | null;
signal: NodeJS.Signals | null;
killed: boolean;
}>();
const secondDeferred = createDeferred<{
stdout: string;
stderr: string;
code: number | null;
signal: NodeJS.Signals | null;
killed: boolean;
}>();
runCommandWithTimeoutMock
.mockImplementationOnce(() => firstDeferred.promise)
.mockImplementationOnce(() => secondDeferred.promise);
const firstRun = runClaudeCliAgent({
sessionId: "s1",
sessionFile: "/tmp/session.jsonl",
workspaceDir: "/tmp",
prompt: "first",
model: "opus",
timeoutMs: 1_000,
runId: "run-1",
});
const secondRun = runClaudeCliAgent({
sessionId: "s2",
sessionFile: "/tmp/session.jsonl",
workspaceDir: "/tmp",
prompt: "second",
model: "opus",
timeoutMs: 1_000,
runId: "run-2",
});
await Promise.resolve();
expect(runCommandWithTimeoutMock).toHaveBeenCalledTimes(1);
firstDeferred.resolve({
stdout: JSON.stringify({ message: "ok", session_id: "sid-1" }),
stderr: "",
code: 0,
signal: null,
killed: false,
});
await Promise.resolve();
expect(runCommandWithTimeoutMock).toHaveBeenCalledTimes(2);
secondDeferred.resolve({
stdout: JSON.stringify({ message: "ok", session_id: "sid-2" }),
stderr: "",
code: 0,
signal: null,
killed: false,
});
await Promise.all([firstRun, secondRun]);
});
});