mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-15 20:10:42 +00:00
168 lines
4.6 KiB
TypeScript
168 lines
4.6 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { sleep } from "../utils.js";
|
|
import { runClaudeCliAgent } from "./claude-cli-runner.js";
|
|
|
|
const mocks = vi.hoisted(() => ({
|
|
spawn: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("../process/supervisor/index.js", () => ({
|
|
getProcessSupervisor: () => ({
|
|
spawn: (...args: unknown[]) => mocks.spawn(...args),
|
|
cancel: vi.fn(),
|
|
cancelScope: vi.fn(),
|
|
reconcileOrphans: async () => {},
|
|
getRecord: 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,
|
|
};
|
|
}
|
|
|
|
function createManagedRun(
|
|
exit: Promise<{
|
|
reason: "exit" | "overall-timeout" | "no-output-timeout" | "signal" | "manual-cancel";
|
|
exitCode: number | null;
|
|
exitSignal: NodeJS.Signals | null;
|
|
durationMs: number;
|
|
stdout: string;
|
|
stderr: string;
|
|
timedOut: boolean;
|
|
noOutputTimedOut: boolean;
|
|
}>,
|
|
) {
|
|
return {
|
|
runId: "run-test",
|
|
pid: 12345,
|
|
startedAtMs: Date.now(),
|
|
wait: async () => await exit,
|
|
cancel: vi.fn(),
|
|
};
|
|
}
|
|
|
|
function successExit(payload: { message: string; session_id: string }) {
|
|
return {
|
|
reason: "exit" as const,
|
|
exitCode: 0,
|
|
exitSignal: null,
|
|
durationMs: 1,
|
|
stdout: JSON.stringify(payload),
|
|
stderr: "",
|
|
timedOut: false,
|
|
noOutputTimedOut: false,
|
|
};
|
|
}
|
|
|
|
async function waitForCalls(mockFn: { mock: { calls: unknown[][] } }, count: number) {
|
|
for (let i = 0; i < 50; i += 1) {
|
|
if (mockFn.mock.calls.length >= count) {
|
|
return;
|
|
}
|
|
await sleep(0);
|
|
}
|
|
throw new Error(`Expected ${count} calls, got ${mockFn.mock.calls.length}`);
|
|
}
|
|
|
|
describe("runClaudeCliAgent", () => {
|
|
beforeEach(() => {
|
|
mocks.spawn.mockReset();
|
|
});
|
|
|
|
it("starts a new session with --session-id when none is provided", async () => {
|
|
mocks.spawn.mockResolvedValueOnce(
|
|
createManagedRun(Promise.resolve(successExit({ message: "ok", session_id: "sid-1" }))),
|
|
);
|
|
|
|
await runClaudeCliAgent({
|
|
sessionId: "openclaw-session",
|
|
sessionFile: "/tmp/session.jsonl",
|
|
workspaceDir: "/tmp",
|
|
prompt: "hi",
|
|
model: "opus",
|
|
timeoutMs: 1_000,
|
|
runId: "run-1",
|
|
});
|
|
|
|
expect(mocks.spawn).toHaveBeenCalledTimes(1);
|
|
const spawnInput = mocks.spawn.mock.calls[0]?.[0] as { argv: string[]; mode: string };
|
|
expect(spawnInput.mode).toBe("child");
|
|
expect(spawnInput.argv).toContain("claude");
|
|
expect(spawnInput.argv).toContain("--session-id");
|
|
expect(spawnInput.argv).toContain("hi");
|
|
});
|
|
|
|
it("uses --resume when a claude session id is provided", async () => {
|
|
mocks.spawn.mockResolvedValueOnce(
|
|
createManagedRun(Promise.resolve(successExit({ message: "ok", session_id: "sid-2" }))),
|
|
);
|
|
|
|
await runClaudeCliAgent({
|
|
sessionId: "openclaw-session",
|
|
sessionFile: "/tmp/session.jsonl",
|
|
workspaceDir: "/tmp",
|
|
prompt: "hi",
|
|
model: "opus",
|
|
timeoutMs: 1_000,
|
|
runId: "run-2",
|
|
claudeSessionId: "c9d7b831-1c31-4d22-80b9-1e50ca207d4b",
|
|
});
|
|
|
|
expect(mocks.spawn).toHaveBeenCalledTimes(1);
|
|
const spawnInput = mocks.spawn.mock.calls[0]?.[0] as { argv: string[] };
|
|
expect(spawnInput.argv).toContain("--resume");
|
|
expect(spawnInput.argv).toContain("c9d7b831-1c31-4d22-80b9-1e50ca207d4b");
|
|
expect(spawnInput.argv).not.toContain("--session-id");
|
|
expect(spawnInput.argv).toContain("hi");
|
|
});
|
|
|
|
it("serializes concurrent claude-cli runs", async () => {
|
|
const firstDeferred = createDeferred<ReturnType<typeof successExit>>();
|
|
const secondDeferred = createDeferred<ReturnType<typeof successExit>>();
|
|
|
|
mocks.spawn
|
|
.mockResolvedValueOnce(createManagedRun(firstDeferred.promise))
|
|
.mockResolvedValueOnce(createManagedRun(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 waitForCalls(mocks.spawn, 1);
|
|
|
|
firstDeferred.resolve(successExit({ message: "ok", session_id: "sid-1" }));
|
|
|
|
await waitForCalls(mocks.spawn, 2);
|
|
|
|
secondDeferred.resolve(successExit({ message: "ok", session_id: "sid-2" }));
|
|
|
|
await Promise.all([firstRun, secondRun]);
|
|
});
|
|
});
|