// Qa Lab tests cover manual lane plugin behavior. import { MAX_TIMER_TIMEOUT_MS } from "openclaw/plugin-sdk/number-runtime"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; const { startQaLabServer, startQaGatewayChild, startQaProviderServer } = vi.hoisted(() => ({ startQaLabServer: vi.fn(), startQaGatewayChild: vi.fn(), startQaProviderServer: vi.fn(), })); vi.mock("./lab-server.js", () => ({ startQaLabServer, })); vi.mock("./gateway-child.js", () => ({ startQaGatewayChild, })); vi.mock("./providers/server-runtime.js", () => ({ startQaProviderServer, })); import { runQaManualLane } from "./manual-lane.runtime.js"; describe("runQaManualLane", () => { const gatewayCall = vi.fn(); const gatewayStop = vi.fn(); const mockStop = vi.fn(); const labStop = vi.fn(); beforeEach(() => { gatewayCall.mockReset(); gatewayStop.mockReset(); mockStop.mockReset(); labStop.mockReset(); startQaLabServer.mockReset(); startQaGatewayChild.mockReset(); startQaProviderServer.mockReset(); startQaLabServer.mockResolvedValue({ listenUrl: "http://127.0.0.1:43124", baseUrl: "http://127.0.0.1:58000", state: { reset: vi.fn(), addInboundMessage: vi.fn(), addOutboundMessage: vi.fn(), readMessage: vi.fn(), searchMessages: vi.fn(() => []), waitFor: vi.fn(), getSnapshot: () => ({ messages: [ { direction: "outbound", conversation: { id: "qa-operator" }, text: "Protocol note: mock reply.", }, ], }), }, stop: labStop, }); startQaGatewayChild.mockResolvedValue({ call: gatewayCall.mockResolvedValueOnce({ runId: "run-1" }).mockResolvedValueOnce({ status: "ok", }), stop: gatewayStop, }); startQaProviderServer.mockImplementation(async (providerMode: string) => providerMode === "mock-openai" ? { baseUrl: "http://127.0.0.1:44080", stop: mockStop, } : null, ); }); afterEach(() => { vi.clearAllMocks(); }); it("starts the mock provider and threads its base url into the gateway child", async () => { const result = await runQaManualLane({ repoRoot: "/tmp/openclaw-repo", providerMode: "mock-openai", primaryModel: "mock-openai/gpt-5.5", alternateModel: "mock-openai/gpt-5.5-alt", message: "check the kickoff file", timeoutMs: 5_000, replySettleMs: 0, }); expect(startQaProviderServer).toHaveBeenCalledWith("mock-openai"); const [gatewayOptions] = startQaGatewayChild.mock.calls[0] ?? []; expect(gatewayOptions?.repoRoot).toBe("/tmp/openclaw-repo"); expect(gatewayOptions?.providerMode).toBe("mock-openai"); expect(gatewayOptions?.providerBaseUrl).toBe("http://127.0.0.1:44080/v1"); expect(startQaLabServer).toHaveBeenCalledWith({ repoRoot: "/tmp/openclaw-repo", embeddedGateway: "disabled", }); expect(result.reply).toBe("Protocol note: mock reply."); expect(gatewayStop).toHaveBeenCalledTimes(1); expect(mockStop).toHaveBeenCalledTimes(1); expect(labStop).toHaveBeenCalledTimes(1); }); it("skips the mock provider bootstrap for live frontier runs", async () => { const result = await runQaManualLane({ repoRoot: "/tmp/openclaw-repo", providerMode: "live-frontier", primaryModel: "openai/gpt-5.5", alternateModel: "openai/gpt-5.5", message: "check the kickoff file", timeoutMs: 5_000, replySettleMs: 0, }); expect(startQaProviderServer).toHaveBeenCalledWith("live-frontier"); expect(startQaLabServer).toHaveBeenCalledWith({ repoRoot: "/tmp/openclaw-repo", embeddedGateway: "disabled", }); const [gatewayOptions] = startQaGatewayChild.mock.calls[0] ?? []; expect(gatewayOptions?.providerMode).toBe("live-frontier"); expect(gatewayOptions?.providerBaseUrl).toBeUndefined(); expect(result.reply).toBe("Protocol note: mock reply."); }); it("cleans up lab and mock provider when gateway startup fails", async () => { startQaGatewayChild.mockRejectedValueOnce(new Error("gateway startup failed")); await expect( runQaManualLane({ repoRoot: "/tmp/openclaw-repo", providerMode: "mock-openai", primaryModel: "mock-openai/gpt-5.5", alternateModel: "mock-openai/gpt-5.5-alt", message: "check the kickoff file", timeoutMs: 5_000, replySettleMs: 0, }), ).rejects.toThrow("gateway startup failed"); expect(gatewayStop).not.toHaveBeenCalled(); expect(mockStop).toHaveBeenCalledTimes(1); expect(labStop).toHaveBeenCalledTimes(1); }); it("continues provider and lab teardown when gateway stop fails", async () => { gatewayStop.mockRejectedValueOnce(new Error("gateway stop failed")); await expect( runQaManualLane({ repoRoot: "/tmp/openclaw-repo", providerMode: "mock-openai", primaryModel: "mock-openai/gpt-5.5", alternateModel: "mock-openai/gpt-5.5-alt", message: "check the kickoff file", timeoutMs: 5_000, replySettleMs: 0, }), ).rejects.toThrow("gateway stop failed"); expect(gatewayStop).toHaveBeenCalledTimes(1); expect(mockStop).toHaveBeenCalledTimes(1); expect(labStop).toHaveBeenCalledTimes(1); }); it("caps the gateway client timeout for oversized manual waits", async () => { const result = await runQaManualLane({ repoRoot: "/tmp/openclaw-repo", providerMode: "mock-openai", primaryModel: "mock-openai/gpt-5.5", alternateModel: "mock-openai/gpt-5.5-alt", message: "check the kickoff file", timeoutMs: 9e15, replySettleMs: 0, }); expect(result.waited).toEqual({ status: "ok" }); expect(gatewayCall).toHaveBeenLastCalledWith( "agent.wait", { runId: "run-1", timeoutMs: 9e15, }, { timeoutMs: MAX_TIMER_TIMEOUT_MS }, ); }); });