test: stabilize runner and acp mocks

- reuse the shared cli-runner harness in claude runner tests
- make ACP session metadata and startup tests use stable static mocks
This commit is contained in:
Peter Steinberger
2026-03-30 00:27:52 +01:00
parent bf63264c62
commit e4466c72a2
4 changed files with 54 additions and 106 deletions

View File

@@ -10,25 +10,20 @@ const hoisted = vi.hoisted(() => {
};
});
vi.mock("../../config/sessions.js", async () => {
const actual = await vi.importActual<typeof import("../../config/sessions.js")>(
"../../config/sessions.js",
);
return {
...actual,
resolveAllAgentSessionStoreTargets: (cfg: OpenClawConfig, opts: unknown) =>
hoisted.resolveAllAgentSessionStoreTargetsMock(cfg, opts),
loadSessionStore: (storePath: string) => hoisted.loadSessionStoreMock(storePath),
};
});
vi.mock("../../config/sessions.js", () => ({
loadSessionStore: (storePath: string) => hoisted.loadSessionStoreMock(storePath),
resolveAllAgentSessionStoreTargets: (cfg: OpenClawConfig, opts: unknown) =>
hoisted.resolveAllAgentSessionStoreTargetsMock(cfg, opts),
resolveStorePath: vi.fn(() => "/tmp/sessions.json"),
updateSessionStore: vi.fn(),
}));
let listAcpSessionEntries: typeof import("./session-meta.js").listAcpSessionEntries;
describe("listAcpSessionEntries", () => {
beforeEach(async () => {
vi.resetModules();
({ listAcpSessionEntries } = await import("./session-meta.js"));
vi.clearAllMocks();
({ listAcpSessionEntries } = await import("./session-meta.js"));
});
it("reads ACP sessions from resolved configured store targets", async () => {

View File

@@ -12,7 +12,7 @@ type GatewayClientAuth = {
};
type ResolveGatewayConnectionAuth = (params: unknown) => Promise<GatewayClientAuth>;
const mockState = {
const mockState = vi.hoisted(() => ({
gateways: [] as MockGatewayClient[],
gatewayAuth: [] as GatewayClientAuth[],
agentSideConnectionCtor: vi.fn(),
@@ -21,7 +21,7 @@ const mockState = {
token: undefined,
password: undefined,
})),
};
}));
class MockGatewayClient {
private callbacks: GatewayClientCallbacks;
@@ -63,13 +63,11 @@ vi.mock("../config/config.js", () => ({
mode: "local",
},
}),
}));
vi.mock("../gateway/auth.js", () => ({
resolveGatewayAuth: () => ({}),
resolveGatewayPort: vi.fn(() => 18_789),
}));
vi.mock("../gateway/call.js", () => ({
callGateway: vi.fn(),
buildGatewayConnectionDetails: ({ url }: { url?: string }) => {
if (typeof url === "string" && url.trim().length > 0) {
return {
@@ -92,6 +90,10 @@ vi.mock("../gateway/client.js", () => ({
GatewayClient: MockGatewayClient,
}));
vi.mock("../infra/is-main.js", () => ({
isMainModule: () => false,
}));
vi.mock("./translator.js", () => ({
AcpGatewayAgent: class {
start(): void {
@@ -149,7 +151,7 @@ describe("serveAcpGateway startup", () => {
({ serveAcpGateway } = await import("./server.js"));
});
beforeEach(() => {
beforeEach(async () => {
mockState.gateways.length = 0;
mockState.gatewayAuth.length = 0;
mockState.agentSideConnectionCtor.mockReset();

View File

@@ -1,18 +1,8 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
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(),
}),
}));
import {
setupClaudeCliRunnerTestModule,
supervisorSpawnMock,
} from "./cli-runner.test-support.js";
function createDeferred<T>() {
let resolve: (value: T) => void = () => {};
@@ -52,8 +42,7 @@ function createManagedRun(
let runClaudeCliAgent: typeof import("./claude-cli-runner.js").runClaudeCliAgent;
async function loadFreshClaudeCliRunnerModuleForTest() {
vi.resetModules();
({ runClaudeCliAgent } = await import("./claude-cli-runner.js"));
runClaudeCliAgent = await setupClaudeCliRunnerTestModule();
}
function successExit(payload: { message: string; session_id: string }) {
@@ -81,11 +70,11 @@ async function waitForCalls(mockFn: { mock: { calls: unknown[][] } }, count: num
describe("runClaudeCliAgent", () => {
beforeEach(async () => {
await loadFreshClaudeCliRunnerModuleForTest();
mocks.spawn.mockClear();
supervisorSpawnMock.mockClear();
});
it("starts a new session with --session-id when none is provided", async () => {
mocks.spawn.mockResolvedValueOnce(
supervisorSpawnMock.mockResolvedValueOnce(
createManagedRun(Promise.resolve(successExit({ message: "ok", session_id: "sid-1" }))),
);
@@ -99,16 +88,16 @@ describe("runClaudeCliAgent", () => {
runId: "run-1",
});
expect(mocks.spawn).toHaveBeenCalledTimes(1);
const spawnInput = mocks.spawn.mock.calls[0]?.[0] as { argv: string[]; mode: string };
expect(supervisorSpawnMock).toHaveBeenCalledTimes(1);
const spawnInput = supervisorSpawnMock.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(
it("starts fresh when only a legacy claude session id is provided", async () => {
supervisorSpawnMock.mockResolvedValueOnce(
createManagedRun(Promise.resolve(successExit({ message: "ok", session_id: "sid-2" }))),
);
@@ -123,11 +112,11 @@ describe("runClaudeCliAgent", () => {
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(supervisorSpawnMock).toHaveBeenCalledTimes(1);
const spawnInput = supervisorSpawnMock.mock.calls[0]?.[0] as { argv: string[] };
expect(spawnInput.argv).not.toContain("--resume");
expect(spawnInput.argv).not.toContain("c9d7b831-1c31-4d22-80b9-1e50ca207d4b");
expect(spawnInput.argv).toContain("--session-id");
expect(spawnInput.argv).toContain("hi");
});
@@ -135,7 +124,7 @@ describe("runClaudeCliAgent", () => {
const firstDeferred = createDeferred<ReturnType<typeof successExit>>();
const secondDeferred = createDeferred<ReturnType<typeof successExit>>();
mocks.spawn
supervisorSpawnMock
.mockResolvedValueOnce(createManagedRun(firstDeferred.promise))
.mockResolvedValueOnce(createManagedRun(secondDeferred.promise));
@@ -159,11 +148,11 @@ describe("runClaudeCliAgent", () => {
runId: "run-2",
});
await waitForCalls(mocks.spawn, 1);
await waitForCalls(supervisorSpawnMock, 1);
firstDeferred.resolve(successExit({ message: "ok", session_id: "sid-1" }));
await waitForCalls(mocks.spawn, 2);
await waitForCalls(supervisorSpawnMock, 2);
secondDeferred.resolve(successExit({ message: "ok", session_id: "sid-2" }));

View File

@@ -1,24 +1,16 @@
import fs from "node:fs/promises";
import { beforeEach, vi } from "vitest";
import { buildAnthropicCliBackend } from "../../extensions/anthropic/test-api.js";
import { buildGoogleGeminiCliBackend } from "../../extensions/google/test-api.js";
import { buildOpenAICodexCliBackend } from "../../extensions/openai/test-api.js";
import type { OpenClawConfig } from "../config/config.js";
import { createEmptyPluginRegistry } from "../plugins/registry.js";
import { setActivePluginRegistry } from "../plugins/runtime.js";
import type { CliBackendPlugin } from "../plugins/types.js";
import { loadBundledPluginTestApiSync } from "../test-utils/bundled-plugin-public-surface.js";
import { mergeMockedModule } from "../test-utils/vitest-module-mocks.js";
import { setCliRunnerExecuteTestDeps } from "./cli-runner/execute.js";
import { setCliRunnerPrepareTestDeps } from "./cli-runner/prepare.js";
import type { EmbeddedContextFile } from "./pi-embedded-helpers.js";
import type { WorkspaceBootstrapFile } from "./workspace.js";
const { buildAnthropicCliBackend } = loadBundledPluginTestApiSync<{
buildAnthropicCliBackend: () => CliBackendPlugin;
}>("anthropic");
const { buildGoogleGeminiCliBackend } = loadBundledPluginTestApiSync<{
buildGoogleGeminiCliBackend: () => CliBackendPlugin;
}>("google");
const { buildOpenAICodexCliBackend } = loadBundledPluginTestApiSync<{
buildOpenAICodexCliBackend: () => CliBackendPlugin;
}>("openai");
export const supervisorSpawnMock = vi.fn();
export const enqueueSystemEventMock = vi.fn();
export const requestHeartbeatNowMock = vi.fn();
@@ -39,7 +31,7 @@ const hoisted = vi.hoisted(() => {
};
});
vi.mock("../process/supervisor/index.js", () => ({
setCliRunnerExecuteTestDeps({
getProcessSupervisor: () => ({
spawn: (...args: unknown[]) => supervisorSpawnMock(...args),
cancel: vi.fn(),
@@ -47,25 +39,14 @@ vi.mock("../process/supervisor/index.js", () => ({
reconcileOrphans: vi.fn(),
getRecord: vi.fn(),
}),
}));
vi.mock("../infra/system-events.js", () => ({
enqueueSystemEvent: (...args: unknown[]) => enqueueSystemEventMock(...args),
}));
vi.mock("../infra/heartbeat-wake.js", async (importOriginal) => {
return await mergeMockedModule(
await importOriginal<typeof import("../infra/heartbeat-wake.js")>(),
() => ({
requestHeartbeatNow: (...args: unknown[]) => requestHeartbeatNowMock(...args),
}),
);
requestHeartbeatNow: (...args: unknown[]) => requestHeartbeatNowMock(...args),
});
vi.mock("./bootstrap-files.js", () => ({
setCliRunnerPrepareTestDeps({
makeBootstrapWarn: () => () => {},
resolveBootstrapContextForRun: hoisted.resolveBootstrapContextForRunMock,
}));
});
type MockRunExit = {
reason:
@@ -160,37 +141,18 @@ export async function setupCliRunnerTestModule() {
bootstrapFiles: [],
contextFiles: [],
});
vi.resetModules();
vi.doMock("../process/supervisor/index.js", () => ({
getProcessSupervisor: () => ({
spawn: (...args: unknown[]) => supervisorSpawnMock(...args),
cancel: vi.fn(),
cancelScope: vi.fn(),
reconcileOrphans: vi.fn(),
getRecord: vi.fn(),
}),
}));
vi.doMock("../infra/system-events.js", () => ({
enqueueSystemEvent: (...args: unknown[]) => enqueueSystemEventMock(...args),
}));
vi.doMock("../infra/heartbeat-wake.js", async () => {
return await mergeMockedModule(
await vi.importActual<typeof import("../infra/heartbeat-wake.js")>(
"../infra/heartbeat-wake.js",
),
() => ({
requestHeartbeatNow: (...args: unknown[]) => requestHeartbeatNowMock(...args),
}),
);
});
vi.doMock("./bootstrap-files.js", () => ({
makeBootstrapWarn: () => () => {},
resolveBootstrapContextForRun: hoisted.resolveBootstrapContextForRunMock,
}));
return (await import("./cli-runner.js")).runCliAgent;
}
export async function setupClaudeCliRunnerTestModule() {
const runCliAgent = await setupCliRunnerTestModule();
return (params: Parameters<typeof import("./claude-cli-runner.js").runClaudeCliAgent>[0]) =>
runCliAgent({
...params,
provider: params.provider ?? "claude-cli",
});
}
export function stubBootstrapContext(params: {
bootstrapFiles: WorkspaceBootstrapFile[];
contextFiles: EmbeddedContextFile[];