import { beforeEach, vi } from "vitest"; import type { MockFn } from "../test-utils/vitest-mock-fn.js"; import { resetInboundDedupe } from "../auto-reply/reply/inbound-dedupe.js"; type AnyMock = MockFn<(...args: unknown[]) => unknown>; type AnyAsyncMock = MockFn<(...args: unknown[]) => Promise>; type ReplyOpts = | { onReplyStart?: () => void | Promise; } | undefined; const { sessionStorePath } = vi.hoisted(() => ({ sessionStorePath: `/tmp/openclaw-telegram-${Math.random().toString(16).slice(2)}.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, })); 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 setMessageReactionSpy: AnyAsyncMock = vi.fn(async () => undefined); export const setMyCommandsSpy: AnyAsyncMock = vi.fn(async () => undefined); export const deleteMyCommandsSpy: 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 })); type ApiStub = { config: { use: (arg: unknown) => void }; answerCallbackQuery: typeof answerCallbackQuerySpy; sendChatAction: typeof sendChatActionSpy; setMessageReaction: typeof setMessageReactionSpy; setMyCommands: typeof setMyCommandsSpy; deleteMyCommands: typeof deleteMyCommandsSpy; getMe: typeof getMeSpy; sendMessage: typeof sendMessageSpy; sendAnimation: typeof sendAnimationSpy; sendPhoto: typeof sendPhotoSpy; }; const apiStub: ApiStub = { config: { use: useSpy }, answerCallbackQuery: answerCallbackQuerySpy, sendChatAction: sendChatActionSpy, setMessageReaction: setMessageReactionSpy, setMyCommands: setMyCommandsSpy, deleteMyCommands: deleteMyCommandsSpy, getMe: getMeSpy, sendMessage: sendMessageSpy, sendAnimation: sendAnimationSpy, sendPhoto: sendPhotoSpy, }; 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 {}, webhookCallback: vi.fn(), })); 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: unknown, opts?: ReplyOpts) => 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; }; 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.mockReturnValue({ channels: { telegram: { dmPolicy: "open", allowFrom: ["*"] }, }, }); loadWebMedia.mockReset(); onSpy.mockReset(); commandSpy.mockReset(); stopSpy.mockReset(); useSpy.mockReset(); sendAnimationSpy.mockReset(); sendAnimationSpy.mockResolvedValue({ message_id: 78 }); sendPhotoSpy.mockReset(); sendPhotoSpy.mockResolvedValue({ message_id: 79 }); sendMessageSpy.mockReset(); sendMessageSpy.mockResolvedValue({ message_id: 77 }); setMessageReactionSpy.mockReset(); setMessageReactionSpy.mockResolvedValue(undefined); answerCallbackQuerySpy.mockReset(); answerCallbackQuerySpy.mockResolvedValue(undefined); sendChatActionSpy.mockReset(); sendChatActionSpy.mockResolvedValue(undefined); setMyCommandsSpy.mockReset(); setMyCommandsSpy.mockResolvedValue(undefined); deleteMyCommandsSpy.mockReset(); deleteMyCommandsSpy.mockResolvedValue(undefined); getMeSpy.mockReset(); getMeSpy.mockResolvedValue({ username: "openclaw_bot", has_topics_enabled: true, }); middlewareUseSpy.mockReset(); sequentializeSpy.mockReset(); botCtorSpy.mockReset(); sequentializeKey = undefined; });