// Telegram tests cover bot.fetch abort plugin behavior. import { describe, expect, it, vi } from "vitest"; import { getTelegramNetworkErrorOrigin } from "./network-errors.js"; const { botCtorSpy, telegramBotDepsForTest, telegramBotRuntimeForTest } = await import("./bot.create-telegram-bot.test-harness.js"); const { createTelegramBotCore: createTelegramBotBase, setTelegramBotRuntimeForTest } = await import("./bot-core.js"); setTelegramBotRuntimeForTest( telegramBotRuntimeForTest as unknown as Parameters[0], ); const createTelegramBot = (opts: import("./bot.types.js").TelegramBotOptions) => createTelegramBotBase({ ...opts, telegramDeps: telegramBotDepsForTest, }); function createWrappedTelegramClientFetch( proxyFetch: typeof fetch, config?: import("openclaw/plugin-sdk/config-contracts").OpenClawConfig, ) { const shutdown = new AbortController(); botCtorSpy.mockClear(); createTelegramBot({ token: "tok", ...(config ? { config } : {}), fetchAbortSignal: shutdown.signal, proxyFetch, }); const clientFetch = (botCtorSpy.mock.calls.at(-1)?.[1] as { client?: { fetch?: unknown } }) ?.client?.fetch as (input: RequestInfo | URL, init?: RequestInit) => Promise; expect(clientFetch).toBeTypeOf("function"); return { clientFetch, shutdown }; } function createWrappedTelegramClientFetchWithTransport(params: { fetch: typeof fetch; forceFallback?: (reason: string) => boolean; }) { const shutdown = new AbortController(); botCtorSpy.mockClear(); createTelegramBot({ token: "tok", fetchAbortSignal: shutdown.signal, telegramTransport: { fetch: params.fetch, sourceFetch: params.fetch, close: async () => undefined, ...(params.forceFallback ? { forceFallback: params.forceFallback } : {}), }, }); const clientFetch = (botCtorSpy.mock.calls.at(-1)?.[1] as { client?: { fetch?: unknown } }) ?.client?.fetch as (input: RequestInfo | URL, init?: RequestInit) => Promise; expect(clientFetch).toBeTypeOf("function"); return { clientFetch, shutdown }; } describe("createTelegramBot fetch abort", () => { it("aborts wrapped client fetch when fetchAbortSignal aborts", async () => { const fetchSpy = vi.fn( (_input: RequestInfo | URL, init?: RequestInit) => new Promise((resolve) => { const signal = init?.signal as AbortSignal; signal.addEventListener("abort", () => resolve(signal), { once: true }); }), ); const { clientFetch, shutdown } = createWrappedTelegramClientFetch( fetchSpy as unknown as typeof fetch, ); const observedSignalPromise = clientFetch("https://example.test"); shutdown.abort(new Error("shutdown")); const observedSignal = (await observedSignalPromise) as AbortSignal; expect(observedSignal).toBeInstanceOf(AbortSignal); expect(observedSignal.aborted).toBe(true); }); it("tags wrapped Telegram fetch failures with the Bot API method", async () => { const fetchError = Object.assign(new TypeError("fetch failed"), { cause: Object.assign(new Error("connect timeout"), { code: "UND_ERR_CONNECT_TIMEOUT", }), }); const fetchSpy = vi.fn(async () => { throw fetchError; }); const { clientFetch } = createWrappedTelegramClientFetch(fetchSpy as unknown as typeof fetch); await expect(clientFetch("https://api.telegram.org/bot123456:ABC/getUpdates")).rejects.toBe( fetchError, ); expect(getTelegramNetworkErrorOrigin(fetchError)).toEqual({ method: "getupdates", url: "https://api.telegram.org/bot123456:ABC/getUpdates", }); }); it("aborts wrapped getUpdates fetch after the hard polling timeout", async () => { vi.useFakeTimers(); const fetchSpy = vi.fn( (_input: RequestInfo | URL, init?: RequestInit) => new Promise((resolve) => { const signal = init?.signal as AbortSignal; signal.addEventListener("abort", () => resolve(signal), { once: true }); }), ); const { clientFetch } = createWrappedTelegramClientFetch(fetchSpy as unknown as typeof fetch); const observedSignalPromise = clientFetch("https://api.telegram.org/bot123456:ABC/getUpdates"); await vi.advanceTimersByTimeAsync(45_000); const observedSignal = (await observedSignalPromise) as AbortSignal; expect(observedSignal).toBeInstanceOf(AbortSignal); expect(observedSignal.aborted).toBe(true); vi.useRealTimers(); }); it("uses the longer outbound text timeout for sendMessage", async () => { vi.useFakeTimers(); const fetchSpy = vi.fn( (_input: RequestInfo | URL, init?: RequestInit) => new Promise((resolve) => { const signal = init?.signal as AbortSignal; signal.addEventListener("abort", () => resolve(signal), { once: true }); }), ); const { clientFetch } = createWrappedTelegramClientFetch(fetchSpy as unknown as typeof fetch); const observedSignalPromise = clientFetch("https://api.telegram.org/bot123456:ABC/sendMessage"); await vi.advanceTimersByTimeAsync(60_000); const observedSignal = (await observedSignalPromise) as AbortSignal; expect(observedSignal).toBeInstanceOf(AbortSignal); expect(observedSignal.aborted).toBe(true); vi.useRealTimers(); }); it("lets configured timeoutSeconds extend outbound method guards", async () => { vi.useFakeTimers(); const fetchSpy = vi.fn( (_input: RequestInfo | URL, init?: RequestInit) => new Promise((resolve) => { const signal = init?.signal as AbortSignal; signal.addEventListener("abort", () => resolve(signal), { once: true }); }), ); const { clientFetch } = createWrappedTelegramClientFetch( fetchSpy as unknown as typeof fetch, { channels: { telegram: { timeoutSeconds: 90 } }, } as never, ); const observedSignalPromise = clientFetch( "https://api.telegram.org/bot123456:ABC/editMessageText", ); await vi.advanceTimersByTimeAsync(90_000); const observedSignal = (await observedSignalPromise) as AbortSignal; expect(observedSignal).toBeInstanceOf(AbortSignal); expect(observedSignal.aborted).toBe(true); vi.useRealTimers(); }); it("retries timed-out control calls once after forcing transport fallback", async () => { vi.useFakeTimers(); const forceFallback = vi.fn(() => true); const fetchSpy = vi .fn() .mockImplementationOnce( (_input: RequestInfo | URL, init?: RequestInit) => new Promise((_resolve, reject) => { const signal = init?.signal as AbortSignal; signal.addEventListener( "abort", () => reject(toLintErrorObject(signal.reason, "Non-Error rejection")), { once: true }, ); }), ) .mockResolvedValueOnce({ ok: true } as Response); const { clientFetch } = createWrappedTelegramClientFetchWithTransport({ fetch: fetchSpy as unknown as typeof fetch, forceFallback, }); const resultPromise = clientFetch("https://api.telegram.org/bot123456:ABC/deleteWebhook"); await vi.advanceTimersByTimeAsync(15_000); await expect(resultPromise).resolves.toEqual({ ok: true }); expect(forceFallback).toHaveBeenCalledWith("request-timeout"); expect(fetchSpy).toHaveBeenCalledTimes(2); vi.useRealTimers(); }); it.each(["deleteMyCommands", "setMyCommands"])( "retries timed-out command sync call %s once after forcing transport fallback", async (method) => { vi.useFakeTimers(); const forceFallback = vi.fn(() => true); const fetchSpy = vi .fn() .mockImplementationOnce( (_input: RequestInfo | URL, init?: RequestInit) => new Promise((_resolve, reject) => { const signal = init?.signal as AbortSignal; signal.addEventListener( "abort", () => reject(toLintErrorObject(signal.reason, "Non-Error rejection")), { once: true }, ); }), ) .mockResolvedValueOnce({ ok: true } as Response); const { clientFetch } = createWrappedTelegramClientFetchWithTransport({ fetch: fetchSpy as unknown as typeof fetch, forceFallback, }); const resultPromise = clientFetch(`https://api.telegram.org/bot123456:ABC/${method}`); await vi.advanceTimersByTimeAsync(15_000); await expect(resultPromise).resolves.toEqual({ ok: true }); expect(forceFallback).toHaveBeenCalledWith("request-timeout"); expect(fetchSpy).toHaveBeenCalledTimes(2); vi.useRealTimers(); }, ); it("retries timed-out sendChatAction once after forcing transport fallback", async () => { vi.useFakeTimers(); const forceFallback = vi.fn(() => true); const fetchSpy = vi .fn() .mockImplementationOnce( (_input: RequestInfo | URL, init?: RequestInit) => new Promise((_resolve, reject) => { const signal = init?.signal as AbortSignal; signal.addEventListener( "abort", () => reject(toLintErrorObject(signal.reason, "Non-Error rejection")), { once: true }, ); }), ) .mockResolvedValueOnce({ ok: true } as Response); const { clientFetch } = createWrappedTelegramClientFetchWithTransport({ fetch: fetchSpy as unknown as typeof fetch, forceFallback, }); const resultPromise = clientFetch("https://api.telegram.org/bot123456:ABC/sendChatAction"); await vi.advanceTimersByTimeAsync(60_000); await expect(resultPromise).resolves.toEqual({ ok: true }); expect(forceFallback).toHaveBeenCalledWith("request-timeout"); expect(fetchSpy).toHaveBeenCalledTimes(2); vi.useRealTimers(); }); it("retries Telegram 421 responses after forcing transport fallback", async () => { const forceFallback = vi.fn(() => true); const fetchSpy = vi .fn() .mockResolvedValueOnce(new Response("Misdirected Request", { status: 421 })) .mockResolvedValueOnce(new Response("{}", { status: 200 })); const { clientFetch } = createWrappedTelegramClientFetchWithTransport({ fetch: fetchSpy as typeof fetch, forceFallback, }); const result = await clientFetch("https://api.telegram.org/bot123456:ABC/sendMessage"); expect(result).toBeInstanceOf(Response); expect((result as Response).status).toBe(200); expect(forceFallback).toHaveBeenCalledWith("misdirected-request"); expect(fetchSpy).toHaveBeenCalledTimes(2); }); it("retries Telegram 421 fetch errors after forcing transport fallback", async () => { const forceFallback = vi.fn(() => true); const fetchSpy = vi .fn() .mockRejectedValueOnce(Object.assign(new Error("421 Misdirected Request"), { status: 421 })) .mockResolvedValueOnce(new Response("{}", { status: 200 })); const { clientFetch } = createWrappedTelegramClientFetchWithTransport({ fetch: fetchSpy as typeof fetch, forceFallback, }); const result = await clientFetch("https://api.telegram.org/bot123456:ABC/sendMessage"); expect(result).toBeInstanceOf(Response); expect((result as Response).status).toBe(200); expect(forceFallback).toHaveBeenCalledWith("misdirected-request"); expect(fetchSpy).toHaveBeenCalledTimes(2); }); it("preserves the original fetch error when tagging cannot attach metadata", async () => { const frozenError = Object.freeze( Object.assign(new TypeError("fetch failed"), { cause: Object.assign(new Error("connect timeout"), { code: "UND_ERR_CONNECT_TIMEOUT", }), }), ); const fetchSpy = vi.fn(async () => { throw toLintErrorObject(frozenError, "Non-Error thrown"); }); const { clientFetch } = createWrappedTelegramClientFetch(fetchSpy as unknown as typeof fetch); await expect(clientFetch("https://api.telegram.org/bot123456:ABC/getUpdates")).rejects.toBe( frozenError, ); expect(getTelegramNetworkErrorOrigin(frozenError)).toBeNull(); }); }); function toLintErrorObject(value: unknown, fallbackMessage: string): Error { if (value instanceof Error) { return value; } if (typeof value === "string") { return new Error(value); } const error = new Error(fallbackMessage, { cause: value }); if ((typeof value === "object" && value !== null) || typeof value === "function") { Object.assign(error, value); } return error; }