diff --git a/src/gateway/server-methods/agents-mutate.test.ts b/src/gateway/server-methods/agents-mutate.test.ts index 966e270c2ba..3de068f40b2 100644 --- a/src/gateway/server-methods/agents-mutate.test.ts +++ b/src/gateway/server-methods/agents-mutate.test.ts @@ -38,10 +38,15 @@ const mocks = vi.hoisted(() => ({ writeFileWithinRoot: vi.fn(async () => {}), })); -vi.mock("../../config/config.js", () => ({ - loadConfig: () => mocks.loadConfigReturn, - writeConfigFile: mocks.writeConfigFile, -})); +vi.mock("../../config/config.js", async () => { + const actual = + await vi.importActual("../../config/config.js"); + return { + ...actual, + loadConfig: () => mocks.loadConfigReturn, + writeConfigFile: mocks.writeConfigFile, + }; +}); vi.mock("../../commands/agents.config.js", () => ({ applyAgentConfig: mocks.applyAgentConfig, @@ -71,13 +76,17 @@ vi.mock("../../config/sessions/paths.js", () => ({ resolveSessionTranscriptsDirForAgent: mocks.resolveSessionTranscriptsDirForAgent, })); -vi.mock("../../plugin-sdk/browser-maintenance.js", () => ({ +vi.mock("../../../extensions/browser/runtime-api.js", () => ({ movePathToTrash: mocks.movePathToTrash, })); -vi.mock("../../utils.js", () => ({ - resolveUserPath: (p: string) => `/resolved${p.startsWith("/") ? "" : "/"}${p}`, -})); +vi.mock("../../utils.js", async () => { + const actual = await vi.importActual("../../utils.js"); + return { + ...actual, + resolveUserPath: (p: string) => `/resolved${p.startsWith("/") ? "" : "/"}${p}`, + }; +}); vi.mock("../session-utils.js", () => ({ listAgentsForGateway: mocks.listAgentsForGateway, diff --git a/src/gateway/server-methods/channels.status.test.ts b/src/gateway/server-methods/channels.status.test.ts index a5871380e19..e334052eaec 100644 --- a/src/gateway/server-methods/channels.status.test.ts +++ b/src/gateway/server-methods/channels.status.test.ts @@ -2,7 +2,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import type { GatewayRequestHandlerOptions } from "./types.js"; const mocks = vi.hoisted(() => ({ - loadConfig: vi.fn(), + loadConfig: vi.fn(() => ({})), applyPluginAutoEnable: vi.fn(), listChannelPlugins: vi.fn(), buildChannelUiCatalog: vi.fn(), @@ -10,14 +10,14 @@ const mocks = vi.hoisted(() => ({ getChannelActivity: vi.fn(), })); -vi.mock("../../config/config.js", async () => { - const actual = - await vi.importActual("../../config/config.js"); - return { - ...actual, - loadConfig: mocks.loadConfig, - }; -}); +vi.mock("../../config/config.js", () => ({ + loadConfig: mocks.loadConfig, + readConfigFileSnapshot: vi.fn(async () => ({ + config: {}, + path: "openclaw.config.json", + raw: "{}", + })), +})); vi.mock("../../config/plugin-auto-enable.js", () => ({ applyPluginAutoEnable: mocks.applyPluginAutoEnable, diff --git a/src/gateway/server.chat.gateway-server-chat.test.ts b/src/gateway/server.chat.gateway-server-chat.test.ts index 692c767e523..6bf1a1ce338 100644 --- a/src/gateway/server.chat.gateway-server-chat.test.ts +++ b/src/gateway/server.chat.gateway-server-chat.test.ts @@ -1,13 +1,14 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import { describe, expect, test, vi } from "vitest"; +import { beforeEach, describe, expect, test, vi } from "vitest"; import { WebSocket } from "ws"; import { emitAgentEvent, registerAgentRunContext } from "../infra/agent-events.js"; import { extractFirstTextBlock } from "../shared/chat-message-content.js"; import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js"; import { connectOk, + dispatchInboundMessageMock, installGatewayTestHooks, mockGetReplyFromConfigOnce, onceMessage, @@ -32,6 +33,10 @@ installConnectedControlUiServerSuite((started) => { }); describe("gateway server chat", () => { + beforeEach(() => { + dispatchInboundMessageMock.mockReset(); + }); + const buildNoReplyHistoryFixture = (includeMixedAssistant = false) => [ { role: "user", @@ -562,11 +567,39 @@ describe("gateway server chat", () => { }); test("routes /btw replies through side-result events without transcript injection", async () => { - await withMainSessionStore(async () => { - mockGetReplyFromConfigOnce(async () => ({ - text: "323", - btw: { question: "what is 17 * 19?" }, - })); + await withMainSessionStore(async (dir) => { + await fs.writeFile( + path.join(dir, "sess-main.jsonl"), + `${JSON.stringify({ + message: { + role: "user", + content: [{ type: "text", text: "main thread context" }], + timestamp: Date.now(), + }, + })}\n`, + "utf-8", + ); + dispatchInboundMessageMock.mockImplementationOnce( + async (params: { + dispatcher: { + sendFinalReply: (payload: { text: string; btw: { question: string } }) => boolean; + markComplete: () => void; + waitForIdle: () => Promise; + getQueuedCounts: () => { final: number; block: number; tool: number }; + }; + }) => { + params.dispatcher.sendFinalReply({ + text: "323", + btw: { question: "what is 17 * 19?" }, + }); + params.dispatcher.markComplete(); + await params.dispatcher.waitForIdle(); + return { + queuedFinal: true, + counts: params.dispatcher.getQueuedCounts(), + }; + }, + ); const sideResultPromise = onceMessage( ws, (o) => @@ -593,18 +626,21 @@ describe("gateway server chat", () => { }); expect(res.ok).toBe(true); + await vi.waitFor(() => { + expect(dispatchInboundMessageMock).toHaveBeenCalled(); + }); const sideResult = await sideResultPromise; const finalEvent = await finalPromise; expect(sideResult.payload).toMatchObject({ kind: "btw", runId: "idem-btw-1", - sessionKey: "main", + sessionKey: "agent:main:main", question: "what is 17 * 19?", text: "323", }); expect(finalEvent.payload).toMatchObject({ runId: "idem-btw-1", - sessionKey: "main", + sessionKey: "agent:main:main", state: "final", }); @@ -612,23 +648,49 @@ describe("gateway server chat", () => { sessionKey: "main", }); expect(historyRes.ok).toBe(true); - expect(historyRes.payload?.messages ?? []).toEqual([]); + const historyTexts = collectHistoryTextValues(historyRes.payload?.messages ?? []); + expect(historyTexts).toEqual(["main thread context"]); }); }); test("routes block-streamed /btw replies through side-result events", async () => { - await withMainSessionStore(async () => { - mockGetReplyFromConfigOnce(async (_ctx, opts) => { - await opts?.onBlockReply?.({ - text: "first chunk", - btw: { question: "what changed?" }, - }); - await opts?.onBlockReply?.({ - text: "second chunk", - btw: { question: "what changed?" }, - }); - return undefined; - }); + await withMainSessionStore(async (dir) => { + await fs.writeFile( + path.join(dir, "sess-main.jsonl"), + `${JSON.stringify({ + message: { + role: "assistant", + content: [{ type: "text", text: "existing context" }], + timestamp: Date.now(), + }, + })}\n`, + "utf-8", + ); + dispatchInboundMessageMock.mockImplementationOnce( + async (params: { + dispatcher: { + sendBlockReply: (payload: { text: string; btw: { question: string } }) => boolean; + markComplete: () => void; + waitForIdle: () => Promise; + getQueuedCounts: () => { final: number; block: number; tool: number }; + }; + }) => { + params.dispatcher.sendBlockReply({ + text: "first chunk", + btw: { question: "what changed?" }, + }); + params.dispatcher.sendBlockReply({ + text: "second chunk", + btw: { question: "what changed?" }, + }); + params.dispatcher.markComplete(); + await params.dispatcher.waitForIdle(); + return { + queuedFinal: false, + counts: params.dispatcher.getQueuedCounts(), + }; + }, + ); const sideResultPromise = onceMessage( ws, (o) => @@ -646,6 +708,9 @@ describe("gateway server chat", () => { }); expect(res.ok).toBe(true); + await vi.waitFor(() => { + expect(dispatchInboundMessageMock).toHaveBeenCalled(); + }); const sideResult = await sideResultPromise; expect(sideResult.payload).toMatchObject({ kind: "btw", diff --git a/src/gateway/server.talk-config.test.ts b/src/gateway/server.talk-config.test.ts index 30a761b0ee7..f7ce1ae8a9a 100644 --- a/src/gateway/server.talk-config.test.ts +++ b/src/gateway/server.talk-config.test.ts @@ -196,6 +196,7 @@ describe("gateway talk.config", () => { const res = await fetchTalkConfig(ws); expect(res.ok).toBe(true); expectElevenLabsTalkConfig(res.payload?.config?.talk, { + provider: "elevenlabs", voiceId: "voice-123", apiKey: "__OPENCLAW_REDACTED__", silenceTimeoutMs: 1500, @@ -238,6 +239,7 @@ describe("gateway talk.config", () => { const res = await fetchTalkConfig(ws, { includeSecrets: true }); expect(res.ok).toBe(true); expectElevenLabsTalkConfig(res.payload?.config?.talk, { + provider: "elevenlabs", apiKey: "secret-key-abc", }); }); @@ -263,7 +265,10 @@ describe("gateway talk.config", () => { provider: "default", id: "ELEVENLABS_API_KEY", } satisfies SecretRef; - expectElevenLabsTalkConfig(res.payload?.config?.talk, { apiKey: secretRef }); + expectElevenLabsTalkConfig(res.payload?.config?.talk, { + provider: "elevenlabs", + apiKey: secretRef, + }); }); }); }); diff --git a/src/gateway/test-helpers.mocks.ts b/src/gateway/test-helpers.mocks.ts index 9a54abfb2ed..00ebbd3bed9 100644 --- a/src/gateway/test-helpers.mocks.ts +++ b/src/gateway/test-helpers.mocks.ts @@ -36,6 +36,8 @@ type GetReplyFromConfigFn = ( type CronIsolatedRunFn = (...args: unknown[]) => Promise<{ status: string; summary: string }>; type AgentCommandFn = (...args: unknown[]) => Promise; type SendWhatsAppFn = (...args: unknown[]) => Promise<{ messageId: string; toJid: string }>; +type RunBtwSideQuestionFn = (...args: unknown[]) => Promise; +type DispatchInboundMessageFn = (...args: unknown[]) => Promise; const createStubOutboundAdapter = (channelId: ChannelPlugin["id"]): ChannelOutboundAdapter => ({ deliveryMode: "direct", @@ -344,6 +346,8 @@ const hoisted = vi.hoisted(() => { }; cronIsolatedRun: Mock; agentCommand: Mock; + runBtwSideQuestion: Mock; + dispatchInboundMessage: Mock; testIsNixMode: { value: boolean }; sessionStoreSaveDelayMs: { value: number }; embeddedRunMock: { @@ -392,6 +396,8 @@ const hoisted = vi.hoisted(() => { }, cronIsolatedRun: vi.fn(async () => ({ status: "ok", summary: "ok" })), agentCommand: vi.fn().mockResolvedValue(undefined), + runBtwSideQuestion: vi.fn().mockResolvedValue(undefined), + dispatchInboundMessage: vi.fn(), testIsNixMode: { value: false }, sessionStoreSaveDelayMs: { value: 0 }, embeddedRunMock: { @@ -457,6 +463,9 @@ export const testTailscaleWhois = hoisted.testTailscaleWhois; export const piSdkMock = hoisted.piSdkMock; export const cronIsolatedRun: Mock = hoisted.cronIsolatedRun; export const agentCommand: Mock = hoisted.agentCommand; +export const runBtwSideQuestion: Mock = hoisted.runBtwSideQuestion; +export const dispatchInboundMessageMock: Mock = + hoisted.dispatchInboundMessage; export const getReplyFromConfig: Mock = hoisted.getReplyFromConfig; export const mockGetReplyFromConfigOnce = (impl: GetReplyFromConfigFn) => { getReplyFromConfig.mockImplementationOnce(impl); @@ -938,15 +947,41 @@ vi.mock("../commands/agent.js", () => ({ agentCommand, agentCommandFromIngress: agentCommand, })); +vi.mock("../agents/btw.js", () => ({ + runBtwSideQuestion: (...args: Parameters) => + hoisted.runBtwSideQuestion(...args), +})); +vi.mock("/src/agents/btw.js", () => ({ + runBtwSideQuestion: (...args: Parameters) => + hoisted.runBtwSideQuestion(...args), +})); vi.mock("../auto-reply/dispatch.js", async () => { - return await vi.importActual( + const actual = await vi.importActual( "../auto-reply/dispatch.js", ); + return { + ...actual, + dispatchInboundMessage: (...args: Parameters) => { + const impl = hoisted.dispatchInboundMessage.getMockImplementation(); + return impl + ? hoisted.dispatchInboundMessage(...args) + : actual.dispatchInboundMessage(...args); + }, + }; }); vi.mock("/src/auto-reply/dispatch.js", async () => { - return await vi.importActual( + const actual = await vi.importActual( "../auto-reply/dispatch.js", ); + return { + ...actual, + dispatchInboundMessage: (...args: Parameters) => { + const impl = hoisted.dispatchInboundMessage.getMockImplementation(); + return impl + ? hoisted.dispatchInboundMessage(...args) + : actual.dispatchInboundMessage(...args); + }, + }; }); vi.mock("../auto-reply/reply.js", () => ({ getReplyFromConfig: (...args: Parameters) => diff --git a/src/gateway/tools-invoke-http.cron-regression.test.ts b/src/gateway/tools-invoke-http.cron-regression.test.ts index 789c4437605..f4f603b0c72 100644 --- a/src/gateway/tools-invoke-http.cron-regression.test.ts +++ b/src/gateway/tools-invoke-http.cron-regression.test.ts @@ -39,9 +39,13 @@ vi.mock("../agents/pi-tools.before-tool-call.js", () => ({ runBeforeToolCallHook, })); -vi.mock("../plugins/config-state.js", () => ({ - isTestDefaultMemorySlotDisabled: disableDefaultMemorySlot, -})); +vi.mock("../plugins/config-state.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + isTestDefaultMemorySlotDisabled: disableDefaultMemorySlot, + }; +}); vi.mock("../plugins/tools.js", () => ({ getPluginToolMeta: noPluginToolMeta,