| import { beforeEach, describe, expect, it, vi } from "vitest"; |
|
|
| import { monitorTelegramProvider } from "./monitor.js"; |
|
|
| type MockCtx = { |
| message: { |
| chat: { id: number; type: string; title?: string }; |
| text?: string; |
| caption?: string; |
| }; |
| me?: { username: string }; |
| getFile: () => Promise<unknown>; |
| }; |
|
|
| |
| const handlers: Record<string, (ctx: MockCtx) => Promise<void> | void> = {}; |
| const api = { |
| sendMessage: vi.fn(), |
| sendPhoto: vi.fn(), |
| sendVideo: vi.fn(), |
| sendAudio: vi.fn(), |
| sendDocument: vi.fn(), |
| setWebhook: vi.fn(), |
| deleteWebhook: vi.fn(), |
| }; |
| const { initSpy, runSpy, loadConfig } = vi.hoisted(() => ({ |
| initSpy: vi.fn(async () => undefined), |
| runSpy: vi.fn(() => ({ |
| task: () => Promise.resolve(), |
| stop: vi.fn(), |
| })), |
| loadConfig: vi.fn(() => ({ |
| agents: { defaults: { maxConcurrent: 2 } }, |
| channels: { telegram: {} }, |
| })), |
| })); |
|
|
| const { computeBackoff, sleepWithAbort } = vi.hoisted(() => ({ |
| computeBackoff: vi.fn(() => 0), |
| sleepWithAbort: vi.fn(async () => undefined), |
| })); |
|
|
| vi.mock("../config/config.js", async (importOriginal) => { |
| const actual = await importOriginal<typeof import("../config/config.js")>(); |
| return { |
| ...actual, |
| loadConfig, |
| }; |
| }); |
|
|
| vi.mock("./bot.js", () => ({ |
| createTelegramBot: () => { |
| handlers.message = async (ctx: MockCtx) => { |
| const chatId = ctx.message.chat.id; |
| const isGroup = ctx.message.chat.type !== "private"; |
| const text = ctx.message.text ?? ctx.message.caption ?? ""; |
| if (isGroup && !text.includes("@mybot")) { |
| return; |
| } |
| if (!text.trim()) { |
| return; |
| } |
| await api.sendMessage(chatId, `echo:${text}`, { parse_mode: "HTML" }); |
| }; |
| return { |
| on: vi.fn(), |
| api, |
| me: { username: "mybot" }, |
| init: initSpy, |
| stop: vi.fn(), |
| start: vi.fn(), |
| }; |
| }, |
| createTelegramWebhookCallback: vi.fn(), |
| })); |
|
|
| |
| vi.mock("@grammyjs/runner", () => ({ |
| run: runSpy, |
| })); |
|
|
| vi.mock("../infra/backoff.js", () => ({ |
| computeBackoff, |
| sleepWithAbort, |
| })); |
|
|
| vi.mock("../auto-reply/reply.js", () => ({ |
| getReplyFromConfig: async (ctx: { Body?: string }) => ({ |
| text: `echo:${ctx.Body}`, |
| }), |
| })); |
|
|
| describe("monitorTelegramProvider (grammY)", () => { |
| beforeEach(() => { |
| loadConfig.mockReturnValue({ |
| agents: { defaults: { maxConcurrent: 2 } }, |
| channels: { telegram: {} }, |
| }); |
| initSpy.mockClear(); |
| runSpy.mockClear(); |
| computeBackoff.mockClear(); |
| sleepWithAbort.mockClear(); |
| }); |
|
|
| it("processes a DM and sends reply", async () => { |
| Object.values(api).forEach((fn) => { |
| fn?.mockReset?.(); |
| }); |
| await monitorTelegramProvider({ token: "tok" }); |
| expect(handlers.message).toBeDefined(); |
| await handlers.message?.({ |
| message: { |
| message_id: 1, |
| chat: { id: 123, type: "private" }, |
| text: "hi", |
| }, |
| me: { username: "mybot" }, |
| getFile: vi.fn(async () => ({})), |
| }); |
| expect(api.sendMessage).toHaveBeenCalledWith(123, "echo:hi", { |
| parse_mode: "HTML", |
| }); |
| }); |
|
|
| it("uses agent maxConcurrent for runner concurrency", async () => { |
| runSpy.mockClear(); |
| loadConfig.mockReturnValue({ |
| agents: { defaults: { maxConcurrent: 3 } }, |
| channels: { telegram: {} }, |
| }); |
|
|
| await monitorTelegramProvider({ token: "tok" }); |
|
|
| expect(runSpy).toHaveBeenCalledWith( |
| expect.anything(), |
| expect.objectContaining({ |
| sink: { concurrency: 3 }, |
| runner: expect.objectContaining({ |
| silent: true, |
| maxRetryTime: 5 * 60 * 1000, |
| retryInterval: "exponential", |
| }), |
| }), |
| ); |
| }); |
|
|
| it("requires mention in groups by default", async () => { |
| Object.values(api).forEach((fn) => { |
| fn?.mockReset?.(); |
| }); |
| await monitorTelegramProvider({ token: "tok" }); |
| await handlers.message?.({ |
| message: { |
| message_id: 2, |
| chat: { id: -99, type: "supergroup", title: "G" }, |
| text: "hello all", |
| }, |
| me: { username: "mybot" }, |
| getFile: vi.fn(async () => ({})), |
| }); |
| expect(api.sendMessage).not.toHaveBeenCalled(); |
| }); |
|
|
| it("retries on recoverable network errors", async () => { |
| const networkError = Object.assign(new Error("timeout"), { code: "ETIMEDOUT" }); |
| runSpy |
| .mockImplementationOnce(() => ({ |
| task: () => Promise.reject(networkError), |
| stop: vi.fn(), |
| })) |
| .mockImplementationOnce(() => ({ |
| task: () => Promise.resolve(), |
| stop: vi.fn(), |
| })); |
|
|
| await monitorTelegramProvider({ token: "tok" }); |
|
|
| expect(computeBackoff).toHaveBeenCalled(); |
| expect(sleepWithAbort).toHaveBeenCalled(); |
| expect(runSpy).toHaveBeenCalledTimes(2); |
| }); |
|
|
| it("surfaces non-recoverable errors", async () => { |
| runSpy.mockImplementationOnce(() => ({ |
| task: () => Promise.reject(new Error("bad token")), |
| stop: vi.fn(), |
| })); |
|
|
| await expect(monitorTelegramProvider({ token: "tok" })).rejects.toThrow("bad token"); |
| }); |
| }); |
|
|