From ae6fe67550ac86ecf816caa6b61d1c8b18f4c600 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 16 Feb 2026 03:41:50 +0000 Subject: [PATCH] test: align e2e coverage with supervisor session flow --- src/agents/claude-cli-runner.e2e.test.ts | 138 ++++++++++++---------- src/agents/tools/message-tool.e2e.test.ts | 2 +- src/agents/tools/sessions.e2e.test.ts | 3 +- 3 files changed, 78 insertions(+), 65 deletions(-) diff --git a/src/agents/claude-cli-runner.e2e.test.ts b/src/agents/claude-cli-runner.e2e.test.ts index 587a13ff2dd..afa353daba3 100644 --- a/src/agents/claude-cli-runner.e2e.test.ts +++ b/src/agents/claude-cli-runner.e2e.test.ts @@ -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() { let resolve: (value: T) => void; @@ -18,6 +30,40 @@ function createDeferred() { }; } +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>(); + const secondDeferred = createDeferred>(); - 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]); }); diff --git a/src/agents/tools/message-tool.e2e.test.ts b/src/agents/tools/message-tool.e2e.test.ts index 5c974e001c7..6a7d2eed24b 100644 --- a/src/agents/tools/message-tool.e2e.test.ts +++ b/src/agents/tools/message-tool.e2e.test.ts @@ -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"); }); }); diff --git a/src/agents/tools/sessions.e2e.test.ts b/src/agents/tools/sessions.e2e.test.ts index f94be78d57f..4e3d6a55652 100644 --- a/src/agents/tools/sessions.e2e.test.ts +++ b/src/agents/tools/sessions.e2e.test.ts @@ -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" }); }); });