mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-18 08:57:26 +00:00
test: align e2e coverage with supervisor session flow
This commit is contained in:
@@ -2,7 +2,19 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { sleep } from "../utils.js";
|
||||
import { runClaudeCliAgent } from "./claude-cli-runner.js";
|
||||
|
||||
const runCommandWithTimeoutMock = vi.fn();
|
||||
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;
|
||||
@@ -18,6 +30,40 @@ function createDeferred<T>() {
|
||||
};
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -28,23 +74,15 @@ async function waitForCalls(mockFn: { mock: { calls: unknown[][] } }, count: num
|
||||
throw new Error(`Expected ${count} calls, got ${mockFn.mock.calls.length}`);
|
||||
}
|
||||
|
||||
vi.mock("../process/exec.js", () => ({
|
||||
runCommandWithTimeout: (...args: unknown[]) => runCommandWithTimeoutMock(...args),
|
||||
}));
|
||||
|
||||
describe("runClaudeCliAgent", () => {
|
||||
beforeEach(() => {
|
||||
runCommandWithTimeoutMock.mockReset();
|
||||
mocks.spawn.mockReset();
|
||||
});
|
||||
|
||||
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: "",
|
||||
code: 0,
|
||||
signal: null,
|
||||
killed: false,
|
||||
});
|
||||
mocks.spawn.mockResolvedValueOnce(
|
||||
createManagedRun(Promise.resolve(successExit({ message: "ok", session_id: "sid-1" }))),
|
||||
);
|
||||
|
||||
await runClaudeCliAgent({
|
||||
sessionId: "openclaw-session",
|
||||
@@ -56,21 +94,18 @@ describe("runClaudeCliAgent", () => {
|
||||
runId: "run-1",
|
||||
});
|
||||
|
||||
expect(runCommandWithTimeoutMock).toHaveBeenCalledTimes(1);
|
||||
const argv = runCommandWithTimeoutMock.mock.calls[0]?.[0] as string[];
|
||||
expect(argv).toContain("claude");
|
||||
expect(argv).toContain("--session-id");
|
||||
expect(argv).toContain("hi");
|
||||
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 () => {
|
||||
runCommandWithTimeoutMock.mockResolvedValueOnce({
|
||||
stdout: JSON.stringify({ message: "ok", session_id: "sid-2" }),
|
||||
stderr: "",
|
||||
code: 0,
|
||||
signal: null,
|
||||
killed: false,
|
||||
});
|
||||
mocks.spawn.mockResolvedValueOnce(
|
||||
createManagedRun(Promise.resolve(successExit({ message: "ok", session_id: "sid-2" }))),
|
||||
);
|
||||
|
||||
await runClaudeCliAgent({
|
||||
sessionId: "openclaw-session",
|
||||
@@ -83,32 +118,21 @@ describe("runClaudeCliAgent", () => {
|
||||
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("c9d7b831-1c31-4d22-80b9-1e50ca207d4b");
|
||||
expect(argv).toContain("hi");
|
||||
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<{
|
||||
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;
|
||||
}>();
|
||||
const firstDeferred = createDeferred<ReturnType<typeof successExit>>();
|
||||
const secondDeferred = createDeferred<ReturnType<typeof successExit>>();
|
||||
|
||||
runCommandWithTimeoutMock
|
||||
.mockImplementationOnce(() => firstDeferred.promise)
|
||||
.mockImplementationOnce(() => secondDeferred.promise);
|
||||
mocks.spawn
|
||||
.mockResolvedValueOnce(createManagedRun(firstDeferred.promise))
|
||||
.mockResolvedValueOnce(createManagedRun(secondDeferred.promise));
|
||||
|
||||
const firstRun = runClaudeCliAgent({
|
||||
sessionId: "s1",
|
||||
@@ -130,25 +154,13 @@ describe("runClaudeCliAgent", () => {
|
||||
runId: "run-2",
|
||||
});
|
||||
|
||||
await waitForCalls(runCommandWithTimeoutMock, 1);
|
||||
await waitForCalls(mocks.spawn, 1);
|
||||
|
||||
firstDeferred.resolve({
|
||||
stdout: JSON.stringify({ message: "ok", session_id: "sid-1" }),
|
||||
stderr: "",
|
||||
code: 0,
|
||||
signal: null,
|
||||
killed: false,
|
||||
});
|
||||
firstDeferred.resolve(successExit({ message: "ok", session_id: "sid-1" }));
|
||||
|
||||
await waitForCalls(runCommandWithTimeoutMock, 2);
|
||||
await waitForCalls(mocks.spawn, 2);
|
||||
|
||||
secondDeferred.resolve({
|
||||
stdout: JSON.stringify({ message: "ok", session_id: "sid-2" }),
|
||||
stderr: "",
|
||||
code: 0,
|
||||
signal: null,
|
||||
killed: false,
|
||||
});
|
||||
secondDeferred.resolve(successExit({ message: "ok", session_id: "sid-2" }));
|
||||
|
||||
await Promise.all([firstRun, secondRun]);
|
||||
});
|
||||
|
||||
@@ -44,7 +44,7 @@ describe("message tool agent routing", () => {
|
||||
|
||||
const call = mocks.runMessageAction.mock.calls[0]?.[0];
|
||||
expect(call?.agentId).toBe("alpha");
|
||||
expect(call?.sessionKey).toBeUndefined();
|
||||
expect(call?.sessionKey).toBe("agent:alpha:main");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -213,7 +213,8 @@ describe("sessions_send gating", () => {
|
||||
timeoutSeconds: 0,
|
||||
});
|
||||
|
||||
expect(callGatewayMock).not.toHaveBeenCalled();
|
||||
expect(callGatewayMock).toHaveBeenCalledTimes(1);
|
||||
expect(callGatewayMock.mock.calls[0]?.[0]).toMatchObject({ method: "sessions.list" });
|
||||
expect(result.details).toMatchObject({ status: "forbidden" });
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user