diff --git a/extensions/feishu/src/monitor.account.ts b/extensions/feishu/src/monitor.account.ts index ff3a0ba9dc9..5164dbf695f 100644 --- a/extensions/feishu/src/monitor.account.ts +++ b/extensions/feishu/src/monitor.account.ts @@ -629,12 +629,14 @@ export type MonitorSingleAccountParams = { runtime?: RuntimeEnv; abortSignal?: AbortSignal; botOpenIdSource?: BotOpenIdSource; + fireAndForget?: boolean; }; export async function monitorSingleAccount(params: MonitorSingleAccountParams): Promise { const { cfg, account, runtime, abortSignal } = params; const { accountId } = account; const log = runtime?.log ?? console.log; + const fireAndForget = params.fireAndForget ?? true; const botOpenIdSource = params.botOpenIdSource ?? { kind: "fetch" }; const botIdentity = @@ -675,7 +677,7 @@ export async function monitorSingleAccount(params: MonitorSingleAccountParams): accountId, runtime, chatHistories, - fireAndForget: true, + fireAndForget, }); if (connectionMode === "webhook") { diff --git a/extensions/feishu/src/monitor.acp-init-failure.lifecycle.test.ts b/extensions/feishu/src/monitor.acp-init-failure.lifecycle.test.ts index b7b9a63dc70..6f1cf55aef7 100644 --- a/extensions/feishu/src/monitor.acp-init-failure.lifecycle.test.ts +++ b/extensions/feishu/src/monitor.acp-init-failure.lifecycle.test.ts @@ -1,10 +1,11 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { createPluginRuntimeMock } from "../../../test/helpers/extensions/plugin-runtime-mock.js"; import type { ClawdbotConfig, PluginRuntime, RuntimeEnv } from "../runtime-api.js"; -import { monitorSingleAccount } from "./monitor.account.js"; -import { setFeishuRuntime } from "./runtime.js"; import type { ResolvedFeishuAccount } from "./types.js"; +type MonitorSingleAccount = typeof import("./monitor.account.js").monitorSingleAccount; +type SetFeishuRuntime = typeof import("./runtime.js").setFeishuRuntime; + const createEventDispatcherMock = vi.hoisted(() => vi.fn()); const monitorWebSocketMock = vi.hoisted(() => vi.fn(async () => {})); const monitorWebhookMock = vi.hoisted(() => vi.fn(async () => {})); @@ -25,6 +26,8 @@ const listFeishuThreadMessagesMock = vi.hoisted(() => vi.fn(async () => [])); let handlers: Record Promise> = {}; let lastRuntime: RuntimeEnv | null = null; +let monitorSingleAccount: MonitorSingleAccount; +let setFeishuRuntime: SetFeishuRuntime; const originalStateDir = process.env.OPENCLAW_STATE_DIR; vi.mock("./client.js", async () => { @@ -185,6 +188,7 @@ async function setupLifecycleMonitor() { cfg: createLifecycleConfig(), account: createLifecycleAccount(), runtime: lastRuntime, + fireAndForget: false, botOpenIdSource: { kind: "prefetched", botOpenId: "ou_bot_1", @@ -200,7 +204,15 @@ async function setupLifecycleMonitor() { } describe("Feishu ACP-init failure lifecycle", () => { - beforeEach(() => { + beforeEach(async () => { + vi.resetModules(); + vi.doUnmock("./bot.js"); + vi.doUnmock("./card-action.js"); + vi.doUnmock("./monitor.account.js"); + vi.doUnmock("./runtime.js"); + ({ monitorSingleAccount } = await import("./monitor.account.js")); + ({ setFeishuRuntime } = await import("./runtime.js")); + vi.clearAllMocks(); handlers = {}; lastRuntime = null; @@ -334,6 +346,11 @@ describe("Feishu ACP-init failure lifecycle", () => { }); afterEach(() => { + vi.resetModules(); + vi.doUnmock("./bot.js"); + vi.doUnmock("./card-action.js"); + vi.doUnmock("./monitor.account.js"); + vi.doUnmock("./runtime.js"); if (originalStateDir === undefined) { delete process.env.OPENCLAW_STATE_DIR; return; diff --git a/extensions/feishu/src/monitor.bot-menu.lifecycle.test.ts b/extensions/feishu/src/monitor.bot-menu.lifecycle.test.ts index 50c3b3d6f32..9a01e4708af 100644 --- a/extensions/feishu/src/monitor.bot-menu.lifecycle.test.ts +++ b/extensions/feishu/src/monitor.bot-menu.lifecycle.test.ts @@ -1,10 +1,11 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { createPluginRuntimeMock } from "../../../test/helpers/extensions/plugin-runtime-mock.js"; import type { ClawdbotConfig, PluginRuntime, RuntimeEnv } from "../runtime-api.js"; -import { monitorSingleAccount } from "./monitor.account.js"; -import { setFeishuRuntime } from "./runtime.js"; import type { ResolvedFeishuAccount } from "./types.js"; +type MonitorSingleAccount = typeof import("./monitor.account.js").monitorSingleAccount; +type SetFeishuRuntime = typeof import("./runtime.js").setFeishuRuntime; + type BoundConversation = { bindingId: string; targetSessionKey: string; @@ -34,6 +35,8 @@ const sendMessageFeishuMock = vi.hoisted(() => let handlers: Record Promise> = {}; let lastRuntime: RuntimeEnv | null = null; +let monitorSingleAccount: MonitorSingleAccount; +let setFeishuRuntime: SetFeishuRuntime; const originalStateDir = process.env.OPENCLAW_STATE_DIR; vi.mock("./client.js", async () => { @@ -174,6 +177,7 @@ async function setupLifecycleMonitor() { cfg: createLifecycleConfig(), account: createLifecycleAccount(), runtime: lastRuntime, + fireAndForget: false, botOpenIdSource: { kind: "prefetched", botOpenId: "ou_bot_1", @@ -189,7 +193,15 @@ async function setupLifecycleMonitor() { } describe("Feishu bot-menu lifecycle", () => { - beforeEach(() => { + beforeEach(async () => { + vi.resetModules(); + vi.doUnmock("./bot.js"); + vi.doUnmock("./card-action.js"); + vi.doUnmock("./monitor.account.js"); + vi.doUnmock("./runtime.js"); + ({ monitorSingleAccount } = await import("./monitor.account.js")); + ({ setFeishuRuntime } = await import("./runtime.js")); + vi.clearAllMocks(); handlers = {}; lastRuntime = null; @@ -292,6 +304,11 @@ describe("Feishu bot-menu lifecycle", () => { }); afterEach(() => { + vi.resetModules(); + vi.doUnmock("./bot.js"); + vi.doUnmock("./card-action.js"); + vi.doUnmock("./monitor.account.js"); + vi.doUnmock("./runtime.js"); if (originalStateDir === undefined) { delete process.env.OPENCLAW_STATE_DIR; return; diff --git a/extensions/feishu/src/monitor.broadcast.reply-once.lifecycle.test.ts b/extensions/feishu/src/monitor.broadcast.reply-once.lifecycle.test.ts index 3c1a51a084a..f9d63c80398 100644 --- a/extensions/feishu/src/monitor.broadcast.reply-once.lifecycle.test.ts +++ b/extensions/feishu/src/monitor.broadcast.reply-once.lifecycle.test.ts @@ -1,10 +1,11 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { createPluginRuntimeMock } from "../../../test/helpers/extensions/plugin-runtime-mock.js"; import type { ClawdbotConfig, PluginRuntime, RuntimeEnv } from "../runtime-api.js"; -import { monitorSingleAccount } from "./monitor.account.js"; -import { setFeishuRuntime } from "./runtime.js"; import type { ResolvedFeishuAccount } from "./types.js"; +type MonitorSingleAccount = typeof import("./monitor.account.js").monitorSingleAccount; +type SetFeishuRuntime = typeof import("./runtime.js").setFeishuRuntime; + const createEventDispatcherMock = vi.hoisted(() => vi.fn()); const monitorWebSocketMock = vi.hoisted(() => vi.fn(async () => {})); const monitorWebhookMock = vi.hoisted(() => vi.fn(async () => {})); @@ -31,6 +32,8 @@ const sendMessageFeishuMock = vi.hoisted(() => let handlersByAccount = new Map Promise>>(); let runtimesByAccount = new Map(); +let monitorSingleAccount: MonitorSingleAccount; +let setFeishuRuntime: SetFeishuRuntime; const originalStateDir = process.env.OPENCLAW_STATE_DIR; vi.mock("./client.js", async () => { @@ -204,6 +207,7 @@ async function setupLifecycleMonitor(accountId: "account-A" | "account-B") { cfg: createLifecycleConfig(), account: createLifecycleAccount(accountId), runtime, + fireAndForget: false, botOpenIdSource: { kind: "prefetched", botOpenId: "ou_bot_1", @@ -219,7 +223,15 @@ async function setupLifecycleMonitor(accountId: "account-A" | "account-B") { } describe("Feishu broadcast reply-once lifecycle", () => { - beforeEach(() => { + beforeEach(async () => { + vi.resetModules(); + vi.doUnmock("./bot.js"); + vi.doUnmock("./card-action.js"); + vi.doUnmock("./monitor.account.js"); + vi.doUnmock("./runtime.js"); + ({ monitorSingleAccount } = await import("./monitor.account.js")); + ({ setFeishuRuntime } = await import("./runtime.js")); + vi.clearAllMocks(); handlersByAccount = new Map(); runtimesByAccount = new Map(); @@ -327,6 +339,11 @@ describe("Feishu broadcast reply-once lifecycle", () => { }); afterEach(() => { + vi.resetModules(); + vi.doUnmock("./bot.js"); + vi.doUnmock("./card-action.js"); + vi.doUnmock("./monitor.account.js"); + vi.doUnmock("./runtime.js"); if (originalStateDir === undefined) { delete process.env.OPENCLAW_STATE_DIR; return; diff --git a/extensions/feishu/src/monitor.card-action.lifecycle.test.ts b/extensions/feishu/src/monitor.card-action.lifecycle.test.ts index e297fff9a09..b7854bc9c30 100644 --- a/extensions/feishu/src/monitor.card-action.lifecycle.test.ts +++ b/extensions/feishu/src/monitor.card-action.lifecycle.test.ts @@ -2,10 +2,11 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { createPluginRuntimeMock } from "../../../test/helpers/extensions/plugin-runtime-mock.js"; import type { ClawdbotConfig, PluginRuntime, RuntimeEnv } from "../runtime-api.js"; import { createFeishuCardInteractionEnvelope } from "./card-interaction.js"; -import { monitorSingleAccount } from "./monitor.account.js"; -import { setFeishuRuntime } from "./runtime.js"; import type { ResolvedFeishuAccount } from "./types.js"; +type MonitorSingleAccount = typeof import("./monitor.account.js").monitorSingleAccount; +type SetFeishuRuntime = typeof import("./runtime.js").setFeishuRuntime; + type BoundConversation = { bindingId: string; targetSessionKey: string; @@ -35,6 +36,8 @@ const listFeishuThreadMessagesMock = vi.hoisted(() => vi.fn(async () => [])); let handlers: Record Promise> = {}; let lastRuntime: RuntimeEnv | null = null; +let monitorSingleAccount: MonitorSingleAccount; +let setFeishuRuntime: SetFeishuRuntime; const originalStateDir = process.env.OPENCLAW_STATE_DIR; vi.mock("./client.js", async () => { @@ -200,6 +203,7 @@ async function setupLifecycleMonitor() { cfg: createLifecycleConfig(), account: createLifecycleAccount(), runtime: lastRuntime, + fireAndForget: false, botOpenIdSource: { kind: "prefetched", botOpenId: "ou_bot_1", @@ -215,7 +219,15 @@ async function setupLifecycleMonitor() { } describe("Feishu card-action lifecycle", () => { - beforeEach(() => { + beforeEach(async () => { + vi.resetModules(); + vi.doUnmock("./bot.js"); + vi.doUnmock("./card-action.js"); + vi.doUnmock("./monitor.account.js"); + vi.doUnmock("./runtime.js"); + ({ monitorSingleAccount } = await import("./monitor.account.js")); + ({ setFeishuRuntime } = await import("./runtime.js")); + vi.clearAllMocks(); handlers = {}; lastRuntime = null; @@ -318,6 +330,11 @@ describe("Feishu card-action lifecycle", () => { }); afterEach(() => { + vi.resetModules(); + vi.doUnmock("./bot.js"); + vi.doUnmock("./card-action.js"); + vi.doUnmock("./monitor.account.js"); + vi.doUnmock("./runtime.js"); if (originalStateDir === undefined) { delete process.env.OPENCLAW_STATE_DIR; return; diff --git a/extensions/feishu/src/monitor.reply-once.lifecycle.test.ts b/extensions/feishu/src/monitor.reply-once.lifecycle.test.ts index e78f0b28a3c..27ce03ac1b3 100644 --- a/extensions/feishu/src/monitor.reply-once.lifecycle.test.ts +++ b/extensions/feishu/src/monitor.reply-once.lifecycle.test.ts @@ -1,10 +1,11 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { createPluginRuntimeMock } from "../../../test/helpers/extensions/plugin-runtime-mock.js"; import type { ClawdbotConfig, PluginRuntime, RuntimeEnv } from "../runtime-api.js"; -import { monitorSingleAccount } from "./monitor.account.js"; -import { setFeishuRuntime } from "./runtime.js"; import type { ResolvedFeishuAccount } from "./types.js"; +type MonitorSingleAccount = typeof import("./monitor.account.js").monitorSingleAccount; +type SetFeishuRuntime = typeof import("./runtime.js").setFeishuRuntime; + const createEventDispatcherMock = vi.hoisted(() => vi.fn()); const monitorWebSocketMock = vi.hoisted(() => vi.fn(async () => {})); const monitorWebhookMock = vi.hoisted(() => vi.fn(async () => {})); @@ -31,6 +32,8 @@ const sendMessageFeishuMock = vi.hoisted(() => let handlers: Record Promise> = {}; let lastRuntime: RuntimeEnv | null = null; +let monitorSingleAccount: MonitorSingleAccount; +let setFeishuRuntime: SetFeishuRuntime; const originalStateDir = process.env.OPENCLAW_STATE_DIR; vi.mock("./client.js", async () => { @@ -186,6 +189,7 @@ async function setupLifecycleMonitor() { cfg: createLifecycleConfig(), account: createLifecycleAccount(), runtime: lastRuntime, + fireAndForget: false, botOpenIdSource: { kind: "prefetched", botOpenId: "ou_bot_1", @@ -201,7 +205,15 @@ async function setupLifecycleMonitor() { } describe("Feishu reply-once lifecycle", () => { - beforeEach(() => { + beforeEach(async () => { + vi.resetModules(); + vi.doUnmock("./bot.js"); + vi.doUnmock("./card-action.js"); + vi.doUnmock("./monitor.account.js"); + vi.doUnmock("./runtime.js"); + ({ monitorSingleAccount } = await import("./monitor.account.js")); + ({ setFeishuRuntime } = await import("./runtime.js")); + vi.clearAllMocks(); handlers = {}; lastRuntime = null; @@ -304,6 +316,11 @@ describe("Feishu reply-once lifecycle", () => { }); afterEach(() => { + vi.resetModules(); + vi.doUnmock("./bot.js"); + vi.doUnmock("./card-action.js"); + vi.doUnmock("./monitor.account.js"); + vi.doUnmock("./runtime.js"); if (originalStateDir === undefined) { delete process.env.OPENCLAW_STATE_DIR; return; diff --git a/extensions/openshell/src/remote-fs-bridge.test.ts b/extensions/openshell/src/remote-fs-bridge.test.ts index 5a245e1d8fb..e01205e4bc7 100644 --- a/extensions/openshell/src/remote-fs-bridge.test.ts +++ b/extensions/openshell/src/remote-fs-bridge.test.ts @@ -105,7 +105,7 @@ function rewriteLocalPaths(value: string, roots: { workspace: string; agent: str } function normalizeScriptForLocalShell(script: string) { - return script + const normalizedScript = script .replace( 'stats=$(stat -c "%F|%h" -- "$1")', `stats=$(python3 - "$1" <<'PY' @@ -125,6 +125,13 @@ kind = 'directory' if stat.S_ISDIR(st.st_mode) else 'regular file' if stat.S_ISR print(f"{kind}|{st.st_size}|{int(st.st_mtime)}") PY`, ); + const mutationHelperPattern = /python3 \/dev\/fd\/3 "\$@" 3<<'PY'\n([\s\S]*?)\nPY/; + const mutationHelperMatch = normalizedScript.match(mutationHelperPattern); + if (!mutationHelperMatch) { + return normalizedScript; + } + const helperSource = mutationHelperMatch[1]?.replaceAll("'", `'"'"'`) ?? ""; + return normalizedScript.replace(mutationHelperPattern, `python3 -c '${helperSource}' "$@"`); } describe("openshell remote fs bridge", () => { diff --git a/extensions/slack/src/monitor/slash.test-harness.ts b/extensions/slack/src/monitor/slash.test-harness.ts index 21d5910988e..48a11cf3460 100644 --- a/extensions/slack/src/monitor/slash.test-harness.ts +++ b/extensions/slack/src/monitor/slash.test-harness.ts @@ -12,10 +12,6 @@ const mocks = vi.hoisted(() => ({ resolveStorePathMock: vi.fn(), })); -vi.mock("openclaw/plugin-sdk/reply-runtime", () => ({ - dispatchReplyWithDispatcher: (...args: unknown[]) => mocks.dispatchMock(...args), - finalizeInboundContext: (...args: unknown[]) => mocks.finalizeInboundContextMock(...args), -})); vi.mock("openclaw/plugin-sdk/reply-runtime", async (importOriginal) => { const actual = await importOriginal(); return { @@ -42,16 +38,6 @@ vi.mock("openclaw/plugin-sdk/routing", async (importOriginal) => { }; }); -vi.mock("openclaw/plugin-sdk/channel-runtime", () => ({ - resolveConversationLabel: (...args: unknown[]) => mocks.resolveConversationLabelMock(...args), - createReplyPrefixOptions: (...args: unknown[]) => mocks.createReplyPrefixOptionsMock(...args), -})); - -vi.mock("openclaw/plugin-sdk/config-runtime", () => ({ - recordSessionMetaFromInbound: (...args: unknown[]) => - mocks.recordSessionMetaFromInboundMock(...args), - resolveStorePath: (...args: unknown[]) => mocks.resolveStorePathMock(...args), -})); vi.mock("openclaw/plugin-sdk/channel-runtime", async (importOriginal) => { const actual = await importOriginal(); return { diff --git a/extensions/telegram/src/bot.create-telegram-bot.test-harness.ts b/extensions/telegram/src/bot.create-telegram-bot.test-harness.ts index cf5c3652b25..1c4143b88a5 100644 --- a/extensions/telegram/src/bot.create-telegram-bot.test-harness.ts +++ b/extensions/telegram/src/bot.create-telegram-bot.test-harness.ts @@ -6,9 +6,21 @@ import type { GetReplyOptions, ReplyPayload } from "openclaw/plugin-sdk/reply-ru import { createReplyDispatcher } from "openclaw/plugin-sdk/reply-runtime"; import type { MockFn } from "openclaw/plugin-sdk/testing"; import { beforeEach, vi } from "vitest"; +import type { TelegramBotDeps } from "./bot-deps.js"; -type AnyMock = MockFn<(...args: unknown[]) => unknown>; -type AnyAsyncMock = MockFn<(...args: unknown[]) => Promise>; +type AnyMock = ReturnType; +type AnyAsyncMock = ReturnType; +type LoadConfigFn = typeof import("openclaw/plugin-sdk/config-runtime").loadConfig; +type ResolveStorePathFn = typeof import("openclaw/plugin-sdk/config-runtime").resolveStorePath; +type TelegramBotRuntimeForTest = NonNullable< + Parameters[0] +>; +type DispatchReplyWithBufferedBlockDispatcherFn = + typeof import("openclaw/plugin-sdk/reply-runtime").dispatchReplyWithBufferedBlockDispatcher; +type DispatchReplyWithBufferedBlockDispatcherResult = Awaited< + ReturnType +>; +type DispatchReplyHarnessParams = Parameters[0]; const { sessionStorePath } = vi.hoisted(() => ({ sessionStorePath: `/tmp/openclaw-telegram-${process.pid}-${process.env.VITEST_POOL_ID ?? "0"}.json`, @@ -22,12 +34,67 @@ export function getLoadWebMediaMock(): AnyMock { return loadWebMedia; } -vi.mock("openclaw/plugin-sdk/web-media", () => ({ - loadWebMedia, -})); -vi.mock("openclaw/plugin-sdk/web-media.js", () => ({ - loadWebMedia, -})); +const { loadConfig, resolveStorePathMock } = vi.hoisted( + (): { + loadConfig: MockFn; + resolveStorePathMock: MockFn; + } => ({ + loadConfig: vi.fn(() => ({})), + resolveStorePathMock: vi.fn( + (storePath?: string) => storePath ?? sessionStorePath, + ), + }), +); + +export function getLoadConfigMock(): AnyMock { + return loadConfig; +} +vi.doMock("openclaw/plugin-sdk/config-runtime", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + loadConfig, + resolveStorePath: resolveStorePathMock, + }; +}); + +const { readChannelAllowFromStore, upsertChannelPairingRequest } = vi.hoisted( + (): { + readChannelAllowFromStore: MockFn; + upsertChannelPairingRequest: AnyAsyncMock; + } => ({ + readChannelAllowFromStore: vi.fn(async () => [] as string[]), + upsertChannelPairingRequest: vi.fn(async () => ({ + code: "PAIRCODE", + created: true, + })), + }), +); + +export function getReadChannelAllowFromStoreMock(): AnyAsyncMock { + return readChannelAllowFromStore; +} + +export function getUpsertChannelPairingRequestMock(): AnyAsyncMock { + return upsertChannelPairingRequest; +} + +vi.doMock("openclaw/plugin-sdk/conversation-runtime", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + readChannelAllowFromStore, + upsertChannelPairingRequest, + }; +}); +vi.doMock("openclaw/plugin-sdk/conversation-runtime.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + readChannelAllowFromStore, + upsertChannelPairingRequest, + }; +}); // All spy variables used inside vi.mock("grammy", ...) must be created via // vi.hoisted() so they are available when the hoisted factory runs, regardless @@ -38,7 +105,7 @@ const grammySpies = vi.hoisted(() => ({ onSpy: vi.fn() as AnyMock, stopSpy: vi.fn() as AnyMock, commandSpy: vi.fn() as AnyMock, - botCtorSpy: vi.fn() as AnyMock, + botCtorSpy: vi.fn((_: string, __?: { client?: { fetch?: typeof fetch } }) => undefined), answerCallbackQuerySpy: vi.fn(async () => undefined) as AnyAsyncMock, sendChatActionSpy: vi.fn() as AnyMock, editMessageTextSpy: vi.fn(async () => ({ message_id: 88 })) as AnyAsyncMock, @@ -56,26 +123,26 @@ const grammySpies = vi.hoisted(() => ({ getFileSpy: vi.fn(async () => ({ file_path: "media/file.jpg" })) as AnyAsyncMock, })); -export const { - useSpy, - middlewareUseSpy, - onSpy, - stopSpy, - commandSpy, - botCtorSpy, - answerCallbackQuerySpy, - sendChatActionSpy, - editMessageTextSpy, - editMessageReplyMarkupSpy, - sendMessageDraftSpy, - setMessageReactionSpy, - setMyCommandsSpy, - getMeSpy, - sendMessageSpy, - sendAnimationSpy, - sendPhotoSpy, - getFileSpy, -} = grammySpies; +export const useSpy: MockFn<(arg: unknown) => void> = grammySpies.useSpy; +export const middlewareUseSpy: AnyMock = grammySpies.middlewareUseSpy; +export const onSpy: AnyMock = grammySpies.onSpy; +export const stopSpy: AnyMock = grammySpies.stopSpy; +export const commandSpy: AnyMock = grammySpies.commandSpy; +export const botCtorSpy: MockFn< + (token: string, options?: { client?: { fetch?: typeof fetch } }) => void +> = grammySpies.botCtorSpy; +export const answerCallbackQuerySpy: AnyAsyncMock = grammySpies.answerCallbackQuerySpy; +export const sendChatActionSpy: AnyMock = grammySpies.sendChatActionSpy; +export const editMessageTextSpy: AnyAsyncMock = grammySpies.editMessageTextSpy; +export const editMessageReplyMarkupSpy: AnyAsyncMock = grammySpies.editMessageReplyMarkupSpy; +export const sendMessageDraftSpy: AnyAsyncMock = grammySpies.sendMessageDraftSpy; +export const setMessageReactionSpy: AnyAsyncMock = grammySpies.setMessageReactionSpy; +export const setMyCommandsSpy: AnyAsyncMock = grammySpies.setMyCommandsSpy; +export const getMeSpy: AnyAsyncMock = grammySpies.getMeSpy; +export const sendMessageSpy: AnyAsyncMock = grammySpies.sendMessageSpy; +export const sendAnimationSpy: AnyAsyncMock = grammySpies.sendAnimationSpy; +export const sendPhotoSpy: AnyAsyncMock = grammySpies.sendPhotoSpy; +export const getFileSpy: AnyAsyncMock = grammySpies.getFileSpy; vi.mock("grammy", () => ({ Bot: class { @@ -103,66 +170,19 @@ vi.mock("grammy", () => ({ public token: string, public options?: { client?: { fetch?: typeof fetch } }, ) { - grammySpies.botCtorSpy(token, options); + (grammySpies.botCtorSpy as unknown as (token: string, options?: unknown) => void)( + token, + options, + ); } }, InputFile: class {}, + HttpError: class MockHttpError extends Error {}, + GrammyError: class MockGrammyError extends Error {}, + API_CONSTANTS: { DEFAULT_UPDATE_TYPES: [] }, + webhookCallback: vi.fn(), })); -const { loadConfig } = vi.hoisted((): { loadConfig: AnyMock } => ({ - loadConfig: vi.fn(() => ({})), -})); - -export function getLoadConfigMock(): AnyMock { - return loadConfig; -} -vi.mock("openclaw/plugin-sdk/config-runtime", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - loadConfig, - resolveStorePath: vi.fn((storePath) => storePath ?? sessionStorePath), - }; -}); - -const { readChannelAllowFromStore, upsertChannelPairingRequest } = vi.hoisted( - (): { - readChannelAllowFromStore: AnyAsyncMock; - upsertChannelPairingRequest: AnyAsyncMock; - } => ({ - readChannelAllowFromStore: vi.fn(async () => [] as string[]), - upsertChannelPairingRequest: vi.fn(async () => ({ - code: "PAIRCODE", - created: true, - })), - }), -); - -export function getReadChannelAllowFromStoreMock(): AnyAsyncMock { - return readChannelAllowFromStore; -} - -export function getUpsertChannelPairingRequestMock(): AnyAsyncMock { - return upsertChannelPairingRequest; -} - -vi.mock("openclaw/plugin-sdk/conversation-runtime", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - readChannelAllowFromStore, - upsertChannelPairingRequest, - }; -}); -vi.doMock("openclaw/plugin-sdk/conversation-runtime.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - readChannelAllowFromStore, - upsertChannelPairingRequest, - }; -}); - const skillCommandsHoisted = vi.hoisted(() => ({ listSkillCommandsForAgents: vi.fn(() => []), })); @@ -182,21 +202,8 @@ const replySpyHoisted = vi.hoisted(() => ({ >, })); export const listSkillCommandsForAgents = skillCommandsHoisted.listSkillCommandsForAgents; -export const replySpy = skillCommandsHoisted.replySpy; - -vi.mock("openclaw/plugin-sdk/reply-runtime", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - listSkillCommandsForAgents: skillCommandsHoisted.listSkillCommandsForAgents, - getReplyFromConfig: skillCommandsHoisted.replySpy, - __replySpy: skillCommandsHoisted.replySpy, - dispatchReplyWithBufferedBlockDispatcher: vi.fn( - async ({ ctx, replyOptions }: { ctx: MsgContext; replyOptions?: GetReplyOptions }) => { - await skillCommandsHoisted.replySpy(ctx, replyOptions); - return { queuedFinal: false }; - }, - ), +const buildModelsProviderData = modelProviderDataHoisted.buildModelsProviderData; +export const replySpy = replySpyHoisted.replySpy; async function dispatchHarnessReplies( params: DispatchReplyHarnessParams, @@ -250,9 +257,6 @@ const dispatchReplyHoisted = vi.hoisted(() => ({ }), ), })); -export const listSkillCommandsForAgents = skillCommandListHoisted.listSkillCommandsForAgents; -const buildModelsProviderData = modelProviderDataHoisted.buildModelsProviderData; -export const replySpy = replySpyHoisted.replySpy; export const dispatchReplyWithBufferedBlockDispatcher = dispatchReplyHoisted.dispatchReplyWithBufferedBlockDispatcher; @@ -304,33 +308,34 @@ vi.doMock("openclaw/plugin-sdk/reply-runtime", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, - listSkillCommandsForAgents: skillCommandListHoisted.listSkillCommandsForAgents, + listSkillCommandsForAgents: skillCommandsHoisted.listSkillCommandsForAgents, getReplyFromConfig: replySpyHoisted.replySpy, __replySpy: replySpyHoisted.replySpy, dispatchReplyWithBufferedBlockDispatcher: dispatchReplyHoisted.dispatchReplyWithBufferedBlockDispatcher, - buildModelsProviderData, + buildModelsProviderData: modelProviderDataHoisted.buildModelsProviderData, }; }); vi.doMock("openclaw/plugin-sdk/reply-runtime.js", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, - listSkillCommandsForAgents: skillCommandListHoisted.listSkillCommandsForAgents, + listSkillCommandsForAgents: skillCommandsHoisted.listSkillCommandsForAgents, getReplyFromConfig: replySpyHoisted.replySpy, __replySpy: replySpyHoisted.replySpy, dispatchReplyWithBufferedBlockDispatcher: dispatchReplyHoisted.dispatchReplyWithBufferedBlockDispatcher, - buildModelsProviderData, + buildModelsProviderData: modelProviderDataHoisted.buildModelsProviderData, }; }); const systemEventsHoisted = vi.hoisted(() => ({ - enqueueSystemEventSpy: vi.fn(), + enqueueSystemEventSpy: vi.fn(() => false), })); -export const enqueueSystemEventSpy: AnyMock = systemEventsHoisted.enqueueSystemEventSpy; +export const enqueueSystemEventSpy: MockFn = + systemEventsHoisted.enqueueSystemEventSpy; -vi.mock("openclaw/plugin-sdk/infra-runtime", async (importOriginal) => { +vi.doMock("openclaw/plugin-sdk/infra-runtime", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, @@ -343,7 +348,7 @@ const sentMessageCacheHoisted = vi.hoisted(() => ({ })); export const wasSentByBot = sentMessageCacheHoisted.wasSentByBot; -vi.mock("./sent-message-cache.js", () => ({ +vi.doMock("./sent-message-cache.js", () => ({ wasSentByBot: sentMessageCacheHoisted.wasSentByBot, recordSentMessage: vi.fn(), clearSentMessageCache: vi.fn(), @@ -360,12 +365,41 @@ const runnerHoisted = vi.hoisted(() => ({ })); export const sequentializeSpy: AnyMock = runnerHoisted.sequentializeSpy; export let sequentializeKey: ((ctx: unknown) => string) | undefined; -vi.mock("@grammyjs/runner", () => ({ - sequentialize: (keyFn: (ctx: unknown) => string) => { +export const throttlerSpy: AnyMock = runnerHoisted.throttlerSpy; +export const telegramBotRuntimeForTest: TelegramBotRuntimeForTest = { + Bot: class { + api = { + config: { use: grammySpies.useSpy }, + answerCallbackQuery: grammySpies.answerCallbackQuerySpy, + sendChatAction: grammySpies.sendChatActionSpy, + editMessageText: grammySpies.editMessageTextSpy, + editMessageReplyMarkup: grammySpies.editMessageReplyMarkupSpy, + sendMessageDraft: grammySpies.sendMessageDraftSpy, + setMessageReaction: grammySpies.setMessageReactionSpy, + setMyCommands: grammySpies.setMyCommandsSpy, + getMe: grammySpies.getMeSpy, + sendMessage: grammySpies.sendMessageSpy, + sendAnimation: grammySpies.sendAnimationSpy, + sendPhoto: grammySpies.sendPhotoSpy, + getFile: grammySpies.getFileSpy, + }; + use = grammySpies.middlewareUseSpy; + on = grammySpies.onSpy; + stop = grammySpies.stopSpy; + command = grammySpies.commandSpy; + catch = vi.fn(); + constructor( + public token: string, + public options?: { client?: { fetch?: typeof fetch } }, + ) { + (grammySpies.botCtorSpy as unknown as (token: string, options?: unknown) => void)( + token, + options, + ); + } + } as unknown as TelegramBotRuntimeForTest["Bot"], + sequentialize: ((keyFn: (ctx: unknown) => string) => { sequentializeKey = keyFn; - return runnerHoisted.sequentializeSpy(); - }, -})); return ( runnerHoisted.sequentializeSpy as unknown as () => ReturnType< TelegramBotRuntimeForTest["sequentialize"] @@ -392,8 +426,13 @@ export const telegramBotDepsForTest: TelegramBotDeps = { wasSentByBot: wasSentByBot as TelegramBotDeps["wasSentByBot"], }; -export const throttlerSpy: AnyMock = runnerHoisted.throttlerSpy; - +vi.doMock("./bot.runtime.js", () => telegramBotRuntimeForTest); +vi.mock("@grammyjs/runner", () => ({ + sequentialize: (keyFn: (ctx: unknown) => string) => { + sequentializeKey = keyFn; + return runnerHoisted.sequentializeSpy(); + }, +})); vi.mock("@grammyjs/transformer-throttler", () => ({ apiThrottler: () => runnerHoisted.throttlerSpy(), })); diff --git a/src/config/schema.help.ts b/src/config/schema.help.ts index 243d5585d5e..6269ec78b84 100644 --- a/src/config/schema.help.ts +++ b/src/config/schema.help.ts @@ -694,10 +694,6 @@ export const FIELD_HELP: Record = { "Optional Perplexity/OpenRouter chat-completions base URL override. Setting this opts Perplexity into the legacy Sonar/OpenRouter compatibility path.", "tools.web.search.perplexity.model": 'Optional Sonar/OpenRouter model override (default: "perplexity/sonar-pro"). Setting this opts Perplexity into the legacy chat-completions compatibility path.', - "Search provider id. Auto-detected from available API keys if omitted.", - "tools.web.search.maxResults": "Number of results to return (1-10).", - "tools.web.search.timeoutSeconds": "Timeout in seconds for web_search requests.", - "tools.web.search.cacheTtlMinutes": "Cache TTL in minutes for web_search results.", "tools.web.fetch.enabled": "Enable the web_fetch tool (lightweight HTTP fetch).", "tools.web.fetch.maxChars": "Max characters returned by web_fetch (truncated).", "tools.web.fetch.maxCharsCap": diff --git a/src/config/schema.labels.ts b/src/config/schema.labels.ts index 6c59fae78af..7b6913e431d 100644 --- a/src/config/schema.labels.ts +++ b/src/config/schema.labels.ts @@ -216,9 +216,23 @@ export const FIELD_LABELS: Record = { "tools.message.broadcast.enabled": "Enable Message Broadcast", "tools.web.search.enabled": "Enable Web Search Tool", "tools.web.search.provider": "Web Search Provider", + "tools.web.search.apiKey": "Brave Search API Key", "tools.web.search.maxResults": "Web Search Max Results", "tools.web.search.timeoutSeconds": "Web Search Timeout (sec)", "tools.web.search.cacheTtlMinutes": "Web Search Cache TTL (min)", + "tools.web.search.firecrawl.apiKey": "Web Search Firecrawl API Key", // pragma: allowlist secret + "tools.web.search.firecrawl.baseUrl": "Web Search Firecrawl Base URL", + "tools.web.search.brave.mode": "Brave Search Mode", + "tools.web.search.gemini.apiKey": "Web Search Gemini API Key", // pragma: allowlist secret + "tools.web.search.gemini.model": "Web Search Gemini Model", + "tools.web.search.grok.apiKey": "Web Search Grok API Key", // pragma: allowlist secret + "tools.web.search.grok.model": "Web Search Grok Model", + "tools.web.search.kimi.apiKey": "Web Search Kimi API Key", // pragma: allowlist secret + "tools.web.search.kimi.baseUrl": "Web Search Kimi Base URL", + "tools.web.search.kimi.model": "Web Search Kimi Model", + "tools.web.search.perplexity.apiKey": "Web Search Perplexity API Key", // pragma: allowlist secret + "tools.web.search.perplexity.baseUrl": "Web Search Perplexity Base URL", + "tools.web.search.perplexity.model": "Web Search Perplexity Model", "tools.web.fetch.enabled": "Enable Web Fetch Tool", "tools.web.fetch.maxChars": "Web Fetch Max Chars", "tools.web.fetch.maxCharsCap": "Web Fetch Hard Max Chars", diff --git a/src/config/types.tools.ts b/src/config/types.tools.ts index 6ccdacb6f00..cd36c942232 100644 --- a/src/config/types.tools.ts +++ b/src/config/types.tools.ts @@ -469,53 +469,7 @@ export type ToolsConfig = { timeoutSeconds?: number; /** Cache TTL in minutes for search results. */ cacheTtlMinutes?: number; - /** Perplexity-specific configuration (used when provider="perplexity"). */ - perplexity?: { - /** API key for Perplexity (defaults to PERPLEXITY_API_KEY env var). */ - apiKey?: SecretInput; - /** @deprecated Legacy Sonar/OpenRouter field. Ignored by Search API. */ - baseUrl?: string; - /** @deprecated Legacy Sonar/OpenRouter field. Ignored by Search API. */ - model?: string; - }; - /** Firecrawl-specific configuration (used when provider="firecrawl"). */ - firecrawl?: { - /** Firecrawl API key (defaults to FIRECRAWL_API_KEY env var). */ - apiKey?: SecretInput; - /** Base URL for API requests (defaults to "https://api.firecrawl.dev"). */ - baseUrl?: string; - }; - /** Grok-specific configuration (used when provider="grok"). */ - grok?: { - /** API key for xAI (defaults to XAI_API_KEY env var). */ - apiKey?: SecretInput; - /** Model to use (defaults to "grok-4-1-fast"). */ - model?: string; - /** Include inline citations in response text as markdown links (default: false). */ - inlineCitations?: boolean; - }; - /** Gemini-specific configuration (used when provider="gemini"). */ - gemini?: { - /** Gemini API key (defaults to GEMINI_API_KEY env var). */ - apiKey?: SecretInput; - /** Model to use for grounded search (defaults to "gemini-2.5-flash"). */ - model?: string; - }; - /** Kimi-specific configuration (used when provider="kimi"). */ - kimi?: { - /** Moonshot/Kimi API key (defaults to KIMI_API_KEY or MOONSHOT_API_KEY env var). */ - apiKey?: SecretInput; - /** Base URL for API requests (defaults to "https://api.moonshot.ai/v1"). */ - baseUrl?: string; - /** Model to use (defaults to "moonshot-v1-128k"). */ - model?: string; - }; - /** Brave-specific configuration (used when provider="brave"). */ - brave?: { - /** Brave Search mode: "web" (standard results) or "llm-context" (pre-extracted page content). Default: "web". */ - mode?: "web" | "llm-context"; - }; - }; + /** Provider-specific configuration (used when provider="brave"). */ /** @deprecated Legacy Brave scoped config. */ brave?: WebSearchLegacyProviderConfig; /** @deprecated Legacy Firecrawl scoped config. */ diff --git a/ui/src/ui/views/agents-utils.test.ts b/ui/src/ui/views/agents-utils.test.ts index 391ead64655..7ff71ba36de 100644 --- a/ui/src/ui/views/agents-utils.test.ts +++ b/ui/src/ui/views/agents-utils.test.ts @@ -141,6 +141,7 @@ describe("renderOverview", () => { navCollapsed: false, navWidth: 220, navGroupsCollapsed: {}, + borderRadius: 50, }, password: "", lastError: null,