import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { describe, expect, it } from "vitest"; import { getOnHandler, getLoadConfigMock, onSpy, replySpy, sendMessageSpy, } from "./bot.create-telegram-bot.test-harness.js"; import { createTelegramBot } from "./bot.js"; const loadConfig = getLoadConfigMock(); describe("createTelegramBot", () => { // groupPolicy tests it("sends replies without native reply threading", async () => { onSpy.mockReset(); sendMessageSpy.mockReset(); replySpy.mockReset(); replySpy.mockResolvedValue({ text: "a".repeat(4500) }); createTelegramBot({ token: "tok" }); const handler = getOnHandler("message") as (ctx: Record) => Promise; await handler({ message: { chat: { id: 5, type: "private" }, text: "hi", date: 1736380800, message_id: 101, }, me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); expect(sendMessageSpy.mock.calls.length).toBeGreaterThan(1); for (const call of sendMessageSpy.mock.calls) { expect(call[2]?.reply_to_message_id).toBeUndefined(); } }); it("honors replyToMode=first for threaded replies", async () => { onSpy.mockReset(); sendMessageSpy.mockReset(); replySpy.mockReset(); replySpy.mockResolvedValue({ text: "a".repeat(4500), replyToId: "101", }); createTelegramBot({ token: "tok", replyToMode: "first" }); const handler = getOnHandler("message") as (ctx: Record) => Promise; await handler({ message: { chat: { id: 5, type: "private" }, text: "hi", date: 1736380800, message_id: 101, }, me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); expect(sendMessageSpy.mock.calls.length).toBeGreaterThan(1); const [first, ...rest] = sendMessageSpy.mock.calls; expect(first?.[2]?.reply_to_message_id).toBe(101); for (const call of rest) { expect(call[2]?.reply_to_message_id).toBeUndefined(); } }); it("prefixes final replies with responsePrefix", async () => { onSpy.mockReset(); sendMessageSpy.mockReset(); replySpy.mockReset(); replySpy.mockResolvedValue({ text: "final reply" }); loadConfig.mockReturnValue({ channels: { telegram: { dmPolicy: "open", allowFrom: ["*"] }, }, messages: { responsePrefix: "PFX" }, }); createTelegramBot({ token: "tok" }); const handler = getOnHandler("message") as (ctx: Record) => Promise; await handler({ message: { chat: { id: 5, type: "private" }, text: "hi", date: 1736380800, }, me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); expect(sendMessageSpy).toHaveBeenCalledTimes(1); expect(sendMessageSpy.mock.calls[0][1]).toBe("PFX final reply"); }); it("honors replyToMode=all for threaded replies", async () => { onSpy.mockReset(); sendMessageSpy.mockReset(); replySpy.mockReset(); replySpy.mockResolvedValue({ text: "a".repeat(4500), replyToId: "101", }); createTelegramBot({ token: "tok", replyToMode: "all" }); const handler = getOnHandler("message") as (ctx: Record) => Promise; await handler({ message: { chat: { id: 5, type: "private" }, text: "hi", date: 1736380800, message_id: 101, }, me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); expect(sendMessageSpy.mock.calls.length).toBeGreaterThan(1); for (const call of sendMessageSpy.mock.calls) { expect(call[2]?.reply_to_message_id).toBe(101); } }); it("blocks group messages when telegram.groups is set without a wildcard", async () => { onSpy.mockReset(); replySpy.mockReset(); loadConfig.mockReturnValue({ channels: { telegram: { groups: { "123": { requireMention: false }, }, }, }, }); createTelegramBot({ token: "tok" }); const handler = getOnHandler("message") as (ctx: Record) => Promise; await handler({ message: { chat: { id: 456, type: "group", title: "Ops" }, text: "@openclaw_bot hello", date: 1736380800, }, me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); expect(replySpy).not.toHaveBeenCalled(); }); it("skips group messages without mention when requireMention is enabled", async () => { onSpy.mockReset(); replySpy.mockReset(); loadConfig.mockReturnValue({ channels: { telegram: { groups: { "*": { requireMention: true } } }, }, }); 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: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); expect(replySpy).not.toHaveBeenCalled(); }); it("honors routed group activation from session store", async () => { onSpy.mockReset(); replySpy.mockReset(); const storeDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-telegram-")); const storePath = path.join(storeDir, "sessions.json"); fs.writeFileSync( storePath, JSON.stringify({ "agent:ops:telegram:group:123": { groupActivation: "always" }, }), "utf-8", ); loadConfig.mockReturnValue({ channels: { telegram: { groupPolicy: "open", groups: { "*": { requireMention: true } }, }, }, bindings: [ { agentId: "ops", match: { channel: "telegram", peer: { kind: "group", id: "123" }, }, }, ], session: { store: storePath }, }); createTelegramBot({ token: "tok" }); const handler = getOnHandler("message") as (ctx: Record) => Promise; await handler({ message: { chat: { id: 123, type: "group", title: "Routing" }, text: "hello", date: 1736380800, }, me: { username: "openclaw_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }), }); expect(replySpy).toHaveBeenCalledTimes(1); }); });