import { beforeEach, vi } from "vitest"; import { resetInboundDedupe } from "../auto-reply/reply/inbound-dedupe.js"; import type { MsgContext } from "../auto-reply/templating.js"; import type { GetReplyOptions, ReplyPayload } from "../auto-reply/types.js"; import type { OpenClawConfig } from "../config/config.js"; import type { MockFn } from "../test-utils/vitest-mock-fn.js"; type AnyMock = MockFn<(...args: unknown[]) => unknown>; type AnyAsyncMock = MockFn<(...args: unknown[]) => Promise>; const { sessionStorePath } = vi.hoisted(() => ({ sessionStorePath: `/tmp/openclaw-telegram-${process.pid}-${process.env.VITEST_POOL_ID ?? "0"}.json`, })); const { loadWebMedia } = vi.hoisted((): { loadWebMedia: AnyMock } => ({ loadWebMedia: vi.fn(), })); export function getLoadWebMediaMock(): AnyMock { return loadWebMedia; } vi.mock("../web/media.js", () => ({ loadWebMedia, })); const { loadConfig } = vi.hoisted((): { loadConfig: AnyMock } => ({ loadConfig: vi.fn(() => ({})), })); export function getLoadConfigMock(): AnyMock { return loadConfig; } vi.mock("../config/config.js", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, loadConfig, }; }); vi.mock("../config/sessions.js", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, 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("../pairing/pairing-store.js", () => ({ readChannelAllowFromStore, upsertChannelPairingRequest, })); const skillCommandsHoisted = vi.hoisted(() => ({ listSkillCommandsForAgents: vi.fn(() => []), })); export const listSkillCommandsForAgents = skillCommandsHoisted.listSkillCommandsForAgents; vi.mock("../auto-reply/skill-commands.js", () => ({ listSkillCommandsForAgents, })); const systemEventsHoisted = vi.hoisted(() => ({ enqueueSystemEventSpy: vi.fn(), })); export const enqueueSystemEventSpy: AnyMock = systemEventsHoisted.enqueueSystemEventSpy; vi.mock("../infra/system-events.js", () => ({ enqueueSystemEvent: enqueueSystemEventSpy, })); const sentMessageCacheHoisted = vi.hoisted(() => ({ wasSentByBot: vi.fn(() => false), })); export const wasSentByBot = sentMessageCacheHoisted.wasSentByBot; vi.mock("./sent-message-cache.js", () => ({ wasSentByBot, recordSentMessage: vi.fn(), clearSentMessageCache: vi.fn(), })); export const useSpy: MockFn<(arg: unknown) => void> = vi.fn(); export const middlewareUseSpy: AnyMock = vi.fn(); export const onSpy: AnyMock = vi.fn(); export const stopSpy: AnyMock = vi.fn(); export const commandSpy: AnyMock = vi.fn(); export const botCtorSpy: AnyMock = vi.fn(); export const answerCallbackQuerySpy: AnyAsyncMock = vi.fn(async () => undefined); export const sendChatActionSpy: AnyMock = vi.fn(); export const editMessageTextSpy: AnyAsyncMock = vi.fn(async () => ({ message_id: 88 })); export const editMessageReplyMarkupSpy: AnyAsyncMock = vi.fn(async () => ({ message_id: 88 })); export const sendMessageDraftSpy: AnyAsyncMock = vi.fn(async () => true); export const setMessageReactionSpy: AnyAsyncMock = vi.fn(async () => undefined); export const setMyCommandsSpy: AnyAsyncMock = vi.fn(async () => undefined); export const getMeSpy: AnyAsyncMock = vi.fn(async () => ({ username: "openclaw_bot", has_topics_enabled: true, })); export const sendMessageSpy: AnyAsyncMock = vi.fn(async () => ({ message_id: 77 })); export const sendAnimationSpy: AnyAsyncMock = vi.fn(async () => ({ message_id: 78 })); export const sendPhotoSpy: AnyAsyncMock = vi.fn(async () => ({ message_id: 79 })); export const getFileSpy: AnyAsyncMock = vi.fn(async () => ({ file_path: "media/file.jpg" })); type ApiStub = { config: { use: (arg: unknown) => void }; answerCallbackQuery: typeof answerCallbackQuerySpy; sendChatAction: typeof sendChatActionSpy; editMessageText: typeof editMessageTextSpy; editMessageReplyMarkup: typeof editMessageReplyMarkupSpy; sendMessageDraft: typeof sendMessageDraftSpy; setMessageReaction: typeof setMessageReactionSpy; setMyCommands: typeof setMyCommandsSpy; getMe: typeof getMeSpy; sendMessage: typeof sendMessageSpy; sendAnimation: typeof sendAnimationSpy; sendPhoto: typeof sendPhotoSpy; getFile: typeof getFileSpy; }; const apiStub: ApiStub = { config: { use: useSpy }, answerCallbackQuery: answerCallbackQuerySpy, sendChatAction: sendChatActionSpy, editMessageText: editMessageTextSpy, editMessageReplyMarkup: editMessageReplyMarkupSpy, sendMessageDraft: sendMessageDraftSpy, setMessageReaction: setMessageReactionSpy, setMyCommands: setMyCommandsSpy, getMe: getMeSpy, sendMessage: sendMessageSpy, sendAnimation: sendAnimationSpy, sendPhoto: sendPhotoSpy, getFile: getFileSpy, }; vi.mock("grammy", () => ({ Bot: class { api = apiStub; use = middlewareUseSpy; on = onSpy; stop = stopSpy; command = commandSpy; catch = vi.fn(); constructor( public token: string, public options?: { client?: { fetch?: typeof fetch } }, ) { botCtorSpy(token, options); } }, InputFile: class {}, })); const sequentializeMiddleware = vi.fn(); export const sequentializeSpy: AnyMock = vi.fn(() => sequentializeMiddleware); export let sequentializeKey: ((ctx: unknown) => string) | undefined; vi.mock("@grammyjs/runner", () => ({ sequentialize: (keyFn: (ctx: unknown) => string) => { sequentializeKey = keyFn; return sequentializeSpy(); }, })); export const throttlerSpy: AnyMock = vi.fn(() => "throttler"); vi.mock("@grammyjs/transformer-throttler", () => ({ apiThrottler: () => throttlerSpy(), })); export const replySpy: MockFn< ( ctx: MsgContext, opts?: GetReplyOptions, configOverride?: OpenClawConfig, ) => Promise > = vi.fn(async (_ctx, opts) => { await opts?.onReplyStart?.(); return undefined; }); vi.mock("../auto-reply/reply.js", () => ({ getReplyFromConfig: replySpy, __replySpy: replySpy, })); export const getOnHandler = (event: string) => { const handler = onSpy.mock.calls.find((call) => call[0] === event)?.[1]; if (!handler) { throw new Error(`Missing handler for event: ${event}`); } return handler as (ctx: Record) => Promise; }; const DEFAULT_TELEGRAM_TEST_CONFIG: OpenClawConfig = { agents: { defaults: { envelopeTimezone: "utc", }, }, channels: { telegram: { dmPolicy: "open", allowFrom: ["*"] }, }, }; export function makeTelegramMessageCtx(params: { chat: { id: number; type: string; title?: string; is_forum?: boolean; }; from: { id: number; username?: string }; text: string; date?: number; messageId?: number; messageThreadId?: number; }) { return { message: { chat: params.chat, from: params.from, text: params.text, date: params.date ?? 1736380800, message_id: params.messageId ?? 42, ...(params.messageThreadId === undefined ? {} : { message_thread_id: params.messageThreadId }), }, me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }; } export function makeForumGroupMessageCtx(params?: { chatId?: number; threadId?: number; text?: string; fromId?: number; username?: string; title?: string; }) { return makeTelegramMessageCtx({ chat: { id: params?.chatId ?? -1001234567890, type: "supergroup", title: params?.title ?? "Forum Group", is_forum: true, }, from: { id: params?.fromId ?? 12345, username: params?.username ?? "testuser" }, text: params?.text ?? "hello", messageThreadId: params?.threadId, }); } beforeEach(() => { resetInboundDedupe(); loadConfig.mockReset(); loadConfig.mockReturnValue(DEFAULT_TELEGRAM_TEST_CONFIG); loadWebMedia.mockReset(); readChannelAllowFromStore.mockReset(); readChannelAllowFromStore.mockResolvedValue([]); upsertChannelPairingRequest.mockReset(); upsertChannelPairingRequest.mockResolvedValue({ code: "PAIRCODE", created: true } as const); onSpy.mockReset(); commandSpy.mockReset(); stopSpy.mockReset(); useSpy.mockReset(); replySpy.mockReset(); replySpy.mockImplementation(async (_ctx, opts) => { await opts?.onReplyStart?.(); return undefined; }); sendAnimationSpy.mockReset(); sendAnimationSpy.mockResolvedValue({ message_id: 78 }); sendPhotoSpy.mockReset(); sendPhotoSpy.mockResolvedValue({ message_id: 79 }); sendMessageSpy.mockReset(); sendMessageSpy.mockResolvedValue({ message_id: 77 }); getFileSpy.mockReset(); getFileSpy.mockResolvedValue({ file_path: "media/file.jpg" }); setMessageReactionSpy.mockReset(); setMessageReactionSpy.mockResolvedValue(undefined); answerCallbackQuerySpy.mockReset(); answerCallbackQuerySpy.mockResolvedValue(undefined); sendChatActionSpy.mockReset(); sendChatActionSpy.mockResolvedValue(undefined); setMyCommandsSpy.mockReset(); setMyCommandsSpy.mockResolvedValue(undefined); getMeSpy.mockReset(); getMeSpy.mockResolvedValue({ username: "openclaw_bot", has_topics_enabled: true, }); editMessageTextSpy.mockReset(); editMessageTextSpy.mockResolvedValue({ message_id: 88 }); editMessageReplyMarkupSpy.mockReset(); editMessageReplyMarkupSpy.mockResolvedValue({ message_id: 88 }); sendMessageDraftSpy.mockReset(); sendMessageDraftSpy.mockResolvedValue(true); enqueueSystemEventSpy.mockReset(); wasSentByBot.mockReset(); wasSentByBot.mockReturnValue(false); listSkillCommandsForAgents.mockReset(); listSkillCommandsForAgents.mockReturnValue([]); middlewareUseSpy.mockReset(); sequentializeSpy.mockReset(); botCtorSpy.mockReset(); sequentializeKey = undefined; });