Fix CI regressions across config and extension tests

This commit is contained in:
Junebugg1214 2026-03-19 22:16:04 -04:00
parent f76af1258a
commit f1653d2ea1
13 changed files with 283 additions and 199 deletions

View File

@ -629,12 +629,14 @@ export type MonitorSingleAccountParams = {
runtime?: RuntimeEnv;
abortSignal?: AbortSignal;
botOpenIdSource?: BotOpenIdSource;
fireAndForget?: boolean;
};
export async function monitorSingleAccount(params: MonitorSingleAccountParams): Promise<void> {
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") {

View File

@ -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<string, (data: unknown) => Promise<void>> = {};
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;

View File

@ -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<string, (data: unknown) => Promise<void>> = {};
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;

View File

@ -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<string, Record<string, (data: unknown) => Promise<void>>>();
let runtimesByAccount = new Map<string, RuntimeEnv>();
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;

View File

@ -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<string, (data: unknown) => Promise<void>> = {};
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;

View File

@ -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<string, (data: unknown) => Promise<void>> = {};
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;

View File

@ -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", () => {

View File

@ -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<typeof import("openclaw/plugin-sdk/reply-runtime")>();
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<typeof import("openclaw/plugin-sdk/channel-runtime")>();
return {

View File

@ -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<unknown>>;
type AnyMock = ReturnType<typeof vi.fn>;
type AnyAsyncMock = ReturnType<typeof vi.fn>;
type LoadConfigFn = typeof import("openclaw/plugin-sdk/config-runtime").loadConfig;
type ResolveStorePathFn = typeof import("openclaw/plugin-sdk/config-runtime").resolveStorePath;
type TelegramBotRuntimeForTest = NonNullable<
Parameters<typeof import("./bot.js").setTelegramBotRuntimeForTest>[0]
>;
type DispatchReplyWithBufferedBlockDispatcherFn =
typeof import("openclaw/plugin-sdk/reply-runtime").dispatchReplyWithBufferedBlockDispatcher;
type DispatchReplyWithBufferedBlockDispatcherResult = Awaited<
ReturnType<DispatchReplyWithBufferedBlockDispatcherFn>
>;
type DispatchReplyHarnessParams = Parameters<DispatchReplyWithBufferedBlockDispatcherFn>[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<LoadConfigFn>;
resolveStorePathMock: MockFn<ResolveStorePathFn>;
} => ({
loadConfig: vi.fn<LoadConfigFn>(() => ({})),
resolveStorePathMock: vi.fn<ResolveStorePathFn>(
(storePath?: string) => storePath ?? sessionStorePath,
),
}),
);
export function getLoadConfigMock(): AnyMock {
return loadConfig;
}
vi.doMock("openclaw/plugin-sdk/config-runtime", async (importOriginal) => {
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/config-runtime")>();
return {
...actual,
loadConfig,
resolveStorePath: resolveStorePathMock,
};
});
const { readChannelAllowFromStore, upsertChannelPairingRequest } = vi.hoisted(
(): {
readChannelAllowFromStore: MockFn<TelegramBotDeps["readChannelAllowFromStore"]>;
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<typeof import("openclaw/plugin-sdk/conversation-runtime")>();
return {
...actual,
readChannelAllowFromStore,
upsertChannelPairingRequest,
};
});
vi.doMock("openclaw/plugin-sdk/conversation-runtime.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/conversation-runtime")>();
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<typeof import("openclaw/plugin-sdk/config-runtime")>();
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<typeof import("openclaw/plugin-sdk/conversation-runtime")>();
return {
...actual,
readChannelAllowFromStore,
upsertChannelPairingRequest,
};
});
vi.doMock("openclaw/plugin-sdk/conversation-runtime.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/conversation-runtime")>();
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<typeof import("openclaw/plugin-sdk/reply-runtime")>();
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<typeof import("openclaw/plugin-sdk/reply-runtime")>();
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<typeof import("openclaw/plugin-sdk/reply-runtime")>();
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<TelegramBotDeps["enqueueSystemEvent"]>(() => false),
}));
export const enqueueSystemEventSpy: AnyMock = systemEventsHoisted.enqueueSystemEventSpy;
export const enqueueSystemEventSpy: MockFn<TelegramBotDeps["enqueueSystemEvent"]> =
systemEventsHoisted.enqueueSystemEventSpy;
vi.mock("openclaw/plugin-sdk/infra-runtime", async (importOriginal) => {
vi.doMock("openclaw/plugin-sdk/infra-runtime", async (importOriginal) => {
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/infra-runtime")>();
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(),
}));

View File

@ -694,10 +694,6 @@ export const FIELD_HELP: Record<string, string> = {
"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":

View File

@ -216,9 +216,23 @@ export const FIELD_LABELS: Record<string, string> = {
"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",

View File

@ -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. */

View File

@ -141,6 +141,7 @@ describe("renderOverview", () => {
navCollapsed: false,
navWidth: 220,
navGroupsCollapsed: {},
borderRadius: 50,
},
password: "",
lastError: null,