test(telegram): rewire bot harnesses to runtime seams

This commit is contained in:
Ayaan Zaidi 2026-03-18 09:47:58 +05:30
parent edcf3e9d32
commit 23f618d62d
No known key found for this signature in database
3 changed files with 126 additions and 42 deletions

View File

@ -7,6 +7,21 @@ import { beforeEach, vi } from "vitest";
type AnyMock = MockFn<(...args: unknown[]) => unknown>;
type AnyAsyncMock = MockFn<(...args: unknown[]) => Promise<unknown>>;
type DispatchReplyWithBufferedBlockDispatcherFn =
typeof import("openclaw/plugin-sdk/reply-runtime").dispatchReplyWithBufferedBlockDispatcher;
type DispatchReplyWithBufferedBlockDispatcherResult = Awaited<
ReturnType<DispatchReplyWithBufferedBlockDispatcherFn>
>;
type DispatchReplyHarnessParams = {
ctx: MsgContext;
replyOptions?: GetReplyOptions;
dispatcherOptions?: {
typingCallbacks?: {
start?: () => void | Promise<void>;
};
deliver?: (payload: ReplyPayload, info: { kind: "final" }) => void | Promise<void>;
};
};
const { sessionStorePath } = vi.hoisted(() => ({
sessionStorePath: `/tmp/openclaw-telegram-${process.pid}-${process.env.VITEST_POOL_ID ?? "0"}.json`,
@ -20,18 +35,21 @@ export function getLoadWebMediaMock(): AnyMock {
return loadWebMedia;
}
vi.mock("openclaw/plugin-sdk/web-media", () => ({
vi.doMock("openclaw/plugin-sdk/web-media", () => ({
loadWebMedia,
}));
const { loadConfig } = vi.hoisted((): { loadConfig: AnyMock } => ({
loadConfig: vi.fn(() => ({})),
}));
const { resolveStorePathMock } = vi.hoisted((): { resolveStorePathMock: AnyMock } => ({
resolveStorePathMock: vi.fn((storePath?: string) => storePath ?? sessionStorePath),
}));
export function getLoadConfigMock(): AnyMock {
return loadConfig;
}
vi.mock("openclaw/plugin-sdk/config-runtime", async (importOriginal) => {
vi.doMock("openclaw/plugin-sdk/config-runtime", async (importOriginal) => {
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/config-runtime")>();
return {
...actual,
@ -39,11 +57,11 @@ vi.mock("openclaw/plugin-sdk/config-runtime", async (importOriginal) => {
};
});
vi.mock("openclaw/plugin-sdk/config-runtime", async (importOriginal) => {
vi.doMock("openclaw/plugin-sdk/config-runtime", async (importOriginal) => {
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/config-runtime")>();
return {
...actual,
resolveStorePath: vi.fn((storePath) => storePath ?? sessionStorePath),
resolveStorePath: resolveStorePathMock,
};
});
@ -68,7 +86,7 @@ export function getUpsertChannelPairingRequestMock(): AnyAsyncMock {
return upsertChannelPairingRequest;
}
vi.mock("openclaw/plugin-sdk/conversation-runtime", async (importOriginal) => {
vi.doMock("openclaw/plugin-sdk/conversation-runtime", async (importOriginal) => {
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/conversation-runtime")>();
return {
...actual,
@ -89,23 +107,36 @@ const skillCommandsHoisted = vi.hoisted(() => ({
configOverride?: OpenClawConfig,
) => Promise<ReplyPayload | ReplyPayload[] | undefined>
>,
dispatchReplyWithBufferedBlockDispatcher: vi.fn<DispatchReplyWithBufferedBlockDispatcherFn>(
async (params: DispatchReplyHarnessParams) => {
const result: DispatchReplyWithBufferedBlockDispatcherResult = {
queuedFinal: false,
counts: {} as DispatchReplyWithBufferedBlockDispatcherResult["counts"],
};
await params.dispatcherOptions?.typingCallbacks?.start?.();
const reply = await skillCommandsHoisted.replySpy(params.ctx, params.replyOptions);
const payloads = reply === undefined ? [] : Array.isArray(reply) ? reply : [reply];
for (const payload of payloads) {
await params.dispatcherOptions?.deliver?.(payload, { kind: "final" });
}
return result;
},
),
}));
export const listSkillCommandsForAgents = skillCommandsHoisted.listSkillCommandsForAgents;
export const replySpy = skillCommandsHoisted.replySpy;
export const dispatchReplyWithBufferedBlockDispatcher =
skillCommandsHoisted.dispatchReplyWithBufferedBlockDispatcher;
vi.mock("openclaw/plugin-sdk/reply-runtime", async (importOriginal) => {
vi.doMock("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 };
},
),
dispatchReplyWithBufferedBlockDispatcher:
skillCommandsHoisted.dispatchReplyWithBufferedBlockDispatcher,
};
});
@ -114,7 +145,7 @@ const systemEventsHoisted = vi.hoisted(() => ({
}));
export const enqueueSystemEventSpy: AnyMock = 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,
@ -127,7 +158,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(),
@ -181,7 +212,19 @@ export const {
getFileSpy,
} = grammySpies;
vi.mock("grammy", () => ({
const runnerHoisted = vi.hoisted(() => ({
sequentializeMiddleware: vi.fn(async (_ctx: unknown, next?: () => Promise<void>) => {
if (typeof next === "function") {
await next();
}
}),
sequentializeSpy: vi.fn(() => runnerHoisted.sequentializeMiddleware),
throttlerSpy: vi.fn(() => "throttler"),
}));
export const sequentializeSpy: AnyMock = runnerHoisted.sequentializeSpy;
export let sequentializeKey: ((ctx: unknown) => string) | undefined;
export const throttlerSpy: AnyMock = runnerHoisted.throttlerSpy;
export const telegramBotRuntimeForTest = {
Bot: class {
api = {
config: { use: grammySpies.useSpy },
@ -210,32 +253,30 @@ vi.mock("grammy", () => ({
grammySpies.botCtorSpy(token, options);
}
},
InputFile: class {},
}));
const runnerHoisted = vi.hoisted(() => ({
sequentializeMiddleware: vi.fn(async (_ctx: unknown, next?: () => Promise<void>) => {
if (typeof next === "function") {
await next();
}
}),
sequentializeSpy: vi.fn(() => runnerHoisted.sequentializeMiddleware),
throttlerSpy: vi.fn(() => "throttler"),
}));
export const sequentializeSpy: AnyMock = runnerHoisted.sequentializeSpy;
export let sequentializeKey: ((ctx: unknown) => string) | undefined;
vi.mock("@grammyjs/runner", () => ({
sequentialize: (keyFn: (ctx: unknown) => string) => {
sequentializeKey = keyFn;
return runnerHoisted.sequentializeSpy();
},
}));
export const throttlerSpy: AnyMock = runnerHoisted.throttlerSpy;
vi.mock("@grammyjs/transformer-throttler", () => ({
apiThrottler: () => runnerHoisted.throttlerSpy(),
}));
loadConfig,
};
export const telegramBotMessageDispatchRuntimeForTest = {
dispatchReplyWithBufferedBlockDispatcher,
};
export const telegramBotNativeCommandsRuntimeForTest = {
dispatchReplyWithBufferedBlockDispatcher,
listSkillCommandsForAgents,
};
export const telegramBotHandlersRuntimeForTest = {
loadConfig,
resolveStorePath: resolveStorePathMock,
readChannelAllowFromStore,
enqueueSystemEvent: enqueueSystemEventSpy,
listSkillCommandsForAgents,
wasSentByBot,
};
vi.doMock("./bot.runtime.js", () => telegramBotRuntimeForTest);
export const getOnHandler = (event: string) => {
const handler = onSpy.mock.calls.find((call) => call[0] === event)?.[1];
@ -310,6 +351,8 @@ beforeEach(() => {
resetInboundDedupe();
loadConfig.mockReset();
loadConfig.mockReturnValue(DEFAULT_TELEGRAM_TEST_CONFIG);
resolveStorePathMock.mockReset();
resolveStorePathMock.mockImplementation((storePath?: string) => storePath ?? sessionStorePath);
loadWebMedia.mockReset();
readChannelAllowFromStore.mockReset();
readChannelAllowFromStore.mockResolvedValue([]);
@ -324,6 +367,22 @@ beforeEach(() => {
await opts?.onReplyStart?.();
return undefined;
});
dispatchReplyWithBufferedBlockDispatcher.mockReset();
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(
async (params: DispatchReplyHarnessParams) => {
const result: DispatchReplyWithBufferedBlockDispatcherResult = {
queuedFinal: false,
counts: {} as DispatchReplyWithBufferedBlockDispatcherResult["counts"],
};
await params.dispatcherOptions?.typingCallbacks?.start?.();
const reply = await replySpy(params.ctx, params.replyOptions);
const payloads = reply === undefined ? [] : Array.isArray(reply) ? reply : [reply];
for (const payload of payloads) {
await params.dispatcherOptions?.deliver?.(payload, { kind: "final" });
}
return result;
},
);
sendAnimationSpy.mockReset();
sendAnimationSpy.mockResolvedValue({ message_id: 78 });

View File

@ -5,7 +5,7 @@ import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
import { escapeRegExp, formatEnvelopeTimestamp } from "../../../test/helpers/envelope-timestamp.js";
import { withEnvAsync } from "../../../test/helpers/extensions/env.js";
import { useFrozenTime, useRealTime } from "../../../test/helpers/extensions/frozen-time.js";
import {
const {
answerCallbackQuerySpy,
botCtorSpy,
commandSpy,
@ -26,13 +26,26 @@ import {
sequentializeSpy,
setMessageReactionSpy,
setMyCommandsSpy,
telegramBotHandlersRuntimeForTest,
telegramBotMessageDispatchRuntimeForTest,
telegramBotNativeCommandsRuntimeForTest,
telegramBotRuntimeForTest,
throttlerSpy,
useSpy,
} from "./bot.create-telegram-bot.test-harness.js";
} = await import("./bot.create-telegram-bot.test-harness.js");
import { resolveTelegramFetch } from "./fetch.js";
// Import after the harness registers `vi.mock(...)` for grammY and Telegram internals.
const { createTelegramBot, getTelegramSequentialKey } = await import("./bot.js");
const { createTelegramBot, getTelegramSequentialKey, setTelegramBotRuntimeForTest } =
await import("./bot.js");
const { setBotHandlersRuntimeForTest } = await import("./bot-handlers.runtime.js");
const { setBotMessageDispatchRuntimeForTest } = await import("./bot-message-dispatch.js");
const { setBotNativeCommandsRuntimeForTest } = await import("./bot-native-commands.js");
setTelegramBotRuntimeForTest(telegramBotRuntimeForTest);
setBotHandlersRuntimeForTest(telegramBotHandlersRuntimeForTest);
setBotMessageDispatchRuntimeForTest(telegramBotMessageDispatchRuntimeForTest);
setBotNativeCommandsRuntimeForTest(telegramBotNativeCommandsRuntimeForTest);
const loadConfig = getLoadConfigMock();
const loadWebMedia = getLoadWebMediaMock();

View File

@ -7,7 +7,7 @@ import {
registerPluginInteractiveHandler,
} from "../../../src/plugins/interactive.js";
import { escapeRegExp, formatEnvelopeTimestamp } from "../../../test/helpers/envelope-timestamp.js";
import {
const {
answerCallbackQuerySpy,
commandSpy,
editMessageReplyMarkupSpy,
@ -22,8 +22,12 @@ import {
replySpy,
sendMessageSpy,
setMyCommandsSpy,
telegramBotHandlersRuntimeForTest,
telegramBotMessageDispatchRuntimeForTest,
telegramBotNativeCommandsRuntimeForTest,
telegramBotRuntimeForTest,
wasSentByBot,
} from "./bot.create-telegram-bot.test-harness.js";
} = await import("./bot.create-telegram-bot.test-harness.js");
// Import after the harness registers `vi.mock(...)` for grammY and Telegram internals.
const { listNativeCommandSpecs, listNativeCommandSpecsForConfig } =
@ -31,7 +35,15 @@ const { listNativeCommandSpecs, listNativeCommandSpecsForConfig } =
const { loadSessionStore } = await import("../../../src/config/sessions.js");
const { normalizeTelegramCommandName } =
await import("../../../src/config/telegram-custom-commands.js");
const { createTelegramBot } = await import("./bot.js");
const { createTelegramBot, setTelegramBotRuntimeForTest } = await import("./bot.js");
const { setBotHandlersRuntimeForTest } = await import("./bot-handlers.runtime.js");
const { setBotMessageDispatchRuntimeForTest } = await import("./bot-message-dispatch.js");
const { setBotNativeCommandsRuntimeForTest } = await import("./bot-native-commands.js");
setTelegramBotRuntimeForTest(telegramBotRuntimeForTest);
setBotHandlersRuntimeForTest(telegramBotHandlersRuntimeForTest);
setBotMessageDispatchRuntimeForTest(telegramBotMessageDispatchRuntimeForTest);
setBotNativeCommandsRuntimeForTest(telegramBotNativeCommandsRuntimeForTest);
const loadConfig = getLoadConfigMock();
const readChannelAllowFromStore = getReadChannelAllowFromStoreMock();