diff --git a/src/telegram/bot.create-telegram-bot.accepts-group-messages-mentionpatterns-match-without-botusername.test.ts b/src/telegram/bot.create-telegram-bot.accepts-group-messages-mentionpatterns-match-without-botusername.test.ts index 46f1ba98f57..7b3a1cb9038 100644 --- a/src/telegram/bot.create-telegram-bot.accepts-group-messages-mentionpatterns-match-without-botusername.test.ts +++ b/src/telegram/bot.create-telegram-bot.accepts-group-messages-mentionpatterns-match-without-botusername.test.ts @@ -1,163 +1,22 @@ -import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { escapeRegExp, formatEnvelopeTimestamp } from "../../test/helpers/envelope-timestamp.js"; -import { resetInboundDedupe } from "../auto-reply/reply/inbound-dedupe.js"; +import { + getOnHandler, + getLoadConfigMock, + onSpy, + replySpy, + sendMessageSpy, + setMessageReactionSpy, + setMyCommandsSpy, +} from "./bot.create-telegram-bot.test-harness.js"; import { createTelegramBot } from "./bot.js"; -const { sessionStorePath } = vi.hoisted(() => ({ - sessionStorePath: `/tmp/openclaw-telegram-${Math.random().toString(16).slice(2)}.json`, -})); - -const { loadWebMedia } = vi.hoisted(() => ({ - loadWebMedia: vi.fn(), -})); - -vi.mock("../web/media.js", () => ({ - loadWebMedia, -})); - -const { loadConfig } = vi.hoisted(() => ({ - loadConfig: vi.fn(() => ({})), -})); -vi.mock("../config/config.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - loadConfig, - }; -}); - -vi.mock("../config/sessions.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - resolveStorePath: vi.fn((storePath) => storePath ?? sessionStorePath), - }; -}); - -const { readChannelAllowFromStore, upsertChannelPairingRequest } = vi.hoisted(() => ({ - readChannelAllowFromStore: vi.fn(async () => [] as string[]), - upsertChannelPairingRequest: vi.fn(async () => ({ - code: "PAIRCODE", - created: true, - })), -})); - -vi.mock("../pairing/pairing-store.js", () => ({ - readChannelAllowFromStore, - upsertChannelPairingRequest, -})); - -const useSpy = vi.fn(); -const middlewareUseSpy = vi.fn(); -const onSpy = vi.fn(); -const stopSpy = vi.fn(); -const commandSpy = vi.fn(); -const botCtorSpy = vi.fn(); -const answerCallbackQuerySpy = vi.fn(async () => undefined); -const sendChatActionSpy = vi.fn(); -const setMessageReactionSpy = vi.fn(async () => undefined); -const setMyCommandsSpy = vi.fn(async () => undefined); -const sendMessageSpy = vi.fn(async () => ({ message_id: 77 })); -const sendAnimationSpy = vi.fn(async () => ({ message_id: 78 })); -const sendPhotoSpy = vi.fn(async () => ({ message_id: 79 })); -type ApiStub = { - config: { use: (arg: unknown) => void }; - answerCallbackQuery: typeof answerCallbackQuerySpy; - sendChatAction: typeof sendChatActionSpy; - setMessageReaction: typeof setMessageReactionSpy; - setMyCommands: typeof setMyCommandsSpy; - sendMessage: typeof sendMessageSpy; - sendAnimation: typeof sendAnimationSpy; - sendPhoto: typeof sendPhotoSpy; -}; -const apiStub: ApiStub = { - config: { use: useSpy }, - answerCallbackQuery: answerCallbackQuerySpy, - sendChatAction: sendChatActionSpy, - setMessageReaction: setMessageReactionSpy, - setMyCommands: setMyCommandsSpy, - sendMessage: sendMessageSpy, - sendAnimation: sendAnimationSpy, - sendPhoto: sendPhotoSpy, -}; - -vi.mock("grammy", () => ({ - Bot: class { - api = apiStub; - use = middlewareUseSpy; - on = onSpy; - stop = stopSpy; - command = commandSpy; - catch = vi.fn(); - constructor( - public token: string, - public options?: { client?: { fetch?: typeof fetch } }, - ) { - botCtorSpy(token, options); - } - }, - InputFile: class {}, - webhookCallback: vi.fn(), -})); - -const sequentializeMiddleware = vi.fn(); -const sequentializeSpy = vi.fn(() => sequentializeMiddleware); -let _sequentializeKey: ((ctx: unknown) => string) | undefined; -vi.mock("@grammyjs/runner", () => ({ - sequentialize: (keyFn: (ctx: unknown) => string) => { - _sequentializeKey = keyFn; - return sequentializeSpy(); - }, -})); - -const throttlerSpy = vi.fn(() => "throttler"); - -vi.mock("@grammyjs/transformer-throttler", () => ({ - apiThrottler: () => throttlerSpy(), -})); - -vi.mock("../auto-reply/reply.js", () => { - const replySpy = vi.fn(async (_ctx, opts) => { - await opts?.onReplyStart?.(); - return undefined; - }); - return { getReplyFromConfig: replySpy, __replySpy: replySpy }; -}); - -let replyModule: typeof import("../auto-reply/reply.js"); - -const getOnHandler = (event: string) => { - const handler = onSpy.mock.calls.find((call) => call[0] === event)?.[1]; - if (!handler) { - throw new Error(`Missing handler for event: ${event}`); - } - return handler as (ctx: Record) => Promise; -}; +const loadConfig = getLoadConfigMock(); const ORIGINAL_TZ = process.env.TZ; describe("createTelegramBot", () => { - beforeAll(async () => { - replyModule = await import("../auto-reply/reply.js"); - }); - beforeEach(() => { process.env.TZ = "UTC"; - resetInboundDedupe(); - loadConfig.mockReturnValue({ - channels: { - telegram: { dmPolicy: "open", allowFrom: ["*"] }, - }, - }); - loadWebMedia.mockReset(); - sendAnimationSpy.mockReset(); - sendPhotoSpy.mockReset(); - setMessageReactionSpy.mockReset(); - answerCallbackQuerySpy.mockReset(); - setMyCommandsSpy.mockReset(); - middlewareUseSpy.mockReset(); - sequentializeSpy.mockReset(); - botCtorSpy.mockReset(); - _sequentializeKey = undefined; }); afterEach(() => { process.env.TZ = ORIGINAL_TZ; @@ -167,7 +26,6 @@ describe("createTelegramBot", () => { it("accepts group messages when mentionPatterns match (without @botUsername)", async () => { onSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; replySpy.mockReset(); loadConfig.mockReturnValue({ @@ -215,7 +73,6 @@ describe("createTelegramBot", () => { it("accepts group messages when mentionPatterns match even if another user is mentioned", async () => { onSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; replySpy.mockReset(); loadConfig.mockReturnValue({ @@ -256,7 +113,6 @@ describe("createTelegramBot", () => { it("keeps group envelope headers stable (sender identity is separate)", async () => { onSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; replySpy.mockReset(); loadConfig.mockReturnValue({ @@ -307,7 +163,6 @@ describe("createTelegramBot", () => { it("reacts to mention-gated group messages when ackReaction is enabled", async () => { onSpy.mockReset(); setMessageReactionSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; replySpy.mockReset(); loadConfig.mockReturnValue({ @@ -352,7 +207,6 @@ describe("createTelegramBot", () => { }); it("skips group messages when requireMention is enabled and no mention matches", async () => { onSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; replySpy.mockReset(); loadConfig.mockReturnValue({ @@ -384,7 +238,6 @@ describe("createTelegramBot", () => { }); it("allows group messages when requireMention is enabled but mentions cannot be detected", async () => { onSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; replySpy.mockReset(); loadConfig.mockReturnValue({ @@ -419,7 +272,6 @@ describe("createTelegramBot", () => { it("includes reply-to context when a Telegram reply is received", async () => { onSpy.mockReset(); sendMessageSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; replySpy.mockReset(); createTelegramBot({ token: "tok" }); diff --git a/src/telegram/bot.create-telegram-bot.applies-topic-skill-filters-system-prompts.test.ts b/src/telegram/bot.create-telegram-bot.applies-topic-skill-filters-system-prompts.test.ts index 0e1a68cb521..d68d74454e6 100644 --- a/src/telegram/bot.create-telegram-bot.applies-topic-skill-filters-system-prompts.test.ts +++ b/src/telegram/bot.create-telegram-bot.applies-topic-skill-filters-system-prompts.test.ts @@ -1,167 +1,21 @@ -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import { resetInboundDedupe } from "../auto-reply/reply/inbound-dedupe.js"; +import { describe, expect, it } from "vitest"; +import { + commandSpy, + getOnHandler, + getLoadConfigMock, + onSpy, + replySpy, + sendMessageSpy, +} from "./bot.create-telegram-bot.test-harness.js"; import { createTelegramBot } from "./bot.js"; -const { sessionStorePath } = vi.hoisted(() => ({ - sessionStorePath: `/tmp/openclaw-telegram-${Math.random().toString(16).slice(2)}.json`, -})); - -const { loadWebMedia } = vi.hoisted(() => ({ - loadWebMedia: vi.fn(), -})); - -vi.mock("../web/media.js", () => ({ - loadWebMedia, -})); - -const { loadConfig } = vi.hoisted(() => ({ - loadConfig: vi.fn(() => ({})), -})); -vi.mock("../config/config.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - loadConfig, - }; -}); - -vi.mock("../config/sessions.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - resolveStorePath: vi.fn((storePath) => storePath ?? sessionStorePath), - }; -}); - -const { readChannelAllowFromStore, upsertChannelPairingRequest } = vi.hoisted(() => ({ - readChannelAllowFromStore: vi.fn(async () => [] as string[]), - upsertChannelPairingRequest: vi.fn(async () => ({ - code: "PAIRCODE", - created: true, - })), -})); - -vi.mock("../pairing/pairing-store.js", () => ({ - readChannelAllowFromStore, - upsertChannelPairingRequest, -})); - -const useSpy = vi.fn(); -const middlewareUseSpy = vi.fn(); -const onSpy = vi.fn(); -const stopSpy = vi.fn(); -const commandSpy = vi.fn(); -const botCtorSpy = vi.fn(); -const answerCallbackQuerySpy = vi.fn(async () => undefined); -const sendChatActionSpy = vi.fn(); -const setMessageReactionSpy = vi.fn(async () => undefined); -const setMyCommandsSpy = vi.fn(async () => undefined); -const sendMessageSpy = vi.fn(async () => ({ message_id: 77 })); -const sendAnimationSpy = vi.fn(async () => ({ message_id: 78 })); -const sendPhotoSpy = vi.fn(async () => ({ message_id: 79 })); -type ApiStub = { - config: { use: (arg: unknown) => void }; - answerCallbackQuery: typeof answerCallbackQuerySpy; - sendChatAction: typeof sendChatActionSpy; - setMessageReaction: typeof setMessageReactionSpy; - setMyCommands: typeof setMyCommandsSpy; - sendMessage: typeof sendMessageSpy; - sendAnimation: typeof sendAnimationSpy; - sendPhoto: typeof sendPhotoSpy; -}; -const apiStub: ApiStub = { - config: { use: useSpy }, - answerCallbackQuery: answerCallbackQuerySpy, - sendChatAction: sendChatActionSpy, - setMessageReaction: setMessageReactionSpy, - setMyCommands: setMyCommandsSpy, - sendMessage: sendMessageSpy, - sendAnimation: sendAnimationSpy, - sendPhoto: sendPhotoSpy, -}; - -vi.mock("grammy", () => ({ - Bot: class { - api = apiStub; - use = middlewareUseSpy; - on = onSpy; - stop = stopSpy; - command = commandSpy; - catch = vi.fn(); - constructor( - public token: string, - public options?: { client?: { fetch?: typeof fetch } }, - ) { - botCtorSpy(token, options); - } - }, - InputFile: class {}, - webhookCallback: vi.fn(), -})); - -const sequentializeMiddleware = vi.fn(); -const sequentializeSpy = vi.fn(() => sequentializeMiddleware); -let _sequentializeKey: ((ctx: unknown) => string) | undefined; -vi.mock("@grammyjs/runner", () => ({ - sequentialize: (keyFn: (ctx: unknown) => string) => { - _sequentializeKey = keyFn; - return sequentializeSpy(); - }, -})); - -const throttlerSpy = vi.fn(() => "throttler"); - -vi.mock("@grammyjs/transformer-throttler", () => ({ - apiThrottler: () => throttlerSpy(), -})); - -vi.mock("../auto-reply/reply.js", () => { - const replySpy = vi.fn(async (_ctx, opts) => { - await opts?.onReplyStart?.(); - return undefined; - }); - return { getReplyFromConfig: replySpy, __replySpy: replySpy }; -}); - -let replyModule: typeof import("../auto-reply/reply.js"); - -const getOnHandler = (event: string) => { - const handler = onSpy.mock.calls.find((call) => call[0] === event)?.[1]; - if (!handler) { - throw new Error(`Missing handler for event: ${event}`); - } - return handler as (ctx: Record) => Promise; -}; +const loadConfig = getLoadConfigMock(); describe("createTelegramBot", () => { - beforeAll(async () => { - replyModule = await import("../auto-reply/reply.js"); - }); - - beforeEach(() => { - resetInboundDedupe(); - loadConfig.mockReturnValue({ - channels: { - telegram: { dmPolicy: "open", allowFrom: ["*"] }, - }, - }); - loadWebMedia.mockReset(); - sendAnimationSpy.mockReset(); - sendPhotoSpy.mockReset(); - setMessageReactionSpy.mockReset(); - answerCallbackQuerySpy.mockReset(); - setMyCommandsSpy.mockReset(); - middlewareUseSpy.mockReset(); - sequentializeSpy.mockReset(); - botCtorSpy.mockReset(); - _sequentializeKey = undefined; - }); - // groupPolicy tests it("applies topic skill filters and system prompts", async () => { onSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; replySpy.mockReset(); loadConfig.mockReturnValue({ @@ -216,7 +70,6 @@ describe("createTelegramBot", () => { onSpy.mockReset(); sendMessageSpy.mockReset(); commandSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; replySpy.mockReset(); replySpy.mockResolvedValue({ text: "response" }); @@ -260,7 +113,6 @@ describe("createTelegramBot", () => { onSpy.mockReset(); sendMessageSpy.mockReset(); commandSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; replySpy.mockReset(); replySpy.mockResolvedValue({ text: "response" }); @@ -306,7 +158,6 @@ describe("createTelegramBot", () => { onSpy.mockReset(); sendMessageSpy.mockReset(); commandSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; replySpy.mockReset(); replySpy.mockImplementation(async (_ctx, opts) => { await opts?.onToolResult?.({ text: "tool update" }); @@ -347,7 +198,6 @@ describe("createTelegramBot", () => { }); it("dedupes duplicate message updates by update_id", async () => { onSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; replySpy.mockReset(); loadConfig.mockReturnValue({ diff --git a/src/telegram/bot.create-telegram-bot.blocks-all-group-messages-grouppolicy-is.test.ts b/src/telegram/bot.create-telegram-bot.blocks-all-group-messages-grouppolicy-is.test.ts index cf10939453c..40bee194b61 100644 --- a/src/telegram/bot.create-telegram-bot.blocks-all-group-messages-grouppolicy-is.test.ts +++ b/src/telegram/bot.create-telegram-bot.blocks-all-group-messages-grouppolicy-is.test.ts @@ -1,167 +1,19 @@ -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import { resetInboundDedupe } from "../auto-reply/reply/inbound-dedupe.js"; +import { describe, expect, it } from "vitest"; +import { + getLoadConfigMock, + getOnHandler, + onSpy, + replySpy, +} from "./bot.create-telegram-bot.test-harness.js"; import { createTelegramBot } from "./bot.js"; -const { sessionStorePath } = vi.hoisted(() => ({ - sessionStorePath: `/tmp/openclaw-telegram-${Math.random().toString(16).slice(2)}.json`, -})); - -const { loadWebMedia } = vi.hoisted(() => ({ - loadWebMedia: vi.fn(), -})); - -vi.mock("../web/media.js", () => ({ - loadWebMedia, -})); - -const { loadConfig } = vi.hoisted(() => ({ - loadConfig: vi.fn(() => ({})), -})); -vi.mock("../config/config.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - loadConfig, - }; -}); - -vi.mock("../config/sessions.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - resolveStorePath: vi.fn((storePath) => storePath ?? sessionStorePath), - }; -}); - -const { readChannelAllowFromStore, upsertChannelPairingRequest } = vi.hoisted(() => ({ - readChannelAllowFromStore: vi.fn(async () => [] as string[]), - upsertChannelPairingRequest: vi.fn(async () => ({ - code: "PAIRCODE", - created: true, - })), -})); - -vi.mock("../pairing/pairing-store.js", () => ({ - readChannelAllowFromStore, - upsertChannelPairingRequest, -})); - -const useSpy = vi.fn(); -const middlewareUseSpy = vi.fn(); -const onSpy = vi.fn(); -const stopSpy = vi.fn(); -const commandSpy = vi.fn(); -const botCtorSpy = vi.fn(); -const answerCallbackQuerySpy = vi.fn(async () => undefined); -const sendChatActionSpy = vi.fn(); -const setMessageReactionSpy = vi.fn(async () => undefined); -const setMyCommandsSpy = vi.fn(async () => undefined); -const sendMessageSpy = vi.fn(async () => ({ message_id: 77 })); -const sendAnimationSpy = vi.fn(async () => ({ message_id: 78 })); -const sendPhotoSpy = vi.fn(async () => ({ message_id: 79 })); -type ApiStub = { - config: { use: (arg: unknown) => void }; - answerCallbackQuery: typeof answerCallbackQuerySpy; - sendChatAction: typeof sendChatActionSpy; - setMessageReaction: typeof setMessageReactionSpy; - setMyCommands: typeof setMyCommandsSpy; - sendMessage: typeof sendMessageSpy; - sendAnimation: typeof sendAnimationSpy; - sendPhoto: typeof sendPhotoSpy; -}; -const apiStub: ApiStub = { - config: { use: useSpy }, - answerCallbackQuery: answerCallbackQuerySpy, - sendChatAction: sendChatActionSpy, - setMessageReaction: setMessageReactionSpy, - setMyCommands: setMyCommandsSpy, - sendMessage: sendMessageSpy, - sendAnimation: sendAnimationSpy, - sendPhoto: sendPhotoSpy, -}; - -vi.mock("grammy", () => ({ - Bot: class { - api = apiStub; - use = middlewareUseSpy; - on = onSpy; - stop = stopSpy; - command = commandSpy; - catch = vi.fn(); - constructor( - public token: string, - public options?: { client?: { fetch?: typeof fetch } }, - ) { - botCtorSpy(token, options); - } - }, - InputFile: class {}, - webhookCallback: vi.fn(), -})); - -const sequentializeMiddleware = vi.fn(); -const sequentializeSpy = vi.fn(() => sequentializeMiddleware); -let _sequentializeKey: ((ctx: unknown) => string) | undefined; -vi.mock("@grammyjs/runner", () => ({ - sequentialize: (keyFn: (ctx: unknown) => string) => { - _sequentializeKey = keyFn; - return sequentializeSpy(); - }, -})); - -const throttlerSpy = vi.fn(() => "throttler"); - -vi.mock("@grammyjs/transformer-throttler", () => ({ - apiThrottler: () => throttlerSpy(), -})); - -vi.mock("../auto-reply/reply.js", () => { - const replySpy = vi.fn(async (_ctx, opts) => { - await opts?.onReplyStart?.(); - return undefined; - }); - return { getReplyFromConfig: replySpy, __replySpy: replySpy }; -}); - -let replyModule: typeof import("../auto-reply/reply.js"); - -const getOnHandler = (event: string) => { - const handler = onSpy.mock.calls.find((call) => call[0] === event)?.[1]; - if (!handler) { - throw new Error(`Missing handler for event: ${event}`); - } - return handler as (ctx: Record) => Promise; -}; +const loadConfig = getLoadConfigMock(); describe("createTelegramBot", () => { - beforeAll(async () => { - replyModule = await import("../auto-reply/reply.js"); - }); - - beforeEach(() => { - resetInboundDedupe(); - loadConfig.mockReturnValue({ - channels: { - telegram: { dmPolicy: "open", allowFrom: ["*"] }, - }, - }); - loadWebMedia.mockReset(); - sendAnimationSpy.mockReset(); - sendPhotoSpy.mockReset(); - setMessageReactionSpy.mockReset(); - answerCallbackQuerySpy.mockReset(); - setMyCommandsSpy.mockReset(); - middlewareUseSpy.mockReset(); - sequentializeSpy.mockReset(); - botCtorSpy.mockReset(); - _sequentializeKey = undefined; - }); - // groupPolicy tests it("blocks all group messages when groupPolicy is 'disabled'", async () => { onSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; replySpy.mockReset(); loadConfig.mockReturnValue({ channels: { @@ -191,7 +43,6 @@ describe("createTelegramBot", () => { }); it("blocks group messages from senders not in allowFrom when groupPolicy is 'allowlist'", async () => { onSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; replySpy.mockReset(); loadConfig.mockReturnValue({ channels: { @@ -220,7 +71,6 @@ describe("createTelegramBot", () => { }); it("allows group messages from senders in allowFrom (by ID) when groupPolicy is 'allowlist'", async () => { onSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; replySpy.mockReset(); loadConfig.mockReturnValue({ channels: { @@ -250,7 +100,6 @@ describe("createTelegramBot", () => { }); it("blocks group messages when allowFrom is configured with @username entries (numeric IDs required)", async () => { onSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; replySpy.mockReset(); loadConfig.mockReturnValue({ channels: { @@ -280,7 +129,6 @@ describe("createTelegramBot", () => { }); it("allows group messages from telegram:-prefixed allowFrom entries when groupPolicy is 'allowlist'", async () => { onSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; replySpy.mockReset(); loadConfig.mockReturnValue({ channels: { @@ -310,7 +158,6 @@ describe("createTelegramBot", () => { }); it("allows group messages from tg:-prefixed allowFrom entries case-insensitively when groupPolicy is 'allowlist'", async () => { onSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; replySpy.mockReset(); loadConfig.mockReturnValue({ channels: { @@ -340,7 +187,6 @@ describe("createTelegramBot", () => { }); it("allows all group messages when groupPolicy is 'open'", async () => { onSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; replySpy.mockReset(); loadConfig.mockReturnValue({ channels: { diff --git a/src/telegram/bot.create-telegram-bot.dedupes-duplicate-callback-query-updates-by-update.test.ts b/src/telegram/bot.create-telegram-bot.dedupes-duplicate-callback-query-updates-by-update.test.ts index 55b851ddae7..00e60d85118 100644 --- a/src/telegram/bot.create-telegram-bot.dedupes-duplicate-callback-query-updates-by-update.test.ts +++ b/src/telegram/bot.create-telegram-bot.dedupes-duplicate-callback-query-updates-by-update.test.ts @@ -1,167 +1,19 @@ -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import { resetInboundDedupe } from "../auto-reply/reply/inbound-dedupe.js"; +import { describe, expect, it } from "vitest"; +import { + getOnHandler, + getLoadConfigMock, + onSpy, + replySpy, +} from "./bot.create-telegram-bot.test-harness.js"; import { createTelegramBot } from "./bot.js"; -const { sessionStorePath } = vi.hoisted(() => ({ - sessionStorePath: `/tmp/openclaw-telegram-${Math.random().toString(16).slice(2)}.json`, -})); - -const { loadWebMedia } = vi.hoisted(() => ({ - loadWebMedia: vi.fn(), -})); - -vi.mock("../web/media.js", () => ({ - loadWebMedia, -})); - -const { loadConfig } = vi.hoisted(() => ({ - loadConfig: vi.fn(() => ({})), -})); -vi.mock("../config/config.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - loadConfig, - }; -}); - -vi.mock("../config/sessions.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - resolveStorePath: vi.fn((storePath) => storePath ?? sessionStorePath), - }; -}); - -const { readChannelAllowFromStore, upsertChannelPairingRequest } = vi.hoisted(() => ({ - readChannelAllowFromStore: vi.fn(async () => [] as string[]), - upsertChannelPairingRequest: vi.fn(async () => ({ - code: "PAIRCODE", - created: true, - })), -})); - -vi.mock("../pairing/pairing-store.js", () => ({ - readChannelAllowFromStore, - upsertChannelPairingRequest, -})); - -const useSpy = vi.fn(); -const middlewareUseSpy = vi.fn(); -const onSpy = vi.fn(); -const stopSpy = vi.fn(); -const commandSpy = vi.fn(); -const botCtorSpy = vi.fn(); -const answerCallbackQuerySpy = vi.fn(async () => undefined); -const sendChatActionSpy = vi.fn(); -const setMessageReactionSpy = vi.fn(async () => undefined); -const setMyCommandsSpy = vi.fn(async () => undefined); -const sendMessageSpy = vi.fn(async () => ({ message_id: 77 })); -const sendAnimationSpy = vi.fn(async () => ({ message_id: 78 })); -const sendPhotoSpy = vi.fn(async () => ({ message_id: 79 })); -type ApiStub = { - config: { use: (arg: unknown) => void }; - answerCallbackQuery: typeof answerCallbackQuerySpy; - sendChatAction: typeof sendChatActionSpy; - setMessageReaction: typeof setMessageReactionSpy; - setMyCommands: typeof setMyCommandsSpy; - sendMessage: typeof sendMessageSpy; - sendAnimation: typeof sendAnimationSpy; - sendPhoto: typeof sendPhotoSpy; -}; -const apiStub: ApiStub = { - config: { use: useSpy }, - answerCallbackQuery: answerCallbackQuerySpy, - sendChatAction: sendChatActionSpy, - setMessageReaction: setMessageReactionSpy, - setMyCommands: setMyCommandsSpy, - sendMessage: sendMessageSpy, - sendAnimation: sendAnimationSpy, - sendPhoto: sendPhotoSpy, -}; - -vi.mock("grammy", () => ({ - Bot: class { - api = apiStub; - use = middlewareUseSpy; - on = onSpy; - stop = stopSpy; - command = commandSpy; - catch = vi.fn(); - constructor( - public token: string, - public options?: { client?: { fetch?: typeof fetch } }, - ) { - botCtorSpy(token, options); - } - }, - InputFile: class {}, - webhookCallback: vi.fn(), -})); - -const sequentializeMiddleware = vi.fn(); -const sequentializeSpy = vi.fn(() => sequentializeMiddleware); -let _sequentializeKey: ((ctx: unknown) => string) | undefined; -vi.mock("@grammyjs/runner", () => ({ - sequentialize: (keyFn: (ctx: unknown) => string) => { - _sequentializeKey = keyFn; - return sequentializeSpy(); - }, -})); - -const throttlerSpy = vi.fn(() => "throttler"); - -vi.mock("@grammyjs/transformer-throttler", () => ({ - apiThrottler: () => throttlerSpy(), -})); - -vi.mock("../auto-reply/reply.js", () => { - const replySpy = vi.fn(async (_ctx, opts) => { - await opts?.onReplyStart?.(); - return undefined; - }); - return { getReplyFromConfig: replySpy, __replySpy: replySpy }; -}); - -let replyModule: typeof import("../auto-reply/reply.js"); - -const getOnHandler = (event: string) => { - const handler = onSpy.mock.calls.find((call) => call[0] === event)?.[1]; - if (!handler) { - throw new Error(`Missing handler for event: ${event}`); - } - return handler as (ctx: Record) => Promise; -}; +const loadConfig = getLoadConfigMock(); describe("createTelegramBot", () => { - beforeAll(async () => { - replyModule = await import("../auto-reply/reply.js"); - }); - - beforeEach(() => { - resetInboundDedupe(); - loadConfig.mockReturnValue({ - channels: { - telegram: { dmPolicy: "open", allowFrom: ["*"] }, - }, - }); - loadWebMedia.mockReset(); - sendAnimationSpy.mockReset(); - sendPhotoSpy.mockReset(); - setMessageReactionSpy.mockReset(); - answerCallbackQuerySpy.mockReset(); - setMyCommandsSpy.mockReset(); - middlewareUseSpy.mockReset(); - sequentializeSpy.mockReset(); - botCtorSpy.mockReset(); - _sequentializeKey = undefined; - }); - // groupPolicy tests it("dedupes duplicate callback_query updates by update_id", async () => { onSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; replySpy.mockReset(); loadConfig.mockReturnValue({ @@ -198,7 +50,6 @@ describe("createTelegramBot", () => { }); it("allows distinct callback_query ids without update_id", async () => { onSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; replySpy.mockReset(); loadConfig.mockReturnValue({ diff --git a/src/telegram/bot.create-telegram-bot.installs-grammy-throttler.test.ts b/src/telegram/bot.create-telegram-bot.installs-grammy-throttler.test.ts index 1b43886f19d..d0f02368840 100644 --- a/src/telegram/bot.create-telegram-bot.installs-grammy-throttler.test.ts +++ b/src/telegram/bot.create-telegram-bot.installs-grammy-throttler.test.ts @@ -1,151 +1,40 @@ -import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { escapeRegExp, formatEnvelopeTimestamp } from "../../test/helpers/envelope-timestamp.js"; -import { resetInboundDedupe } from "../auto-reply/reply/inbound-dedupe.js"; +import { + answerCallbackQuerySpy, + botCtorSpy, + getLoadConfigMock, + getLoadWebMediaMock, + getOnHandler, + getReadChannelAllowFromStoreMock, + getUpsertChannelPairingRequestMock, + middlewareUseSpy, + onSpy, + replySpy, + sendAnimationSpy, + sendChatActionSpy, + sendMessageSpy, + sendPhotoSpy, + sequentializeKey, + sequentializeSpy, + setMessageReactionSpy, + setMyCommandsSpy, + throttlerSpy, + useSpy, +} from "./bot.create-telegram-bot.test-harness.js"; import { createTelegramBot, getTelegramSequentialKey } from "./bot.js"; import { resolveTelegramFetch } from "./fetch.js"; -const { sessionStorePath } = vi.hoisted(() => ({ - sessionStorePath: `/tmp/openclaw-telegram-throttler-${Math.random().toString(16).slice(2)}.json`, -})); -const { loadWebMedia } = vi.hoisted(() => ({ - loadWebMedia: vi.fn(), -})); - -vi.mock("../web/media.js", () => ({ - loadWebMedia, -})); - -const { loadConfig } = vi.hoisted(() => ({ - loadConfig: vi.fn(() => ({})), -})); -vi.mock("../config/config.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - loadConfig, - }; -}); - -vi.mock("../config/sessions.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - resolveStorePath: vi.fn((storePath) => storePath ?? sessionStorePath), - }; -}); - -const { readChannelAllowFromStore, upsertChannelPairingRequest } = vi.hoisted(() => ({ - readChannelAllowFromStore: vi.fn(async () => [] as string[]), - upsertChannelPairingRequest: vi.fn(async () => ({ - code: "PAIRCODE", - created: true, - })), -})); - -vi.mock("../pairing/pairing-store.js", () => ({ - readChannelAllowFromStore, - upsertChannelPairingRequest, -})); - -const useSpy = vi.fn(); -const middlewareUseSpy = vi.fn(); -const onSpy = vi.fn(); -const stopSpy = vi.fn(); -const commandSpy = vi.fn(); -const botCtorSpy = vi.fn(); -const answerCallbackQuerySpy = vi.fn(async () => undefined); -const sendChatActionSpy = vi.fn(); -const setMessageReactionSpy = vi.fn(async () => undefined); -const setMyCommandsSpy = vi.fn(async () => undefined); -const sendMessageSpy = vi.fn(async () => ({ message_id: 77 })); -const sendAnimationSpy = vi.fn(async () => ({ message_id: 78 })); -const sendPhotoSpy = vi.fn(async () => ({ message_id: 79 })); -type ApiStub = { - config: { use: (arg: unknown) => void }; - answerCallbackQuery: typeof answerCallbackQuerySpy; - sendChatAction: typeof sendChatActionSpy; - setMessageReaction: typeof setMessageReactionSpy; - setMyCommands: typeof setMyCommandsSpy; - sendMessage: typeof sendMessageSpy; - sendAnimation: typeof sendAnimationSpy; - sendPhoto: typeof sendPhotoSpy; -}; -const apiStub: ApiStub = { - config: { use: useSpy }, - answerCallbackQuery: answerCallbackQuerySpy, - sendChatAction: sendChatActionSpy, - setMessageReaction: setMessageReactionSpy, - setMyCommands: setMyCommandsSpy, - sendMessage: sendMessageSpy, - sendAnimation: sendAnimationSpy, - sendPhoto: sendPhotoSpy, -}; - -vi.mock("grammy", () => ({ - Bot: class { - api = apiStub; - use = middlewareUseSpy; - on = onSpy; - stop = stopSpy; - command = commandSpy; - catch = vi.fn(); - constructor( - public token: string, - public options?: { - client?: { fetch?: typeof fetch; timeoutSeconds?: number }; - }, - ) { - botCtorSpy(token, options); - } - }, - InputFile: class {}, - webhookCallback: vi.fn(), -})); - -const sequentializeMiddleware = vi.fn(); -const sequentializeSpy = vi.fn(() => sequentializeMiddleware); -let sequentializeKey: ((ctx: unknown) => string) | undefined; -vi.mock("@grammyjs/runner", () => ({ - sequentialize: (keyFn: (ctx: unknown) => string) => { - sequentializeKey = keyFn; - return sequentializeSpy(); - }, -})); - -const throttlerSpy = vi.fn(() => "throttler"); - -vi.mock("@grammyjs/transformer-throttler", () => ({ - apiThrottler: () => throttlerSpy(), -})); - -vi.mock("../auto-reply/reply.js", () => { - const replySpy = vi.fn(async (_ctx, opts) => { - await opts?.onReplyStart?.(); - return undefined; - }); - return { getReplyFromConfig: replySpy, __replySpy: replySpy }; -}); - -let replyModule: typeof import("../auto-reply/reply.js"); - -const getOnHandler = (event: string) => { - const handler = onSpy.mock.calls.find((call) => call[0] === event)?.[1]; - if (!handler) { - throw new Error(`Missing handler for event: ${event}`); - } - return handler as (ctx: Record) => Promise; -}; +const loadConfig = getLoadConfigMock(); +const loadWebMedia = getLoadWebMediaMock(); +const readChannelAllowFromStore = getReadChannelAllowFromStoreMock(); +const upsertChannelPairingRequest = getUpsertChannelPairingRequestMock(); const ORIGINAL_TZ = process.env.TZ; describe("createTelegramBot", () => { - beforeAll(async () => { - replyModule = await import("../auto-reply/reply.js"); - }); - beforeEach(() => { process.env.TZ = "UTC"; - resetInboundDedupe(); loadConfig.mockReturnValue({ agents: { defaults: { @@ -165,7 +54,6 @@ describe("createTelegramBot", () => { middlewareUseSpy.mockReset(); sequentializeSpy.mockReset(); botCtorSpy.mockReset(); - sequentializeKey = undefined; }); afterEach(() => { process.env.TZ = ORIGINAL_TZ; @@ -274,7 +162,6 @@ describe("createTelegramBot", () => { }); it("routes callback_query payloads as messages and answers callbacks", async () => { onSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; replySpy.mockReset(); createTelegramBot({ token: "tok" }); @@ -309,7 +196,6 @@ describe("createTelegramBot", () => { try { onSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; replySpy.mockReset(); createTelegramBot({ token: "tok" }); @@ -349,7 +235,6 @@ describe("createTelegramBot", () => { it("requests pairing by default for unknown DM senders", async () => { onSpy.mockReset(); sendMessageSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; replySpy.mockReset(); loadConfig.mockReturnValue({ @@ -388,7 +273,6 @@ describe("createTelegramBot", () => { it("does not resend pairing code when a request is already pending", async () => { onSpy.mockReset(); sendMessageSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; replySpy.mockReset(); loadConfig.mockReturnValue({ diff --git a/src/telegram/bot.create-telegram-bot.matches-tg-prefixed-allowfrom-entries-case-insensitively.test.ts b/src/telegram/bot.create-telegram-bot.matches-tg-prefixed-allowfrom-entries-case-insensitively.test.ts index c5449baf256..d91415deabc 100644 --- a/src/telegram/bot.create-telegram-bot.matches-tg-prefixed-allowfrom-entries-case-insensitively.test.ts +++ b/src/telegram/bot.create-telegram-bot.matches-tg-prefixed-allowfrom-entries-case-insensitively.test.ts @@ -1,167 +1,21 @@ -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import { resetInboundDedupe } from "../auto-reply/reply/inbound-dedupe.js"; +import { describe, expect, it } from "vitest"; +import { + getLoadConfigMock, + getOnHandler, + onSpy, + replySpy, + sendChatActionSpy, + sendMessageSpy, +} from "./bot.create-telegram-bot.test-harness.js"; import { createTelegramBot } from "./bot.js"; -const { sessionStorePath } = vi.hoisted(() => ({ - sessionStorePath: `/tmp/openclaw-telegram-${Math.random().toString(16).slice(2)}.json`, -})); - -const { loadWebMedia } = vi.hoisted(() => ({ - loadWebMedia: vi.fn(), -})); - -vi.mock("../web/media.js", () => ({ - loadWebMedia, -})); - -const { loadConfig } = vi.hoisted(() => ({ - loadConfig: vi.fn(() => ({})), -})); -vi.mock("../config/config.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - loadConfig, - }; -}); - -vi.mock("../config/sessions.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - resolveStorePath: vi.fn((storePath) => storePath ?? sessionStorePath), - }; -}); - -const { readChannelAllowFromStore, upsertChannelPairingRequest } = vi.hoisted(() => ({ - readChannelAllowFromStore: vi.fn(async () => [] as string[]), - upsertChannelPairingRequest: vi.fn(async () => ({ - code: "PAIRCODE", - created: true, - })), -})); - -vi.mock("../pairing/pairing-store.js", () => ({ - readChannelAllowFromStore, - upsertChannelPairingRequest, -})); - -const useSpy = vi.fn(); -const middlewareUseSpy = vi.fn(); -const onSpy = vi.fn(); -const stopSpy = vi.fn(); -const commandSpy = vi.fn(); -const botCtorSpy = vi.fn(); -const answerCallbackQuerySpy = vi.fn(async () => undefined); -const sendChatActionSpy = vi.fn(); -const setMessageReactionSpy = vi.fn(async () => undefined); -const setMyCommandsSpy = vi.fn(async () => undefined); -const sendMessageSpy = vi.fn(async () => ({ message_id: 77 })); -const sendAnimationSpy = vi.fn(async () => ({ message_id: 78 })); -const sendPhotoSpy = vi.fn(async () => ({ message_id: 79 })); -type ApiStub = { - config: { use: (arg: unknown) => void }; - answerCallbackQuery: typeof answerCallbackQuerySpy; - sendChatAction: typeof sendChatActionSpy; - setMessageReaction: typeof setMessageReactionSpy; - setMyCommands: typeof setMyCommandsSpy; - sendMessage: typeof sendMessageSpy; - sendAnimation: typeof sendAnimationSpy; - sendPhoto: typeof sendPhotoSpy; -}; -const apiStub: ApiStub = { - config: { use: useSpy }, - answerCallbackQuery: answerCallbackQuerySpy, - sendChatAction: sendChatActionSpy, - setMessageReaction: setMessageReactionSpy, - setMyCommands: setMyCommandsSpy, - sendMessage: sendMessageSpy, - sendAnimation: sendAnimationSpy, - sendPhoto: sendPhotoSpy, -}; - -vi.mock("grammy", () => ({ - Bot: class { - api = apiStub; - use = middlewareUseSpy; - on = onSpy; - stop = stopSpy; - command = commandSpy; - catch = vi.fn(); - constructor( - public token: string, - public options?: { client?: { fetch?: typeof fetch } }, - ) { - botCtorSpy(token, options); - } - }, - InputFile: class {}, - webhookCallback: vi.fn(), -})); - -const sequentializeMiddleware = vi.fn(); -const sequentializeSpy = vi.fn(() => sequentializeMiddleware); -let _sequentializeKey: ((ctx: unknown) => string) | undefined; -vi.mock("@grammyjs/runner", () => ({ - sequentialize: (keyFn: (ctx: unknown) => string) => { - _sequentializeKey = keyFn; - return sequentializeSpy(); - }, -})); - -const throttlerSpy = vi.fn(() => "throttler"); - -vi.mock("@grammyjs/transformer-throttler", () => ({ - apiThrottler: () => throttlerSpy(), -})); - -vi.mock("../auto-reply/reply.js", () => { - const replySpy = vi.fn(async (_ctx, opts) => { - await opts?.onReplyStart?.(); - return undefined; - }); - return { getReplyFromConfig: replySpy, __replySpy: replySpy }; -}); - -let replyModule: typeof import("../auto-reply/reply.js"); - -const getOnHandler = (event: string) => { - const handler = onSpy.mock.calls.find((call) => call[0] === event)?.[1]; - if (!handler) { - throw new Error(`Missing handler for event: ${event}`); - } - return handler as (ctx: Record) => Promise; -}; +const loadConfig = getLoadConfigMock(); describe("createTelegramBot", () => { - beforeAll(async () => { - replyModule = await import("../auto-reply/reply.js"); - }); - - beforeEach(() => { - resetInboundDedupe(); - loadConfig.mockReturnValue({ - channels: { - telegram: { dmPolicy: "open", allowFrom: ["*"] }, - }, - }); - loadWebMedia.mockReset(); - sendAnimationSpy.mockReset(); - sendPhotoSpy.mockReset(); - setMessageReactionSpy.mockReset(); - answerCallbackQuerySpy.mockReset(); - setMyCommandsSpy.mockReset(); - middlewareUseSpy.mockReset(); - sequentializeSpy.mockReset(); - botCtorSpy.mockReset(); - _sequentializeKey = undefined; - }); - // groupPolicy tests it("matches tg:-prefixed allowFrom entries case-insensitively in group allowlist", async () => { onSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; replySpy.mockReset(); loadConfig.mockReturnValue({ channels: { @@ -192,7 +46,6 @@ describe("createTelegramBot", () => { }); it("blocks group messages when groupPolicy allowlist has no groupAllowFrom", async () => { onSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; replySpy.mockReset(); loadConfig.mockReturnValue({ channels: { @@ -221,7 +74,6 @@ describe("createTelegramBot", () => { }); it("allows control commands with TG-prefixed groupAllowFrom entries", async () => { onSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; replySpy.mockReset(); loadConfig.mockReturnValue({ channels: { @@ -252,7 +104,6 @@ describe("createTelegramBot", () => { it("isolates forum topic sessions and carries thread metadata", async () => { onSpy.mockReset(); sendChatActionSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; replySpy.mockReset(); loadConfig.mockReturnValue({ @@ -298,7 +149,6 @@ describe("createTelegramBot", () => { it("falls back to General topic thread id for typing in forums", async () => { onSpy.mockReset(); sendChatActionSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; replySpy.mockReset(); loadConfig.mockReturnValue({ @@ -338,7 +188,6 @@ describe("createTelegramBot", () => { it("routes General topic replies using thread id 1", async () => { onSpy.mockReset(); sendMessageSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; replySpy.mockReset(); replySpy.mockResolvedValue({ text: "response" }); diff --git a/src/telegram/bot.create-telegram-bot.matches-usernames-case-insensitively-grouppolicy-is.test.ts b/src/telegram/bot.create-telegram-bot.matches-usernames-case-insensitively-grouppolicy-is.test.ts index 9dc4662a7af..c4d434249ff 100644 --- a/src/telegram/bot.create-telegram-bot.matches-usernames-case-insensitively-grouppolicy-is.test.ts +++ b/src/telegram/bot.create-telegram-bot.matches-usernames-case-insensitively-grouppolicy-is.test.ts @@ -1,167 +1,19 @@ -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import { resetInboundDedupe } from "../auto-reply/reply/inbound-dedupe.js"; +import { describe, expect, it } from "vitest"; +import { + getLoadConfigMock, + getOnHandler, + onSpy, + replySpy, +} from "./bot.create-telegram-bot.test-harness.js"; import { createTelegramBot } from "./bot.js"; -const { sessionStorePath } = vi.hoisted(() => ({ - sessionStorePath: `/tmp/openclaw-telegram-${Math.random().toString(16).slice(2)}.json`, -})); - -const { loadWebMedia } = vi.hoisted(() => ({ - loadWebMedia: vi.fn(), -})); - -vi.mock("../web/media.js", () => ({ - loadWebMedia, -})); - -const { loadConfig } = vi.hoisted(() => ({ - loadConfig: vi.fn(() => ({})), -})); -vi.mock("../config/config.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - loadConfig, - }; -}); - -vi.mock("../config/sessions.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - resolveStorePath: vi.fn((storePath) => storePath ?? sessionStorePath), - }; -}); - -const { readChannelAllowFromStore, upsertChannelPairingRequest } = vi.hoisted(() => ({ - readChannelAllowFromStore: vi.fn(async () => [] as string[]), - upsertChannelPairingRequest: vi.fn(async () => ({ - code: "PAIRCODE", - created: true, - })), -})); - -vi.mock("../pairing/pairing-store.js", () => ({ - readChannelAllowFromStore, - upsertChannelPairingRequest, -})); - -const useSpy = vi.fn(); -const middlewareUseSpy = vi.fn(); -const onSpy = vi.fn(); -const stopSpy = vi.fn(); -const commandSpy = vi.fn(); -const botCtorSpy = vi.fn(); -const answerCallbackQuerySpy = vi.fn(async () => undefined); -const sendChatActionSpy = vi.fn(); -const setMessageReactionSpy = vi.fn(async () => undefined); -const setMyCommandsSpy = vi.fn(async () => undefined); -const sendMessageSpy = vi.fn(async () => ({ message_id: 77 })); -const sendAnimationSpy = vi.fn(async () => ({ message_id: 78 })); -const sendPhotoSpy = vi.fn(async () => ({ message_id: 79 })); -type ApiStub = { - config: { use: (arg: unknown) => void }; - answerCallbackQuery: typeof answerCallbackQuerySpy; - sendChatAction: typeof sendChatActionSpy; - setMessageReaction: typeof setMessageReactionSpy; - setMyCommands: typeof setMyCommandsSpy; - sendMessage: typeof sendMessageSpy; - sendAnimation: typeof sendAnimationSpy; - sendPhoto: typeof sendPhotoSpy; -}; -const apiStub: ApiStub = { - config: { use: useSpy }, - answerCallbackQuery: answerCallbackQuerySpy, - sendChatAction: sendChatActionSpy, - setMessageReaction: setMessageReactionSpy, - setMyCommands: setMyCommandsSpy, - sendMessage: sendMessageSpy, - sendAnimation: sendAnimationSpy, - sendPhoto: sendPhotoSpy, -}; - -vi.mock("grammy", () => ({ - Bot: class { - api = apiStub; - use = middlewareUseSpy; - on = onSpy; - stop = stopSpy; - command = commandSpy; - catch = vi.fn(); - constructor( - public token: string, - public options?: { client?: { fetch?: typeof fetch } }, - ) { - botCtorSpy(token, options); - } - }, - InputFile: class {}, - webhookCallback: vi.fn(), -})); - -const sequentializeMiddleware = vi.fn(); -const sequentializeSpy = vi.fn(() => sequentializeMiddleware); -let _sequentializeKey: ((ctx: unknown) => string) | undefined; -vi.mock("@grammyjs/runner", () => ({ - sequentialize: (keyFn: (ctx: unknown) => string) => { - _sequentializeKey = keyFn; - return sequentializeSpy(); - }, -})); - -const throttlerSpy = vi.fn(() => "throttler"); - -vi.mock("@grammyjs/transformer-throttler", () => ({ - apiThrottler: () => throttlerSpy(), -})); - -vi.mock("../auto-reply/reply.js", () => { - const replySpy = vi.fn(async (_ctx, opts) => { - await opts?.onReplyStart?.(); - return undefined; - }); - return { getReplyFromConfig: replySpy, __replySpy: replySpy }; -}); - -let replyModule: typeof import("../auto-reply/reply.js"); - -const getOnHandler = (event: string) => { - const handler = onSpy.mock.calls.find((call) => call[0] === event)?.[1]; - if (!handler) { - throw new Error(`Missing handler for event: ${event}`); - } - return handler as (ctx: Record) => Promise; -}; +const loadConfig = getLoadConfigMock(); describe("createTelegramBot", () => { - beforeAll(async () => { - replyModule = await import("../auto-reply/reply.js"); - }); - - beforeEach(() => { - resetInboundDedupe(); - loadConfig.mockReturnValue({ - channels: { - telegram: { dmPolicy: "open", allowFrom: ["*"] }, - }, - }); - loadWebMedia.mockReset(); - sendAnimationSpy.mockReset(); - sendPhotoSpy.mockReset(); - setMessageReactionSpy.mockReset(); - answerCallbackQuerySpy.mockReset(); - setMyCommandsSpy.mockReset(); - middlewareUseSpy.mockReset(); - sequentializeSpy.mockReset(); - botCtorSpy.mockReset(); - _sequentializeKey = undefined; - }); - // groupPolicy tests it("blocks @username allowFrom entries when groupPolicy is 'allowlist' (numeric IDs required)", async () => { onSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; replySpy.mockReset(); loadConfig.mockReturnValue({ channels: { @@ -191,7 +43,6 @@ describe("createTelegramBot", () => { }); it("allows direct messages regardless of groupPolicy", async () => { onSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; replySpy.mockReset(); loadConfig.mockReturnValue({ channels: { @@ -220,7 +71,6 @@ describe("createTelegramBot", () => { }); it("allows direct messages with tg/Telegram-prefixed allowFrom entries", async () => { onSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; replySpy.mockReset(); loadConfig.mockReturnValue({ channels: { @@ -248,7 +98,6 @@ describe("createTelegramBot", () => { }); it("allows direct messages with telegram:-prefixed allowFrom entries", async () => { onSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; replySpy.mockReset(); loadConfig.mockReturnValue({ channels: { @@ -276,7 +125,6 @@ describe("createTelegramBot", () => { }); it("matches direct message allowFrom against sender user id when chat id differs", async () => { onSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; replySpy.mockReset(); loadConfig.mockReturnValue({ channels: { @@ -304,7 +152,6 @@ describe("createTelegramBot", () => { }); it("falls back to direct message chat id when sender user id is missing", async () => { onSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; replySpy.mockReset(); loadConfig.mockReturnValue({ channels: { @@ -331,7 +178,6 @@ describe("createTelegramBot", () => { }); it("allows group messages with wildcard in allowFrom when groupPolicy is 'allowlist'", async () => { onSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; replySpy.mockReset(); loadConfig.mockReturnValue({ channels: { @@ -361,7 +207,6 @@ describe("createTelegramBot", () => { }); it("blocks group messages with no sender ID when groupPolicy is 'allowlist'", async () => { onSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; replySpy.mockReset(); loadConfig.mockReturnValue({ channels: { @@ -390,7 +235,6 @@ describe("createTelegramBot", () => { }); it("matches telegram:-prefixed allowFrom entries in group allowlist", async () => { onSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; replySpy.mockReset(); loadConfig.mockReturnValue({ channels: { diff --git a/src/telegram/bot.create-telegram-bot.routes-dms-by-telegram-accountid-binding.test.ts b/src/telegram/bot.create-telegram-bot.routes-dms-by-telegram-accountid-binding.test.ts index a6d9df88cdc..44f11894953 100644 --- a/src/telegram/bot.create-telegram-bot.routes-dms-by-telegram-accountid-binding.test.ts +++ b/src/telegram/bot.create-telegram-bot.routes-dms-by-telegram-accountid-binding.test.ts @@ -1,167 +1,23 @@ -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import { resetInboundDedupe } from "../auto-reply/reply/inbound-dedupe.js"; +import { describe, expect, it } from "vitest"; +import { + getLoadConfigMock, + getLoadWebMediaMock, + getOnHandler, + onSpy, + replySpy, + sendAnimationSpy, + sendPhotoSpy, +} from "./bot.create-telegram-bot.test-harness.js"; import { createTelegramBot } from "./bot.js"; -const { sessionStorePath } = vi.hoisted(() => ({ - sessionStorePath: `/tmp/openclaw-telegram-${Math.random().toString(16).slice(2)}.json`, -})); - -const { loadWebMedia } = vi.hoisted(() => ({ - loadWebMedia: vi.fn(), -})); - -vi.mock("../web/media.js", () => ({ - loadWebMedia, -})); - -const { loadConfig } = vi.hoisted(() => ({ - loadConfig: vi.fn(() => ({})), -})); -vi.mock("../config/config.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - loadConfig, - }; -}); - -vi.mock("../config/sessions.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - resolveStorePath: vi.fn((storePath) => storePath ?? sessionStorePath), - }; -}); - -const { readChannelAllowFromStore, upsertChannelPairingRequest } = vi.hoisted(() => ({ - readChannelAllowFromStore: vi.fn(async () => [] as string[]), - upsertChannelPairingRequest: vi.fn(async () => ({ - code: "PAIRCODE", - created: true, - })), -})); - -vi.mock("../pairing/pairing-store.js", () => ({ - readChannelAllowFromStore, - upsertChannelPairingRequest, -})); - -const useSpy = vi.fn(); -const middlewareUseSpy = vi.fn(); -const onSpy = vi.fn(); -const stopSpy = vi.fn(); -const commandSpy = vi.fn(); -const botCtorSpy = vi.fn(); -const answerCallbackQuerySpy = vi.fn(async () => undefined); -const sendChatActionSpy = vi.fn(); -const setMessageReactionSpy = vi.fn(async () => undefined); -const setMyCommandsSpy = vi.fn(async () => undefined); -const sendMessageSpy = vi.fn(async () => ({ message_id: 77 })); -const sendAnimationSpy = vi.fn(async () => ({ message_id: 78 })); -const sendPhotoSpy = vi.fn(async () => ({ message_id: 79 })); -type ApiStub = { - config: { use: (arg: unknown) => void }; - answerCallbackQuery: typeof answerCallbackQuerySpy; - sendChatAction: typeof sendChatActionSpy; - setMessageReaction: typeof setMessageReactionSpy; - setMyCommands: typeof setMyCommandsSpy; - sendMessage: typeof sendMessageSpy; - sendAnimation: typeof sendAnimationSpy; - sendPhoto: typeof sendPhotoSpy; -}; -const apiStub: ApiStub = { - config: { use: useSpy }, - answerCallbackQuery: answerCallbackQuerySpy, - sendChatAction: sendChatActionSpy, - setMessageReaction: setMessageReactionSpy, - setMyCommands: setMyCommandsSpy, - sendMessage: sendMessageSpy, - sendAnimation: sendAnimationSpy, - sendPhoto: sendPhotoSpy, -}; - -vi.mock("grammy", () => ({ - Bot: class { - api = apiStub; - use = middlewareUseSpy; - on = onSpy; - stop = stopSpy; - command = commandSpy; - catch = vi.fn(); - constructor( - public token: string, - public options?: { client?: { fetch?: typeof fetch } }, - ) { - botCtorSpy(token, options); - } - }, - InputFile: class {}, - webhookCallback: vi.fn(), -})); - -const sequentializeMiddleware = vi.fn(); -const sequentializeSpy = vi.fn(() => sequentializeMiddleware); -let _sequentializeKey: ((ctx: unknown) => string) | undefined; -vi.mock("@grammyjs/runner", () => ({ - sequentialize: (keyFn: (ctx: unknown) => string) => { - _sequentializeKey = keyFn; - return sequentializeSpy(); - }, -})); - -const throttlerSpy = vi.fn(() => "throttler"); - -vi.mock("@grammyjs/transformer-throttler", () => ({ - apiThrottler: () => throttlerSpy(), -})); - -vi.mock("../auto-reply/reply.js", () => { - const replySpy = vi.fn(async (_ctx, opts) => { - await opts?.onReplyStart?.(); - return undefined; - }); - return { getReplyFromConfig: replySpy, __replySpy: replySpy }; -}); - -let replyModule: typeof import("../auto-reply/reply.js"); - -const getOnHandler = (event: string) => { - const handler = onSpy.mock.calls.find((call) => call[0] === event)?.[1]; - if (!handler) { - throw new Error(`Missing handler for event: ${event}`); - } - return handler as (ctx: Record) => Promise; -}; +const loadConfig = getLoadConfigMock(); +const loadWebMedia = getLoadWebMediaMock(); describe("createTelegramBot", () => { - beforeAll(async () => { - replyModule = await import("../auto-reply/reply.js"); - }); - - beforeEach(() => { - resetInboundDedupe(); - loadConfig.mockReturnValue({ - channels: { - telegram: { dmPolicy: "open", allowFrom: ["*"] }, - }, - }); - loadWebMedia.mockReset(); - sendAnimationSpy.mockReset(); - sendPhotoSpy.mockReset(); - setMessageReactionSpy.mockReset(); - answerCallbackQuerySpy.mockReset(); - setMyCommandsSpy.mockReset(); - middlewareUseSpy.mockReset(); - sequentializeSpy.mockReset(); - botCtorSpy.mockReset(); - _sequentializeKey = undefined; - }); - // groupPolicy tests it("routes DMs by telegram accountId binding", async () => { onSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; replySpy.mockReset(); loadConfig.mockReturnValue({ @@ -205,7 +61,6 @@ describe("createTelegramBot", () => { }); it("allows per-group requireMention override", async () => { onSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; replySpy.mockReset(); loadConfig.mockReturnValue({ channels: { @@ -236,7 +91,6 @@ describe("createTelegramBot", () => { }); it("allows per-topic requireMention override", async () => { onSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; replySpy.mockReset(); loadConfig.mockReturnValue({ channels: { @@ -278,7 +132,6 @@ describe("createTelegramBot", () => { }); it("honors groups default when no explicit group override exists", async () => { onSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; replySpy.mockReset(); loadConfig.mockReturnValue({ channels: { @@ -306,7 +159,6 @@ describe("createTelegramBot", () => { }); it("does not block group messages when bot username is unknown", async () => { onSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; replySpy.mockReset(); loadConfig.mockReturnValue({ channels: { @@ -333,7 +185,6 @@ describe("createTelegramBot", () => { }); it("routes forum topic messages using parent group binding", async () => { onSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; replySpy.mockReset(); // Binding specifies the base group ID without topic suffix. @@ -389,7 +240,6 @@ describe("createTelegramBot", () => { it("prefers specific topic binding over parent group binding", async () => { onSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; replySpy.mockReset(); // Both a specific topic binding and a parent group binding are configured. @@ -451,7 +301,6 @@ describe("createTelegramBot", () => { it("sends GIF replies as animations", async () => { onSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; replySpy.mockReset(); replySpy.mockResolvedValueOnce({ diff --git a/src/telegram/bot.create-telegram-bot.sends-replies-without-native-reply-threading.test.ts b/src/telegram/bot.create-telegram-bot.sends-replies-without-native-reply-threading.test.ts index f36161d4b81..c2ac3b9ed5c 100644 --- a/src/telegram/bot.create-telegram-bot.sends-replies-without-native-reply-threading.test.ts +++ b/src/telegram/bot.create-telegram-bot.sends-replies-without-native-reply-threading.test.ts @@ -1,173 +1,24 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import { resetInboundDedupe } from "../auto-reply/reply/inbound-dedupe.js"; +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 { sessionStorePath } = vi.hoisted(() => ({ - sessionStorePath: `/tmp/openclaw-telegram-reply-threading-${Math.random() - .toString(16) - .slice(2)}.json`, -})); - -const { loadWebMedia } = vi.hoisted(() => ({ - loadWebMedia: vi.fn(), -})); - -vi.mock("../web/media.js", () => ({ - loadWebMedia, -})); - -const { loadConfig } = vi.hoisted(() => ({ - loadConfig: vi.fn(() => ({})), -})); -vi.mock("../config/config.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - loadConfig, - }; -}); - -vi.mock("../config/sessions.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - resolveStorePath: vi.fn((storePath) => storePath ?? sessionStorePath), - }; -}); - -const { readChannelAllowFromStore, upsertChannelPairingRequest } = vi.hoisted(() => ({ - readChannelAllowFromStore: vi.fn(async () => [] as string[]), - upsertChannelPairingRequest: vi.fn(async () => ({ - code: "PAIRCODE", - created: true, - })), -})); - -vi.mock("../pairing/pairing-store.js", () => ({ - readChannelAllowFromStore, - upsertChannelPairingRequest, -})); - -const useSpy = vi.fn(); -const middlewareUseSpy = vi.fn(); -const onSpy = vi.fn(); -const stopSpy = vi.fn(); -const commandSpy = vi.fn(); -const botCtorSpy = vi.fn(); -const answerCallbackQuerySpy = vi.fn(async () => undefined); -const sendChatActionSpy = vi.fn(); -const setMessageReactionSpy = vi.fn(async () => undefined); -const setMyCommandsSpy = vi.fn(async () => undefined); -const sendMessageSpy = vi.fn(async () => ({ message_id: 77 })); -const sendAnimationSpy = vi.fn(async () => ({ message_id: 78 })); -const sendPhotoSpy = vi.fn(async () => ({ message_id: 79 })); -type ApiStub = { - config: { use: (arg: unknown) => void }; - answerCallbackQuery: typeof answerCallbackQuerySpy; - sendChatAction: typeof sendChatActionSpy; - setMessageReaction: typeof setMessageReactionSpy; - setMyCommands: typeof setMyCommandsSpy; - sendMessage: typeof sendMessageSpy; - sendAnimation: typeof sendAnimationSpy; - sendPhoto: typeof sendPhotoSpy; -}; -const apiStub: ApiStub = { - config: { use: useSpy }, - answerCallbackQuery: answerCallbackQuerySpy, - sendChatAction: sendChatActionSpy, - setMessageReaction: setMessageReactionSpy, - setMyCommands: setMyCommandsSpy, - sendMessage: sendMessageSpy, - sendAnimation: sendAnimationSpy, - sendPhoto: sendPhotoSpy, -}; - -vi.mock("grammy", () => ({ - Bot: class { - api = apiStub; - use = middlewareUseSpy; - on = onSpy; - stop = stopSpy; - command = commandSpy; - catch = vi.fn(); - constructor( - public token: string, - public options?: { client?: { fetch?: typeof fetch } }, - ) { - botCtorSpy(token, options); - } - }, - InputFile: class {}, - webhookCallback: vi.fn(), -})); - -const sequentializeMiddleware = vi.fn(); -const sequentializeSpy = vi.fn(() => sequentializeMiddleware); -let _sequentializeKey: ((ctx: unknown) => string) | undefined; -vi.mock("@grammyjs/runner", () => ({ - sequentialize: (keyFn: (ctx: unknown) => string) => { - _sequentializeKey = keyFn; - return sequentializeSpy(); - }, -})); - -const throttlerSpy = vi.fn(() => "throttler"); - -vi.mock("@grammyjs/transformer-throttler", () => ({ - apiThrottler: () => throttlerSpy(), -})); - -vi.mock("../auto-reply/reply.js", () => { - const replySpy = vi.fn(async (_ctx, opts) => { - await opts?.onReplyStart?.(); - return undefined; - }); - return { getReplyFromConfig: replySpy, __replySpy: replySpy }; -}); - -let replyModule: typeof import("../auto-reply/reply.js"); - -const getOnHandler = (event: string) => { - const handler = onSpy.mock.calls.find((call) => call[0] === event)?.[1]; - if (!handler) { - throw new Error(`Missing handler for event: ${event}`); - } - return handler as (ctx: Record) => Promise; -}; +const loadConfig = getLoadConfigMock(); describe("createTelegramBot", () => { - beforeAll(async () => { - replyModule = await import("../auto-reply/reply.js"); - }); - - beforeEach(() => { - resetInboundDedupe(); - loadConfig.mockReturnValue({ - channels: { - telegram: { dmPolicy: "open", allowFrom: ["*"] }, - }, - }); - loadWebMedia.mockReset(); - sendAnimationSpy.mockReset(); - sendPhotoSpy.mockReset(); - setMessageReactionSpy.mockReset(); - answerCallbackQuerySpy.mockReset(); - setMyCommandsSpy.mockReset(); - middlewareUseSpy.mockReset(); - sequentializeSpy.mockReset(); - botCtorSpy.mockReset(); - _sequentializeKey = undefined; - }); - // groupPolicy tests it("sends replies without native reply threading", async () => { onSpy.mockReset(); sendMessageSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; replySpy.mockReset(); replySpy.mockResolvedValue({ text: "a".repeat(4500) }); @@ -192,7 +43,6 @@ describe("createTelegramBot", () => { it("honors replyToMode=first for threaded replies", async () => { onSpy.mockReset(); sendMessageSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; replySpy.mockReset(); replySpy.mockResolvedValue({ text: "a".repeat(4500), @@ -222,7 +72,6 @@ describe("createTelegramBot", () => { it("prefixes final replies with responsePrefix", async () => { onSpy.mockReset(); sendMessageSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; replySpy.mockReset(); replySpy.mockResolvedValue({ text: "final reply" }); loadConfig.mockReturnValue({ @@ -250,7 +99,6 @@ describe("createTelegramBot", () => { it("honors replyToMode=all for threaded replies", async () => { onSpy.mockReset(); sendMessageSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; replySpy.mockReset(); replySpy.mockResolvedValue({ text: "a".repeat(4500), @@ -277,7 +125,6 @@ describe("createTelegramBot", () => { }); it("blocks group messages when telegram.groups is set without a wildcard", async () => { onSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; replySpy.mockReset(); loadConfig.mockReturnValue({ channels: { @@ -306,7 +153,6 @@ describe("createTelegramBot", () => { }); it("skips group messages without mention when requireMention is enabled", async () => { onSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; replySpy.mockReset(); loadConfig.mockReturnValue({ channels: { @@ -331,7 +177,6 @@ describe("createTelegramBot", () => { }); it("honors routed group activation from session store", async () => { onSpy.mockReset(); - const replySpy = replyModule.__replySpy as unknown as ReturnType; replySpy.mockReset(); const storeDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-telegram-")); const storePath = path.join(storeDir, "sessions.json"); diff --git a/src/telegram/bot.create-telegram-bot.test-harness.ts b/src/telegram/bot.create-telegram-bot.test-harness.ts new file mode 100644 index 00000000000..4643f46577e --- /dev/null +++ b/src/telegram/bot.create-telegram-bot.test-harness.ts @@ -0,0 +1,201 @@ +import { beforeEach, vi } from "vitest"; +import { resetInboundDedupe } from "../auto-reply/reply/inbound-dedupe.js"; + +const { sessionStorePath } = vi.hoisted(() => ({ + sessionStorePath: `/tmp/openclaw-telegram-${Math.random().toString(16).slice(2)}.json`, +})); + +const { loadWebMedia } = vi.hoisted(() => ({ + loadWebMedia: vi.fn(), +})); + +export function getLoadWebMediaMock() { + return loadWebMedia; +} + +vi.mock("../web/media.js", () => ({ + loadWebMedia, +})); + +const { loadConfig } = vi.hoisted(() => ({ + loadConfig: vi.fn(() => ({})), +})); + +export function getLoadConfigMock() { + return loadConfig; +} +vi.mock("../config/config.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + loadConfig, + }; +}); + +vi.mock("../config/sessions.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + resolveStorePath: vi.fn((storePath) => storePath ?? sessionStorePath), + }; +}); + +const { readChannelAllowFromStore, upsertChannelPairingRequest } = vi.hoisted(() => ({ + readChannelAllowFromStore: vi.fn(async () => [] as string[]), + upsertChannelPairingRequest: vi.fn(async () => ({ + code: "PAIRCODE", + created: true, + })), +})); + +export function getReadChannelAllowFromStoreMock() { + return readChannelAllowFromStore; +} + +export function getUpsertChannelPairingRequestMock() { + return upsertChannelPairingRequest; +} + +vi.mock("../pairing/pairing-store.js", () => ({ + readChannelAllowFromStore, + upsertChannelPairingRequest, +})); + +export const useSpy = vi.fn(); +export const middlewareUseSpy = vi.fn(); +export const onSpy = vi.fn(); +export const stopSpy = vi.fn(); +export const commandSpy = vi.fn(); +export const botCtorSpy = vi.fn(); +export const answerCallbackQuerySpy = vi.fn(async () => undefined); +export const sendChatActionSpy = vi.fn(); +export const setMessageReactionSpy = vi.fn(async () => undefined); +export const setMyCommandsSpy = vi.fn(async () => undefined); +export const deleteMyCommandsSpy = vi.fn(async () => undefined); +export const getMeSpy = vi.fn(async () => ({ + username: "openclaw_bot", + has_topics_enabled: true, +})); +export const sendMessageSpy = vi.fn(async () => ({ message_id: 77 })); +export const sendAnimationSpy = vi.fn(async () => ({ message_id: 78 })); +export const sendPhotoSpy = vi.fn(async () => ({ message_id: 79 })); + +type ApiStub = { + config: { use: (arg: unknown) => void }; + answerCallbackQuery: typeof answerCallbackQuerySpy; + sendChatAction: typeof sendChatActionSpy; + setMessageReaction: typeof setMessageReactionSpy; + setMyCommands: typeof setMyCommandsSpy; + deleteMyCommands: typeof deleteMyCommandsSpy; + getMe: typeof getMeSpy; + sendMessage: typeof sendMessageSpy; + sendAnimation: typeof sendAnimationSpy; + sendPhoto: typeof sendPhotoSpy; +}; + +const apiStub: ApiStub = { + config: { use: useSpy }, + answerCallbackQuery: answerCallbackQuerySpy, + sendChatAction: sendChatActionSpy, + setMessageReaction: setMessageReactionSpy, + setMyCommands: setMyCommandsSpy, + deleteMyCommands: deleteMyCommandsSpy, + getMe: getMeSpy, + sendMessage: sendMessageSpy, + sendAnimation: sendAnimationSpy, + sendPhoto: sendPhotoSpy, +}; + +vi.mock("grammy", () => ({ + Bot: class { + api = apiStub; + use = middlewareUseSpy; + on = onSpy; + stop = stopSpy; + command = commandSpy; + catch = vi.fn(); + constructor( + public token: string, + public options?: { client?: { fetch?: typeof fetch } }, + ) { + botCtorSpy(token, options); + } + }, + InputFile: class {}, + webhookCallback: vi.fn(), +})); + +const sequentializeMiddleware = vi.fn(); +export const sequentializeSpy = vi.fn(() => sequentializeMiddleware); +export let sequentializeKey: ((ctx: unknown) => string) | undefined; +vi.mock("@grammyjs/runner", () => ({ + sequentialize: (keyFn: (ctx: unknown) => string) => { + sequentializeKey = keyFn; + return sequentializeSpy(); + }, +})); + +export const throttlerSpy = vi.fn(() => "throttler"); + +vi.mock("@grammyjs/transformer-throttler", () => ({ + apiThrottler: () => throttlerSpy(), +})); + +export const replySpy = vi.fn(async (_ctx, opts) => { + await opts?.onReplyStart?.(); + return undefined; +}); + +vi.mock("../auto-reply/reply.js", () => ({ + getReplyFromConfig: replySpy, + __replySpy: replySpy, +})); + +export const getOnHandler = (event: string) => { + const handler = onSpy.mock.calls.find((call) => call[0] === event)?.[1]; + if (!handler) { + throw new Error(`Missing handler for event: ${event}`); + } + return handler as (ctx: Record) => Promise; +}; + +beforeEach(() => { + resetInboundDedupe(); + loadConfig.mockReturnValue({ + channels: { + telegram: { dmPolicy: "open", allowFrom: ["*"] }, + }, + }); + loadWebMedia.mockReset(); + onSpy.mockReset(); + commandSpy.mockReset(); + stopSpy.mockReset(); + useSpy.mockReset(); + + sendAnimationSpy.mockReset(); + sendAnimationSpy.mockResolvedValue({ message_id: 78 }); + sendPhotoSpy.mockReset(); + sendPhotoSpy.mockResolvedValue({ message_id: 79 }); + sendMessageSpy.mockReset(); + sendMessageSpy.mockResolvedValue({ message_id: 77 }); + + setMessageReactionSpy.mockReset(); + setMessageReactionSpy.mockResolvedValue(undefined); + answerCallbackQuerySpy.mockReset(); + answerCallbackQuerySpy.mockResolvedValue(undefined); + sendChatActionSpy.mockReset(); + sendChatActionSpy.mockResolvedValue(undefined); + setMyCommandsSpy.mockReset(); + setMyCommandsSpy.mockResolvedValue(undefined); + deleteMyCommandsSpy.mockReset(); + deleteMyCommandsSpy.mockResolvedValue(undefined); + getMeSpy.mockReset(); + getMeSpy.mockResolvedValue({ + username: "openclaw_bot", + has_topics_enabled: true, + }); + middlewareUseSpy.mockReset(); + sequentializeSpy.mockReset(); + botCtorSpy.mockReset(); + sequentializeKey = undefined; +});