| 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<unknown>>; |
|
|
| 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<typeof import("../config/config.js")>(); |
| return { |
| ...actual, |
| loadConfig, |
| }; |
| }); |
|
|
| vi.mock("../config/sessions.js", async (importOriginal) => { |
| const actual = await importOriginal<typeof import("../config/sessions.js")>(); |
| 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<ReplyPayload | ReplyPayload[] | undefined> |
| > = 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<string, unknown>) => Promise<void>; |
| }; |
|
|
| 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; |
| }); |
|
|