diff --git a/src/telegram/bot.media.downloads-media-file-path-no-file-download.test.ts b/src/telegram/bot.media.downloads-media-file-path-no-file-download.test.ts index 959b18bb731..2c02d69d33f 100644 --- a/src/telegram/bot.media.downloads-media-file-path-no-file-download.test.ts +++ b/src/telegram/bot.media.downloads-media-file-path-no-file-download.test.ts @@ -1,111 +1,12 @@ -import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import * as ssrf from "../infra/net/ssrf.js"; -import { onSpy, saveMediaBufferSpy, sendChatActionSpy } from "./bot.media.e2e-harness.js"; - -const cacheStickerSpy = vi.fn(); -const getCachedStickerSpy = vi.fn(); -const describeStickerImageSpy = vi.fn(); -const resolvePinnedHostname = ssrf.resolvePinnedHostname; -const lookupMock = vi.fn(); -let resolvePinnedHostnameSpy: ReturnType = null; -const TELEGRAM_TEST_TIMINGS = { - mediaGroupFlushMs: 20, - textFragmentGapMs: 30, -} as const; -const TELEGRAM_BOT_IMPORT_TIMEOUT_MS = process.platform === "win32" ? 180_000 : 150_000; -let createTelegramBot: typeof import("./bot.js").createTelegramBot; -let replySpy: ReturnType; - -async function createBotHandler(): Promise<{ - handler: (ctx: Record) => Promise; - replySpy: ReturnType; - runtimeError: ReturnType; -}> { - return createBotHandlerWithOptions({}); -} - -async function createBotHandlerWithOptions(options: { - proxyFetch?: typeof fetch; - runtimeLog?: ReturnType; - runtimeError?: ReturnType; -}): Promise<{ - handler: (ctx: Record) => Promise; - replySpy: ReturnType; - runtimeError: ReturnType; -}> { - onSpy.mockClear(); - replySpy.mockClear(); - sendChatActionSpy.mockClear(); - - const runtimeError = options.runtimeError ?? vi.fn(); - const runtimeLog = options.runtimeLog ?? vi.fn(); - createTelegramBot({ - token: "tok", - testTimings: TELEGRAM_TEST_TIMINGS, - ...(options.proxyFetch ? { proxyFetch: options.proxyFetch } : {}), - runtime: { - log: runtimeLog as (...data: unknown[]) => void, - error: runtimeError as (...data: unknown[]) => void, - exit: () => { - throw new Error("exit"); - }, - }, - }); - const handler = onSpy.mock.calls.find((call) => call[0] === "message")?.[1] as ( - ctx: Record, - ) => Promise; - expect(handler).toBeDefined(); - return { handler, replySpy, runtimeError }; -} - -function mockTelegramFileDownload(params: { - contentType: string; - bytes: Uint8Array; -}): ReturnType { - return vi.spyOn(globalThis, "fetch").mockResolvedValueOnce({ - ok: true, - status: 200, - statusText: "OK", - headers: { get: () => params.contentType }, - arrayBuffer: async () => params.bytes.buffer, - } as unknown as Response); -} - -function mockTelegramPngDownload(): ReturnType { - return vi.spyOn(globalThis, "fetch").mockResolvedValue({ - ok: true, - status: 200, - statusText: "OK", - headers: { get: () => "image/png" }, - arrayBuffer: async () => new Uint8Array([0x89, 0x50, 0x4e, 0x47]).buffer, - } as unknown as Response); -} - -beforeEach(() => { - vi.useRealTimers(); - lookupMock.mockResolvedValue([{ address: "93.184.216.34", family: 4 }]); - resolvePinnedHostnameSpy = vi - .spyOn(ssrf, "resolvePinnedHostname") - .mockImplementation((hostname) => resolvePinnedHostname(hostname, lookupMock)); -}); - -afterEach(() => { - lookupMock.mockClear(); - resolvePinnedHostnameSpy?.mockRestore(); - resolvePinnedHostnameSpy = null; -}); - -beforeAll(async () => { - ({ createTelegramBot } = await import("./bot.js")); - const replyModule = await import("../auto-reply/reply.js"); - replySpy = (replyModule as unknown as { __replySpy: ReturnType }).__replySpy; -}, TELEGRAM_BOT_IMPORT_TIMEOUT_MS); - -vi.mock("./sticker-cache.js", () => ({ - cacheSticker: (...args: unknown[]) => cacheStickerSpy(...args), - getCachedSticker: (...args: unknown[]) => getCachedStickerSpy(...args), - describeStickerImage: (...args: unknown[]) => describeStickerImageSpy(...args), -})); +import { afterEach, describe, expect, it, vi } from "vitest"; +import { setNextSavedMediaPath } from "./bot.media.e2e-harness.js"; +import { + TELEGRAM_TEST_TIMINGS, + createBotHandler, + createBotHandlerWithOptions, + mockTelegramFileDownload, + mockTelegramPngDownload, +} from "./bot.media.test-utils.js"; describe("telegram inbound media", () => { // Parallel vitest shards can make this suite slower than the standalone run. @@ -194,8 +95,7 @@ describe("telegram inbound media", () => { bytes: new Uint8Array([0xff, 0xd8, 0xff, 0x00]), }); const inboundPath = "/tmp/media/inbound/file_1095---f00a04a2-99a0-4d98-99b0-dfe61c5a4198.jpg"; - saveMediaBufferSpy.mockResolvedValueOnce({ - id: "media", + setNextSavedMediaPath({ path: inboundPath, size: 4, contentType: "image/jpeg", @@ -483,245 +383,3 @@ describe("telegram forwarded bursts", () => { FORWARD_BURST_TEST_TIMEOUT_MS, ); }); - -describe("telegram stickers", () => { - const STICKER_TEST_TIMEOUT_MS = process.platform === "win32" ? 30_000 : 20_000; - - beforeEach(() => { - cacheStickerSpy.mockClear(); - getCachedStickerSpy.mockClear(); - describeStickerImageSpy.mockClear(); - // Re-seed defaults so per-test overrides do not leak when using mockClear. - getCachedStickerSpy.mockReturnValue(undefined); - describeStickerImageSpy.mockReturnValue(undefined); - }); - - it( - "downloads static sticker (WEBP) and includes sticker metadata", - async () => { - const { handler, replySpy, runtimeError } = await createBotHandler(); - const fetchSpy = mockTelegramFileDownload({ - contentType: "image/webp", - bytes: new Uint8Array([0x52, 0x49, 0x46, 0x46]), // RIFF header - }); - - await handler({ - message: { - message_id: 100, - chat: { id: 1234, type: "private" }, - sticker: { - file_id: "sticker_file_id_123", - file_unique_id: "sticker_unique_123", - type: "regular", - width: 512, - height: 512, - is_animated: false, - is_video: false, - emoji: "🎉", - set_name: "TestStickerPack", - }, - date: 1736380800, - }, - me: { username: "openclaw_bot" }, - getFile: async () => ({ file_path: "stickers/sticker.webp" }), - }); - - expect(runtimeError).not.toHaveBeenCalled(); - expect(fetchSpy).toHaveBeenCalledWith( - "https://api.telegram.org/file/bottok/stickers/sticker.webp", - expect.objectContaining({ redirect: "manual" }), - ); - expect(replySpy).toHaveBeenCalledTimes(1); - const payload = replySpy.mock.calls[0][0]; - expect(payload.Body).toContain(""); - expect(payload.Sticker?.emoji).toBe("🎉"); - expect(payload.Sticker?.setName).toBe("TestStickerPack"); - expect(payload.Sticker?.fileId).toBe("sticker_file_id_123"); - - fetchSpy.mockRestore(); - }, - STICKER_TEST_TIMEOUT_MS, - ); - - it( - "refreshes cached sticker metadata on cache hit", - async () => { - const { handler, replySpy, runtimeError } = await createBotHandler(); - - getCachedStickerSpy.mockReturnValue({ - fileId: "old_file_id", - fileUniqueId: "sticker_unique_456", - emoji: "😴", - setName: "OldSet", - description: "Cached description", - cachedAt: "2026-01-20T10:00:00.000Z", - }); - - const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValueOnce({ - ok: true, - status: 200, - statusText: "OK", - headers: { get: () => "image/webp" }, - arrayBuffer: async () => new Uint8Array([0x52, 0x49, 0x46, 0x46]).buffer, - } as unknown as Response); - - await handler({ - message: { - message_id: 103, - chat: { id: 1234, type: "private" }, - sticker: { - file_id: "new_file_id", - file_unique_id: "sticker_unique_456", - type: "regular", - width: 512, - height: 512, - is_animated: false, - is_video: false, - emoji: "🔥", - set_name: "NewSet", - }, - date: 1736380800, - }, - me: { username: "openclaw_bot" }, - getFile: async () => ({ file_path: "stickers/sticker.webp" }), - }); - - expect(runtimeError).not.toHaveBeenCalled(); - expect(cacheStickerSpy).toHaveBeenCalledWith( - expect.objectContaining({ - fileId: "new_file_id", - emoji: "🔥", - setName: "NewSet", - }), - ); - const payload = replySpy.mock.calls[0][0]; - expect(payload.Sticker?.fileId).toBe("new_file_id"); - expect(payload.Sticker?.cachedDescription).toBe("Cached description"); - - fetchSpy.mockRestore(); - }, - STICKER_TEST_TIMEOUT_MS, - ); - - it( - "skips animated and video sticker formats that cannot be downloaded", - async () => { - const { handler, replySpy, runtimeError } = await createBotHandler(); - - for (const scenario of [ - { - messageId: 101, - filePath: "stickers/animated.tgs", - sticker: { - file_id: "animated_sticker_id", - file_unique_id: "animated_unique", - type: "regular", - width: 512, - height: 512, - is_animated: true, - is_video: false, - emoji: "😎", - set_name: "AnimatedPack", - }, - }, - { - messageId: 102, - filePath: "stickers/video.webm", - sticker: { - file_id: "video_sticker_id", - file_unique_id: "video_unique", - type: "regular", - width: 512, - height: 512, - is_animated: false, - is_video: true, - emoji: "🎬", - set_name: "VideoPack", - }, - }, - ]) { - replySpy.mockClear(); - runtimeError.mockClear(); - const fetchSpy = vi.spyOn(globalThis, "fetch"); - - await handler({ - message: { - message_id: scenario.messageId, - chat: { id: 1234, type: "private" }, - sticker: scenario.sticker, - date: 1736380800, - }, - me: { username: "openclaw_bot" }, - getFile: async () => ({ file_path: scenario.filePath }), - }); - - expect(fetchSpy).not.toHaveBeenCalled(); - expect(replySpy).not.toHaveBeenCalled(); - expect(runtimeError).not.toHaveBeenCalled(); - fetchSpy.mockRestore(); - } - }, - STICKER_TEST_TIMEOUT_MS, - ); -}); - -describe("telegram text fragments", () => { - afterEach(() => { - vi.clearAllTimers(); - }); - - const TEXT_FRAGMENT_TEST_TIMEOUT_MS = process.platform === "win32" ? 45_000 : 20_000; - const TEXT_FRAGMENT_FLUSH_MS = TELEGRAM_TEST_TIMINGS.textFragmentGapMs + 80; - - it( - "buffers near-limit text and processes sequential parts as one message", - async () => { - onSpy.mockClear(); - replySpy.mockClear(); - vi.useFakeTimers(); - try { - createTelegramBot({ token: "tok", testTimings: TELEGRAM_TEST_TIMINGS }); - const handler = onSpy.mock.calls.find((call) => call[0] === "message")?.[1] as ( - ctx: Record, - ) => Promise; - expect(handler).toBeDefined(); - - const part1 = "A".repeat(4050); - const part2 = "B".repeat(50); - - await handler({ - message: { - chat: { id: 42, type: "private" }, - message_id: 10, - date: 1736380800, - text: part1, - }, - me: { username: "openclaw_bot" }, - getFile: async () => ({}), - }); - - await handler({ - message: { - chat: { id: 42, type: "private" }, - message_id: 11, - date: 1736380801, - text: part2, - }, - me: { username: "openclaw_bot" }, - getFile: async () => ({}), - }); - - expect(replySpy).not.toHaveBeenCalled(); - await vi.advanceTimersByTimeAsync(TEXT_FRAGMENT_FLUSH_MS * 2); - expect(replySpy).toHaveBeenCalledTimes(1); - - const payload = replySpy.mock.calls[0][0] as { RawBody?: string; Body?: string }; - expect(payload.RawBody).toContain(part1.slice(0, 32)); - expect(payload.RawBody).toContain(part2.slice(0, 32)); - } finally { - vi.useRealTimers(); - } - }, - TEXT_FRAGMENT_TEST_TIMEOUT_MS, - ); -}); diff --git a/src/telegram/bot.media.e2e-harness.ts b/src/telegram/bot.media.e2e-harness.ts index 191f92744d2..fec64cbdbf0 100644 --- a/src/telegram/bot.media.e2e-harness.ts +++ b/src/telegram/bot.media.e2e-harness.ts @@ -6,12 +6,38 @@ export const middlewareUseSpy: Mock = vi.fn(); export const onSpy: Mock = vi.fn(); export const stopSpy: Mock = vi.fn(); export const sendChatActionSpy: Mock = vi.fn(); -export const saveMediaBufferSpy: Mock = vi.fn(async (buffer: Buffer, contentType?: string) => ({ - id: "media", - path: "/tmp/telegram-media", - size: buffer.byteLength, - contentType: contentType ?? "application/octet-stream", -})); + +async function defaultSaveMediaBuffer(buffer: Buffer, contentType?: string) { + return { + id: "media", + path: "/tmp/telegram-media", + size: buffer.byteLength, + contentType: contentType ?? "application/octet-stream", + }; +} + +const saveMediaBufferSpy: Mock = vi.fn(defaultSaveMediaBuffer); + +export function setNextSavedMediaPath(params: { + path: string; + id?: string; + contentType?: string; + size?: number; +}) { + saveMediaBufferSpy.mockImplementationOnce( + async (buffer: Buffer, detectedContentType?: string) => ({ + id: params.id ?? "media", + path: params.path, + size: params.size ?? buffer.byteLength, + contentType: params.contentType ?? detectedContentType ?? "application/octet-stream", + }), + ); +} + +export function resetSaveMediaBufferMock() { + saveMediaBufferSpy.mockReset(); + saveMediaBufferSpy.mockImplementation(defaultSaveMediaBuffer); +} type ApiStub = { config: { use: (arg: unknown) => void }; @@ -29,6 +55,7 @@ const apiStub: ApiStub = { beforeEach(() => { resetInboundDedupe(); + resetSaveMediaBufferMock(); }); vi.mock("grammy", () => ({ diff --git a/src/telegram/bot.media.stickers-and-fragments.test.ts b/src/telegram/bot.media.stickers-and-fragments.test.ts new file mode 100644 index 00000000000..fc1b372f778 --- /dev/null +++ b/src/telegram/bot.media.stickers-and-fragments.test.ts @@ -0,0 +1,245 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { + TELEGRAM_TEST_TIMINGS, + cacheStickerSpy, + createBotHandler, + createBotHandlerWithOptions, + describeStickerImageSpy, + getCachedStickerSpy, + mockTelegramFileDownload, +} from "./bot.media.test-utils.js"; + +describe("telegram stickers", () => { + const STICKER_TEST_TIMEOUT_MS = process.platform === "win32" ? 30_000 : 20_000; + + beforeEach(() => { + cacheStickerSpy.mockClear(); + getCachedStickerSpy.mockClear(); + describeStickerImageSpy.mockClear(); + // Re-seed defaults so per-test overrides do not leak when using mockClear. + getCachedStickerSpy.mockReturnValue(undefined); + describeStickerImageSpy.mockReturnValue(undefined); + }); + + it( + "downloads static sticker (WEBP) and includes sticker metadata", + async () => { + const { handler, replySpy, runtimeError } = await createBotHandler(); + const fetchSpy = mockTelegramFileDownload({ + contentType: "image/webp", + bytes: new Uint8Array([0x52, 0x49, 0x46, 0x46]), // RIFF header + }); + + await handler({ + message: { + message_id: 100, + chat: { id: 1234, type: "private" }, + sticker: { + file_id: "sticker_file_id_123", + file_unique_id: "sticker_unique_123", + type: "regular", + width: 512, + height: 512, + is_animated: false, + is_video: false, + emoji: "🎉", + set_name: "TestStickerPack", + }, + date: 1736380800, + }, + me: { username: "openclaw_bot" }, + getFile: async () => ({ file_path: "stickers/sticker.webp" }), + }); + + expect(runtimeError).not.toHaveBeenCalled(); + expect(fetchSpy).toHaveBeenCalledWith( + "https://api.telegram.org/file/bottok/stickers/sticker.webp", + expect.objectContaining({ redirect: "manual" }), + ); + expect(replySpy).toHaveBeenCalledTimes(1); + const payload = replySpy.mock.calls[0][0]; + expect(payload.Body).toContain(""); + expect(payload.Sticker?.emoji).toBe("🎉"); + expect(payload.Sticker?.setName).toBe("TestStickerPack"); + expect(payload.Sticker?.fileId).toBe("sticker_file_id_123"); + + fetchSpy.mockRestore(); + }, + STICKER_TEST_TIMEOUT_MS, + ); + + it( + "refreshes cached sticker metadata on cache hit", + async () => { + const { handler, replySpy, runtimeError } = await createBotHandler(); + + getCachedStickerSpy.mockReturnValue({ + fileId: "old_file_id", + fileUniqueId: "sticker_unique_456", + emoji: "😴", + setName: "OldSet", + description: "Cached description", + cachedAt: "2026-01-20T10:00:00.000Z", + }); + + const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValueOnce({ + ok: true, + status: 200, + statusText: "OK", + headers: { get: () => "image/webp" }, + arrayBuffer: async () => new Uint8Array([0x52, 0x49, 0x46, 0x46]).buffer, + } as unknown as Response); + + await handler({ + message: { + message_id: 103, + chat: { id: 1234, type: "private" }, + sticker: { + file_id: "new_file_id", + file_unique_id: "sticker_unique_456", + type: "regular", + width: 512, + height: 512, + is_animated: false, + is_video: false, + emoji: "🔥", + set_name: "NewSet", + }, + date: 1736380800, + }, + me: { username: "openclaw_bot" }, + getFile: async () => ({ file_path: "stickers/sticker.webp" }), + }); + + expect(runtimeError).not.toHaveBeenCalled(); + expect(cacheStickerSpy).toHaveBeenCalledWith( + expect.objectContaining({ + fileId: "new_file_id", + emoji: "🔥", + setName: "NewSet", + }), + ); + const payload = replySpy.mock.calls[0][0]; + expect(payload.Sticker?.fileId).toBe("new_file_id"); + expect(payload.Sticker?.cachedDescription).toBe("Cached description"); + + fetchSpy.mockRestore(); + }, + STICKER_TEST_TIMEOUT_MS, + ); + + it( + "skips animated and video sticker formats that cannot be downloaded", + async () => { + const { handler, replySpy, runtimeError } = await createBotHandler(); + + for (const scenario of [ + { + messageId: 101, + filePath: "stickers/animated.tgs", + sticker: { + file_id: "animated_sticker_id", + file_unique_id: "animated_unique", + type: "regular", + width: 512, + height: 512, + is_animated: true, + is_video: false, + emoji: "😎", + set_name: "AnimatedPack", + }, + }, + { + messageId: 102, + filePath: "stickers/video.webm", + sticker: { + file_id: "video_sticker_id", + file_unique_id: "video_unique", + type: "regular", + width: 512, + height: 512, + is_animated: false, + is_video: true, + emoji: "🎬", + set_name: "VideoPack", + }, + }, + ]) { + replySpy.mockClear(); + runtimeError.mockClear(); + const fetchSpy = vi.spyOn(globalThis, "fetch"); + + await handler({ + message: { + message_id: scenario.messageId, + chat: { id: 1234, type: "private" }, + sticker: scenario.sticker, + date: 1736380800, + }, + me: { username: "openclaw_bot" }, + getFile: async () => ({ file_path: scenario.filePath }), + }); + + expect(fetchSpy).not.toHaveBeenCalled(); + expect(replySpy).not.toHaveBeenCalled(); + expect(runtimeError).not.toHaveBeenCalled(); + fetchSpy.mockRestore(); + } + }, + STICKER_TEST_TIMEOUT_MS, + ); +}); + +describe("telegram text fragments", () => { + afterEach(() => { + vi.clearAllTimers(); + }); + + const TEXT_FRAGMENT_TEST_TIMEOUT_MS = process.platform === "win32" ? 45_000 : 20_000; + const TEXT_FRAGMENT_FLUSH_MS = TELEGRAM_TEST_TIMINGS.textFragmentGapMs + 80; + + it( + "buffers near-limit text and processes sequential parts as one message", + async () => { + const { handler, replySpy } = await createBotHandlerWithOptions({}); + vi.useFakeTimers(); + try { + const part1 = "A".repeat(4050); + const part2 = "B".repeat(50); + + await handler({ + message: { + chat: { id: 42, type: "private" }, + message_id: 10, + date: 1736380800, + text: part1, + }, + me: { username: "openclaw_bot" }, + getFile: async () => ({}), + }); + + await handler({ + message: { + chat: { id: 42, type: "private" }, + message_id: 11, + date: 1736380801, + text: part2, + }, + me: { username: "openclaw_bot" }, + getFile: async () => ({}), + }); + + expect(replySpy).not.toHaveBeenCalled(); + await vi.advanceTimersByTimeAsync(TEXT_FRAGMENT_FLUSH_MS * 2); + expect(replySpy).toHaveBeenCalledTimes(1); + + const payload = replySpy.mock.calls[0][0] as { RawBody?: string }; + expect(payload.RawBody).toContain(part1.slice(0, 32)); + expect(payload.RawBody).toContain(part2.slice(0, 32)); + } finally { + vi.useRealTimers(); + } + }, + TEXT_FRAGMENT_TEST_TIMEOUT_MS, + ); +}); diff --git a/src/telegram/bot.media.test-utils.ts b/src/telegram/bot.media.test-utils.ts new file mode 100644 index 00000000000..4d49eda3f60 --- /dev/null +++ b/src/telegram/bot.media.test-utils.ts @@ -0,0 +1,112 @@ +import { afterEach, beforeAll, beforeEach, expect, vi } from "vitest"; +import * as ssrf from "../infra/net/ssrf.js"; +import { onSpy, sendChatActionSpy } from "./bot.media.e2e-harness.js"; + +export const cacheStickerSpy = vi.fn(); +export const getCachedStickerSpy = vi.fn(); +export const describeStickerImageSpy = vi.fn(); + +const resolvePinnedHostname = ssrf.resolvePinnedHostname; +const lookupMock = vi.fn(); +let resolvePinnedHostnameSpy: ReturnType = null; + +export const TELEGRAM_TEST_TIMINGS = { + mediaGroupFlushMs: 20, + textFragmentGapMs: 30, +} as const; + +const TELEGRAM_BOT_IMPORT_TIMEOUT_MS = process.platform === "win32" ? 180_000 : 150_000; + +let createTelegramBotRef: typeof import("./bot.js").createTelegramBot; +let replySpyRef: ReturnType; + +export async function createBotHandler(): Promise<{ + handler: (ctx: Record) => Promise; + replySpy: ReturnType; + runtimeError: ReturnType; +}> { + return createBotHandlerWithOptions({}); +} + +export async function createBotHandlerWithOptions(options: { + proxyFetch?: typeof fetch; + runtimeLog?: ReturnType; + runtimeError?: ReturnType; +}): Promise<{ + handler: (ctx: Record) => Promise; + replySpy: ReturnType; + runtimeError: ReturnType; +}> { + onSpy.mockClear(); + replySpyRef.mockClear(); + sendChatActionSpy.mockClear(); + + const runtimeError = options.runtimeError ?? vi.fn(); + const runtimeLog = options.runtimeLog ?? vi.fn(); + createTelegramBotRef({ + token: "tok", + testTimings: TELEGRAM_TEST_TIMINGS, + ...(options.proxyFetch ? { proxyFetch: options.proxyFetch } : {}), + runtime: { + log: runtimeLog as (...data: unknown[]) => void, + error: runtimeError as (...data: unknown[]) => void, + exit: () => { + throw new Error("exit"); + }, + }, + }); + const handler = onSpy.mock.calls.find((call) => call[0] === "message")?.[1] as ( + ctx: Record, + ) => Promise; + expect(handler).toBeDefined(); + return { handler, replySpy: replySpyRef, runtimeError }; +} + +export function mockTelegramFileDownload(params: { + contentType: string; + bytes: Uint8Array; +}): ReturnType { + return vi.spyOn(globalThis, "fetch").mockResolvedValueOnce({ + ok: true, + status: 200, + statusText: "OK", + headers: { get: () => params.contentType }, + arrayBuffer: async () => params.bytes.buffer, + } as unknown as Response); +} + +export function mockTelegramPngDownload(): ReturnType { + return vi.spyOn(globalThis, "fetch").mockResolvedValue({ + ok: true, + status: 200, + statusText: "OK", + headers: { get: () => "image/png" }, + arrayBuffer: async () => new Uint8Array([0x89, 0x50, 0x4e, 0x47]).buffer, + } as unknown as Response); +} + +beforeEach(() => { + vi.useRealTimers(); + lookupMock.mockResolvedValue([{ address: "93.184.216.34", family: 4 }]); + resolvePinnedHostnameSpy = vi + .spyOn(ssrf, "resolvePinnedHostname") + .mockImplementation((hostname) => resolvePinnedHostname(hostname, lookupMock)); +}); + +afterEach(() => { + lookupMock.mockClear(); + resolvePinnedHostnameSpy?.mockRestore(); + resolvePinnedHostnameSpy = null; +}); + +beforeAll(async () => { + ({ createTelegramBot: createTelegramBotRef } = await import("./bot.js")); + const replyModule = await import("../auto-reply/reply.js"); + replySpyRef = (replyModule as unknown as { __replySpy: ReturnType }).__replySpy; +}, TELEGRAM_BOT_IMPORT_TIMEOUT_MS); + +vi.mock("./sticker-cache.js", () => ({ + cacheSticker: (...args: unknown[]) => cacheStickerSpy(...args), + getCachedSticker: (...args: unknown[]) => getCachedStickerSpy(...args), + describeStickerImage: (...args: unknown[]) => describeStickerImageSpy(...args), +}));