import { beforeEach, describe, expect, it, vi } from "vitest"; let createTelegramBot: typeof import("./bot.js").createTelegramBot; let resetInboundDedupe: typeof import("../auto-reply/reply/inbound-dedupe.js").resetInboundDedupe; const { sessionStorePath } = vi.hoisted(() => ({ sessionStorePath: `/tmp/clawdbot-telegram-${Math.random().toString(16).slice(2)}.json`, })); const { loadWebMedia } = vi.hoisted(() => ({ loadWebMedia: vi.fn(), })); vi.mock("../web/media.js", () => ({ loadWebMedia, })); const { loadConfig } = vi.hoisted(() => ({ loadConfig: vi.fn(() => ({})), })); 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 { readTelegramAllowFromStore, upsertTelegramPairingRequest } = vi.hoisted(() => ({ readTelegramAllowFromStore: vi.fn(async () => [] as string[]), upsertTelegramPairingRequest: vi.fn(async () => ({ code: "PAIRCODE", created: true, })), })); vi.mock("./pairing-store.js", () => ({ readTelegramAllowFromStore, upsertTelegramPairingRequest, })); const useSpy = vi.fn(); const middlewareUseSpy = vi.fn(); const onSpy = vi.fn(); const stopSpy = vi.fn(); const commandSpy = vi.fn(); const botCtorSpy = vi.fn(); const answerCallbackQuerySpy = vi.fn(async () => undefined); const sendChatActionSpy = vi.fn(); const setMessageReactionSpy = vi.fn(async () => undefined); const setMyCommandsSpy = vi.fn(async () => undefined); const sendMessageSpy = vi.fn(async () => ({ message_id: 77 })); const sendAnimationSpy = vi.fn(async () => ({ message_id: 78 })); const sendPhotoSpy = 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; sendMessage: typeof sendMessageSpy; sendAnimation: typeof sendAnimationSpy; sendPhoto: typeof sendPhotoSpy; }; const apiStub: ApiStub = { config: { use: useSpy }, answerCallbackQuery: answerCallbackQuerySpy, sendChatAction: sendChatActionSpy, setMessageReaction: setMessageReactionSpy, setMyCommands: setMyCommandsSpy, sendMessage: sendMessageSpy, sendAnimation: sendAnimationSpy, sendPhoto: sendPhotoSpy, }; vi.mock("grammy", () => ({ Bot: class { api = apiStub; use = middlewareUseSpy; on = onSpy; stop = stopSpy; command = commandSpy; constructor( public token: string, public options?: { client?: { fetch?: typeof fetch } }, ) { botCtorSpy(token, options); } }, InputFile: class {}, webhookCallback: vi.fn(), })); const sequentializeMiddleware = vi.fn(); const sequentializeSpy = vi.fn(() => sequentializeMiddleware); let _sequentializeKey: ((ctx: unknown) => string) | undefined; vi.mock("@grammyjs/runner", () => ({ sequentialize: (keyFn: (ctx: unknown) => string) => { _sequentializeKey = keyFn; return sequentializeSpy(); }, })); const throttlerSpy = vi.fn(() => "throttler"); vi.mock("@grammyjs/transformer-throttler", () => ({ apiThrottler: () => throttlerSpy(), })); vi.mock("../auto-reply/reply.js", () => { const replySpy = vi.fn(async (_ctx, opts) => { await opts?.onReplyStart?.(); return undefined; }); return { getReplyFromConfig: replySpy, __replySpy: replySpy }; }); let replyModule: typeof import("../auto-reply/reply.js"); 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; }; describe("createTelegramBot", () => { beforeEach(async () => { vi.resetModules(); ({ resetInboundDedupe } = await import("../auto-reply/reply/inbound-dedupe.js")); ({ createTelegramBot } = await import("./bot.js")); replyModule = await import("../auto-reply/reply.js"); resetInboundDedupe(); loadConfig.mockReturnValue({ channels: { telegram: { dmPolicy: "open", allowFrom: ["*"] }, }, }); loadWebMedia.mockReset(); sendAnimationSpy.mockReset(); sendPhotoSpy.mockReset(); setMessageReactionSpy.mockReset(); answerCallbackQuerySpy.mockReset(); setMyCommandsSpy.mockReset(); middlewareUseSpy.mockReset(); sequentializeSpy.mockReset(); botCtorSpy.mockReset(); _sequentializeKey = undefined; }); // groupPolicy tests it("routes DMs by telegram accountId binding", async () => { onSpy.mockReset(); const replySpy = replyModule.__replySpy as unknown as ReturnType; replySpy.mockReset(); loadConfig.mockReturnValue({ channels: { telegram: { accounts: { opie: { botToken: "tok-opie", dmPolicy: "open", }, }, }, }, bindings: [ { agentId: "opie", match: { channel: "telegram", accountId: "opie" }, }, ], }); createTelegramBot({ token: "tok", accountId: "opie" }); const handler = getOnHandler("message") as (ctx: Record) => Promise; await handler({ message: { chat: { id: 123, type: "private" }, from: { id: 999, username: "testuser" }, text: "hello", date: 1736380800, message_id: 42, }, me: { username: "clawdbot_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); expect(replySpy).toHaveBeenCalledTimes(1); const payload = replySpy.mock.calls[0][0]; expect(payload.AccountId).toBe("opie"); expect(payload.SessionKey).toBe("agent:opie:main"); }); it("allows per-group requireMention override", async () => { onSpy.mockReset(); const replySpy = replyModule.__replySpy as unknown as ReturnType; replySpy.mockReset(); loadConfig.mockReturnValue({ channels: { telegram: { groupPolicy: "open", groups: { "*": { requireMention: true }, "123": { requireMention: false }, }, }, }, }); createTelegramBot({ token: "tok" }); const handler = getOnHandler("message") as (ctx: Record) => Promise; await handler({ message: { chat: { id: 123, type: "group", title: "Dev Chat" }, text: "hello", date: 1736380800, }, me: { username: "clawdbot_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); expect(replySpy).toHaveBeenCalledTimes(1); }); it("allows per-topic requireMention override", async () => { onSpy.mockReset(); const replySpy = replyModule.__replySpy as unknown as ReturnType; replySpy.mockReset(); loadConfig.mockReturnValue({ channels: { telegram: { groupPolicy: "open", groups: { "*": { requireMention: true }, "-1001234567890": { requireMention: true, topics: { "99": { requireMention: false }, }, }, }, }, }, }); createTelegramBot({ token: "tok" }); const handler = getOnHandler("message") as (ctx: Record) => Promise; await handler({ message: { chat: { id: -1001234567890, type: "supergroup", title: "Forum Group", is_forum: true, }, text: "hello", date: 1736380800, message_thread_id: 99, }, me: { username: "clawdbot_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); expect(replySpy).toHaveBeenCalledTimes(1); }); it("honors groups default when no explicit group override exists", async () => { onSpy.mockReset(); const replySpy = replyModule.__replySpy as unknown as ReturnType; replySpy.mockReset(); loadConfig.mockReturnValue({ channels: { telegram: { groupPolicy: "open", groups: { "*": { requireMention: false } }, }, }, }); createTelegramBot({ token: "tok" }); const handler = getOnHandler("message") as (ctx: Record) => Promise; await handler({ message: { chat: { id: 456, type: "group", title: "Ops" }, text: "hello", date: 1736380800, }, me: { username: "clawdbot_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); expect(replySpy).toHaveBeenCalledTimes(1); }); it("does not block group messages when bot username is unknown", async () => { onSpy.mockReset(); const replySpy = replyModule.__replySpy as unknown as ReturnType; replySpy.mockReset(); loadConfig.mockReturnValue({ channels: { telegram: { groupPolicy: "open", groups: { "*": { requireMention: true } }, }, }, }); createTelegramBot({ token: "tok" }); const handler = getOnHandler("message") as (ctx: Record) => Promise; await handler({ message: { chat: { id: 789, type: "group", title: "No Me" }, text: "hello", date: 1736380800, }, getFile: async () => ({ download: async () => new Uint8Array() }), }); expect(replySpy).toHaveBeenCalledTimes(1); }); it("sends GIF replies as animations", async () => { onSpy.mockReset(); const replySpy = replyModule.__replySpy as unknown as ReturnType; replySpy.mockReset(); replySpy.mockResolvedValueOnce({ text: "caption", mediaUrl: "https://example.com/fun", }); loadWebMedia.mockResolvedValueOnce({ buffer: Buffer.from("GIF89a"), contentType: "image/gif", fileName: "fun.gif", }); createTelegramBot({ token: "tok" }); const handler = getOnHandler("message") as (ctx: Record) => Promise; await handler({ message: { chat: { id: 1234, type: "private" }, text: "hello world", date: 1736380800, message_id: 5, from: { first_name: "Ada" }, }, me: { username: "clawdbot_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); expect(sendAnimationSpy).toHaveBeenCalledTimes(1); expect(sendAnimationSpy).toHaveBeenCalledWith("1234", expect.anything(), { caption: "caption", parse_mode: "HTML", reply_to_message_id: undefined, }); expect(sendPhotoSpy).not.toHaveBeenCalled(); }); });