import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { TelegramBotDeps } from "./bot-deps.js"; const buildTelegramMessageContext = vi.hoisted(() => vi.fn()); const dispatchTelegramMessage = vi.hoisted(() => vi.fn()); const telegramInboundInfo = vi.hoisted(() => vi.fn()); const upsertChannelPairingRequest = vi.hoisted(() => vi.fn(async () => ({ code: "PAIRCODE", created: true })), ); vi.mock("openclaw/plugin-sdk/runtime-env", () => ({ createSubsystemLogger: () => ({ child: () => ({ info: telegramInboundInfo, }), }), danger: (message: string) => message, logVerbose: vi.fn(), shouldLogVerbose: () => false, })); vi.mock("./bot-message-context.js", () => ({ buildTelegramMessageContext, })); vi.mock("./bot-message-dispatch.js", () => ({ dispatchTelegramMessage, })); let createTelegramMessageProcessor: typeof import("./bot-message.js").createTelegramMessageProcessor; let formatTelegramInboundLogLine: typeof import("./bot-message.js").formatTelegramInboundLogLine; describe("telegram bot message processor", () => { beforeAll(async () => { ({ createTelegramMessageProcessor, formatTelegramInboundLogLine } = await import("./bot-message.js")); }); beforeEach(() => { buildTelegramMessageContext.mockClear(); dispatchTelegramMessage.mockClear(); telegramInboundInfo.mockClear(); upsertChannelPairingRequest.mockClear(); }); const telegramDepsForTest = { upsertChannelPairingRequest, } as unknown as TelegramBotDeps; const baseDeps = { bot: {}, cfg: {}, account: {}, telegramCfg: {}, historyLimit: 0, groupHistories: {}, dmPolicy: {}, allowFrom: [], groupAllowFrom: [], ackReactionScope: "none", logger: {}, resolveGroupActivation: () => true, resolveGroupRequireMention: () => false, resolveTelegramGroupConfig: () => ({}), runtime: {}, replyToMode: "auto", streamMode: "partial", textLimit: 4096, telegramDeps: telegramDepsForTest, opts: {}, } as unknown as Parameters[0]; async function processSampleMessage( processMessage: ReturnType, ) { await processMessage( { message: { chat: { id: 123, type: "private", title: "chat" }, message_id: 456, }, } as unknown as Parameters[0], [], [], {}, ); } function createDispatchFailureHarness( context: Record, sendMessage: ReturnType, ) { const runtimeError = vi.fn(); buildTelegramMessageContext.mockResolvedValue(createMessageContext(context)); dispatchTelegramMessage.mockRejectedValue(new Error("dispatch exploded")); const processMessage = createTelegramMessageProcessor({ ...baseDeps, bot: { api: { sendMessage } }, runtime: { error: runtimeError }, } as unknown as Parameters[0]); return { processMessage, runtimeError }; } function createMessageContext(context: Record = {}) { return { chatId: 123, ctxPayload: { From: "telegram:123", To: "telegram:123", ChatType: "direct", RawBody: "hello there", }, primaryCtx: { me: { username: "openclaw_bot" } }, route: { sessionKey: "agent:main:main" }, sendTyping: vi.fn().mockResolvedValue(undefined), ...context, }; } it("dispatches when context is available", async () => { const sendTyping = vi.fn().mockResolvedValue(undefined); buildTelegramMessageContext.mockResolvedValue( createMessageContext({ sendTyping, }), ); const processMessage = createTelegramMessageProcessor(baseDeps); await processSampleMessage(processMessage); expect(sendTyping).toHaveBeenCalledTimes(1); expect(dispatchTelegramMessage).toHaveBeenCalledTimes(1); expect(sendTyping.mock.invocationCallOrder[0]).toBeLessThan( dispatchTelegramMessage.mock.invocationCallOrder[0], ); expect(telegramInboundInfo).toHaveBeenCalledWith( "Inbound message telegram:123 -> @openclaw_bot (direct, 11 chars)", ); }); it("skips dispatch when no context is produced", async () => { buildTelegramMessageContext.mockResolvedValue(null); const processMessage = createTelegramMessageProcessor(baseDeps); await processSampleMessage(processMessage); expect(dispatchTelegramMessage).not.toHaveBeenCalled(); expect(telegramInboundInfo).not.toHaveBeenCalled(); }); it("formats Telegram inbound summaries without message content", () => { expect( formatTelegramInboundLogLine({ from: "telegram:123", to: "@openclaw_bot", chatType: "direct", body: "secret message", }), ).toBe("Inbound message telegram:123 -> @openclaw_bot (direct, 14 chars)"); expect( formatTelegramInboundLogLine({ from: "telegram:group:-100", to: "@openclaw_bot", chatType: "group", body: "", mediaType: "image/jpeg", }), ).toBe("Inbound message telegram:group:-100 -> @openclaw_bot (group, image/jpeg, 13 chars)"); }); it("keeps dispatch running when the early typing cue fails", async () => { const sendTyping = vi.fn().mockRejectedValue(new Error("typing failed")); buildTelegramMessageContext.mockResolvedValue( createMessageContext({ sendTyping, }), ); const processMessage = createTelegramMessageProcessor(baseDeps); await processSampleMessage(processMessage); expect(sendTyping).toHaveBeenCalledTimes(1); expect(dispatchTelegramMessage).toHaveBeenCalledTimes(1); }); it("sends user-visible fallback when dispatch throws", async () => { const sendMessage = vi.fn().mockResolvedValue(undefined); const { processMessage, runtimeError } = createDispatchFailureHarness( { chatId: 123, threadSpec: { id: 456, scope: "forum" }, route: { sessionKey: "agent:main:main" }, }, sendMessage, ); await expect(processSampleMessage(processMessage)).resolves.toBeUndefined(); expect(sendMessage).toHaveBeenCalledWith( 123, "Something went wrong while processing your request. Please try again.", { message_thread_id: 456 }, ); expect(runtimeError).toHaveBeenCalledWith(expect.stringContaining("dispatch exploded")); }); it("omits message_thread_id for General-topic fallback replies", async () => { const sendMessage = vi.fn().mockResolvedValue(undefined); const { processMessage } = createDispatchFailureHarness( { chatId: 123, threadSpec: { id: 1, scope: "forum" }, route: { sessionKey: "agent:main:main" }, }, sendMessage, ); await expect(processSampleMessage(processMessage)).resolves.toBeUndefined(); expect(sendMessage).toHaveBeenCalledWith( 123, "Something went wrong while processing your request. Please try again.", undefined, ); }); it("swallows fallback delivery failures after dispatch throws", async () => { const sendMessage = vi.fn().mockRejectedValue(new Error("blocked by user")); const { processMessage, runtimeError } = createDispatchFailureHarness( { chatId: 123, route: { sessionKey: "agent:main:main" }, }, sendMessage, ); await expect(processSampleMessage(processMessage)).resolves.toBeUndefined(); expect(sendMessage).toHaveBeenCalledWith( 123, "Something went wrong while processing your request. Please try again.", undefined, ); expect(runtimeError).toHaveBeenCalledWith(expect.stringContaining("dispatch exploded")); }); });