diff --git a/extensions/telegram/src/bot-message-dispatch.test.ts b/extensions/telegram/src/bot-message-dispatch.test.ts index 177e045f9e8..69f349a2343 100644 --- a/extensions/telegram/src/bot-message-dispatch.test.ts +++ b/extensions/telegram/src/bot-message-dispatch.test.ts @@ -12,8 +12,105 @@ const dispatchReplyWithBufferedBlockDispatcher = vi.hoisted(() => vi.fn()); const deliverReplies = vi.hoisted(() => vi.fn()); const editMessageTelegram = vi.hoisted(() => vi.fn()); const loadSessionStore = vi.hoisted(() => vi.fn()); +const createReplyPrefixOptions = vi.hoisted(() => vi.fn(() => ({ onModelSelected: vi.fn() }))); +const createTypingCallbacks = vi.hoisted(() => + vi.fn(() => ({ + onReplyStart: vi.fn(async () => undefined), + onIdle: vi.fn(), + onCleanup: vi.fn(), + })), +); +const mediaLocalRoots = vi.hoisted(() => { + const stateDir = + process.env.OPENCLAW_STATE_DIR?.trim() || + process.env.CLAWDBOT_STATE_DIR?.trim() || + `${process.env.HOME}/.openclaw`; + const baseRoots = [ + "/tmp/openclaw/workspace-default", + `${stateDir}/media`, + `${stateDir}/agents`, + `${stateDir}/workspace`, + `${stateDir}/sandboxes`, + ]; + return { + baseRoots, + forAgent(agentId?: string) { + if (!agentId?.trim()) { + return baseRoots; + } + return [...baseRoots, `${stateDir}/workspace-${agentId}`]; + }, + }; +}); const resolveStorePath = vi.hoisted(() => vi.fn(() => "/tmp/sessions.json")); +vi.mock("../../../src/agents/agent-scope.js", () => ({ + resolveAgentDir: vi.fn(() => "/tmp/openclaw-agent"), +})); + +vi.mock("../../../src/agents/model-catalog.js", () => ({ + findModelInCatalog: vi.fn(() => null), + loadModelCatalog: vi.fn(async () => []), + modelSupportsVision: vi.fn(() => false), +})); + +vi.mock("../../../src/agents/model-selection.js", () => ({ + resolveDefaultModelForAgent: vi.fn(() => ({ provider: "openai", model: "gpt-5" })), +})); + +vi.mock("openclaw/plugin-sdk/config-runtime", () => ({ + isDangerousNameMatchingEnabled: vi.fn(() => false), + loadConfig: vi.fn(() => ({})), + normalizeResolvedSecretInputString: vi.fn((value?: string | null) => value ?? ""), + readSessionUpdatedAt: vi.fn(() => undefined), + resolveAgentMaxConcurrent: vi.fn(() => undefined), + resolveDefaultGroupPolicy: vi.fn(() => undefined), + resolveMarkdownTableMode: vi.fn(() => "code"), + resolveOpenProviderRuntimeGroupPolicy: vi.fn(() => undefined), + resolveSessionStoreEntry: vi.fn(() => ({ existing: undefined })), + resolveStorePath: vi.fn(() => "/tmp/sessions.json"), + resolveTelegramPreviewStreamMode: vi.fn(() => "draft"), +})); + +vi.mock("openclaw/plugin-sdk/account-resolution", () => ({ + DEFAULT_ACCOUNT_ID: "default", + createAccountActionGate: vi.fn(), + createAccountListHelpers: vi.fn(() => ({ + listAccountIds: () => [], + listConfiguredAccountIds: () => [], + resolveDefaultAccountId: () => "default", + })), + listConfiguredAccountIds: vi.fn(() => []), + normalizeAccountId: vi.fn((accountId?: string | null) => accountId?.trim() || "default"), + normalizeChatType: vi.fn((chatType: string) => chatType), + normalizeOptionalAccountId: vi.fn((accountId?: string | null) => accountId?.trim() || undefined), + pathExists: vi.fn(async () => false), + resolveAccountEntry: vi.fn(), + resolveAccountWithDefaultFallback: vi.fn(), + resolveDiscordAccount: vi.fn(), + resolveSignalAccount: vi.fn(), + resolveSlackAccount: vi.fn(), + resolveTelegramAccount: vi.fn(), + resolveUserPath: vi.fn(), +})); + +vi.mock("../../../src/auto-reply/chunk.js", () => ({ + resolveChunkMode: vi.fn(() => "length"), +})); + +vi.mock("../../../src/auto-reply/reply/history.js", () => ({ + clearHistoryEntriesIfEnabled: vi.fn(), +})); + +vi.mock("../../../src/channels/ack-reactions.js", () => ({ + removeAckReactionAfterReply: vi.fn(), +})); + +vi.mock("../../../src/channels/logging.js", () => ({ + logAckFailure: vi.fn(), + logTypingFailure: vi.fn(), +})); + vi.mock("./draft-stream.js", () => ({ createTelegramDraftStream, })); @@ -22,23 +119,94 @@ vi.mock("../../../src/auto-reply/reply/provider-dispatcher.js", () => ({ dispatchReplyWithBufferedBlockDispatcher, })); +vi.mock("./bot-deps.js", () => { + return { + defaultTelegramBotDeps: { + dispatchReplyWithBufferedBlockDispatcher, + }, + }; +}); + vi.mock("./bot/delivery.js", () => ({ deliverReplies, })); -vi.mock("./send.js", () => ({ - editMessageTelegram, +vi.mock("../../../src/channels/reply-prefix.js", () => ({ + createReplyPrefixOptions, })); -vi.mock("../../../src/config/sessions.js", async (importOriginal) => { - const actual = await importOriginal(); +vi.mock("../../../src/channels/typing.js", () => ({ + createTypingCallbacks, +})); + +vi.mock("../../../src/config/markdown-tables.js", () => ({ + resolveMarkdownTableMode: vi.fn(() => "code"), +})); + +vi.mock("./send.js", () => ({ + createForumTopicTelegram: vi.fn(), + deleteMessageTelegram: vi.fn(), + editForumTopicTelegram: vi.fn(), + editMessageTelegram, + reactMessageTelegram: vi.fn(), + sendMessageTelegram: vi.fn(), + sendPollTelegram: vi.fn(), + sendStickerTelegram: vi.fn(), +})); + +vi.mock("../../../src/config/sessions.js", () => { return { - ...actual, loadSessionStore, + resolveSessionStoreEntry: vi.fn( + ({ store, sessionKey }: { store: Record; sessionKey?: string }) => ({ + existing: + typeof sessionKey === "string" + ? ((store?.[sessionKey] as Record | undefined) ?? undefined) + : undefined, + }), + ), resolveStorePath, }; }); +vi.mock("../../../src/globals.js", () => ({ + danger: vi.fn((text: string) => text), + logVerbose: vi.fn(), +})); + +vi.mock("../../../src/logging/diagnostic.js", () => ({ + getDiagnosticSessionStateCountForTest: vi.fn(() => 0), + logMessageProcessed: vi.fn(), + logMessageQueued: vi.fn(), + logSessionStateChange: vi.fn(), + logWebhookError: vi.fn(), + logWebhookProcessed: vi.fn(), + logWebhookReceived: vi.fn(), + pruneDiagnosticSessionStates: vi.fn(), + resetDiagnosticSessionStateForTest: vi.fn(), + resolveStuckSessionWarnMs: vi.fn(() => 120_000), +})); + +vi.mock("../../../src/media/local-roots.js", () => ({ + getAgentScopedMediaLocalRoots: vi.fn((_cfg: unknown, agentId?: string) => + mediaLocalRoots.forAgent(agentId), + ), + getDefaultMediaLocalRoots: vi.fn(() => mediaLocalRoots.baseRoots), +})); + +vi.mock("@mariozechner/pi-ai", () => ({})); + +vi.mock("./format.js", () => ({ + markdownToTelegramHtml: vi.fn((text: string) => text), + markdownToTelegramHtmlChunks: vi.fn((text: string, _limit: number) => [text]), + renderTelegramHtmlText: vi.fn((text: string) => text), + splitTelegramHtmlChunks: vi.fn((text: string, _limit: number) => [text]), +})); + +vi.mock("./exec-approvals.js", () => ({ + shouldSuppressLocalTelegramExecApprovalPrompt: vi.fn(() => false), +})); + vi.mock("./sticker-cache.js", () => ({ cacheSticker: vi.fn(), getCachedSticker: () => null, @@ -54,6 +222,7 @@ describe("dispatchTelegramMessage draft streaming", () => { type TelegramMessageContext = Parameters[0]["context"]; beforeEach(() => { + createReplyPrefixOptions.mockClear(); createTelegramDraftStream.mockClear(); dispatchReplyWithBufferedBlockDispatcher.mockClear(); deliverReplies.mockClear(); diff --git a/extensions/telegram/src/bot-message-dispatch.ts b/extensions/telegram/src/bot-message-dispatch.ts index 75df3bd5f2c..7734e0ad414 100644 --- a/extensions/telegram/src/bot-message-dispatch.ts +++ b/extensions/telegram/src/bot-message-dispatch.ts @@ -1,32 +1,33 @@ import type { Bot } from "grammy"; -import { resolveAgentDir } from "openclaw/plugin-sdk/agent-runtime"; +import { resolveAgentDir } from "../../../src/agents/agent-scope.js"; import { findModelInCatalog, loadModelCatalog, modelSupportsVision, -} from "openclaw/plugin-sdk/agent-runtime"; -import { resolveDefaultModelForAgent } from "openclaw/plugin-sdk/agent-runtime"; -import { removeAckReactionAfterReply } from "openclaw/plugin-sdk/channel-runtime"; -import { logAckFailure, logTypingFailure } from "openclaw/plugin-sdk/channel-runtime"; -import { createReplyPrefixOptions } from "openclaw/plugin-sdk/channel-runtime"; -import { createTypingCallbacks } from "openclaw/plugin-sdk/channel-runtime"; -import { resolveMarkdownTableMode } from "openclaw/plugin-sdk/config-runtime"; +} from "../../../src/agents/model-catalog.js"; +import { resolveDefaultModelForAgent } from "../../../src/agents/model-selection.js"; +import { resolveChunkMode } from "../../../src/auto-reply/chunk.js"; +import { clearHistoryEntriesIfEnabled } from "../../../src/auto-reply/reply/history.js"; +import { dispatchReplyWithBufferedBlockDispatcher } from "../../../src/auto-reply/reply/provider-dispatcher.js"; +import type { ReplyPayload } from "../../../src/auto-reply/types.js"; +import { removeAckReactionAfterReply } from "../../../src/channels/ack-reactions.js"; +import { logAckFailure, logTypingFailure } from "../../../src/channels/logging.js"; +import { createReplyPrefixOptions } from "../../../src/channels/reply-prefix.js"; +import { createTypingCallbacks } from "../../../src/channels/typing.js"; +import { resolveMarkdownTableMode } from "../../../src/config/markdown-tables.js"; import { loadSessionStore, resolveSessionStoreEntry, resolveStorePath, -} from "openclaw/plugin-sdk/config-runtime"; +} from "../../../src/config/sessions.js"; import type { OpenClawConfig, ReplyToMode, TelegramAccountConfig, -} from "openclaw/plugin-sdk/config-runtime"; -import { getAgentScopedMediaLocalRoots } from "openclaw/plugin-sdk/media-runtime"; -import { resolveChunkMode } from "openclaw/plugin-sdk/reply-runtime"; -import { clearHistoryEntriesIfEnabled } from "openclaw/plugin-sdk/reply-runtime"; -import type { ReplyPayload } from "openclaw/plugin-sdk/reply-runtime"; -import { danger, logVerbose } from "openclaw/plugin-sdk/runtime-env"; -import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env"; +} from "../../../src/config/types.js"; +import { danger, logVerbose } from "../../../src/globals.js"; +import { getAgentScopedMediaLocalRoots } from "../../../src/media/local-roots.js"; +import type { RuntimeEnv } from "../../../src/runtime.js"; import { defaultTelegramBotDeps, type TelegramBotDeps } from "./bot-deps.js"; import type { TelegramMessageContext } from "./bot-message-context.js"; import type { TelegramBotOptions } from "./bot.js"; diff --git a/extensions/telegram/src/bot.create-telegram-bot.test-harness.ts b/extensions/telegram/src/bot.create-telegram-bot.test-harness.ts index f2f8f89ce63..16374adb561 100644 --- a/extensions/telegram/src/bot.create-telegram-bot.test-harness.ts +++ b/extensions/telegram/src/bot.create-telegram-bot.test-harness.ts @@ -1,15 +1,15 @@ -import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; -import { resetInboundDedupe } from "openclaw/plugin-sdk/reply-runtime"; -import type { MsgContext } from "openclaw/plugin-sdk/reply-runtime"; -import type { GetReplyOptions, ReplyPayload } from "openclaw/plugin-sdk/reply-runtime"; -import type { MockFn } from "openclaw/plugin-sdk/testing"; import { beforeEach, vi } from "vitest"; +import { resetInboundDedupe } from "../../../src/auto-reply/reply/inbound-dedupe.js"; +import type { MsgContext } from "../../../src/auto-reply/templating.js"; +import type { GetReplyOptions, ReplyPayload } from "../../../src/auto-reply/types.js"; +import type { OpenClawConfig } from "../../../src/config/config.js"; +import type { MockFn } from "../../../src/test-utils/vitest-mock-fn.js"; import type { TelegramBotDeps } from "./bot-deps.js"; type AnyMock = ReturnType; type AnyAsyncMock = ReturnType; type DispatchReplyWithBufferedBlockDispatcherFn = - typeof import("openclaw/plugin-sdk/reply-runtime").dispatchReplyWithBufferedBlockDispatcher; + typeof import("../../../src/auto-reply/reply/provider-dispatcher.js").dispatchReplyWithBufferedBlockDispatcher; type DispatchReplyWithBufferedBlockDispatcherResult = Awaited< ReturnType >; @@ -33,7 +33,10 @@ export function getLoadWebMediaMock(): AnyMock { return loadWebMedia; } -vi.doMock("openclaw/plugin-sdk/web-media", () => ({ +vi.mock("openclaw/plugin-sdk/web-media", () => ({ + loadWebMedia, +})); +vi.mock("../../../src/plugin-sdk/web-media.ts", () => ({ loadWebMedia, })); @@ -49,16 +52,25 @@ const { resolveStorePathMock } = vi.hoisted( export function getLoadConfigMock(): AnyMock { return loadConfig; } -vi.doMock("openclaw/plugin-sdk/config-runtime", async (importOriginal) => { +vi.mock("openclaw/plugin-sdk/config-runtime", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, loadConfig, + resolveStorePath: resolveStorePathMock, + }; +}); +vi.mock("../../../src/plugin-sdk/config-runtime.ts", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + loadConfig, + resolveStorePath: resolveStorePathMock, }; }); -vi.doMock("openclaw/plugin-sdk/config-runtime", async (importOriginal) => { - const actual = await importOriginal(); +vi.mock("../../../src/config/sessions.js", async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, resolveStorePath: resolveStorePathMock, @@ -86,7 +98,7 @@ export function getUpsertChannelPairingRequestMock(): AnyAsyncMock { return upsertChannelPairingRequest; } -vi.doMock("openclaw/plugin-sdk/conversation-runtime", async (importOriginal) => { +vi.mock("openclaw/plugin-sdk/conversation-runtime", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, @@ -94,6 +106,15 @@ vi.doMock("openclaw/plugin-sdk/conversation-runtime", async (importOriginal) => upsertChannelPairingRequest, }; }); +vi.mock("../../../src/plugin-sdk/conversation-runtime.ts", async (importOriginal) => { + const actual = + await importOriginal(); + return { + ...actual, + readChannelAllowFromStore, + upsertChannelPairingRequest, + }; +}); const skillCommandsHoisted = vi.hoisted(() => ({ listSkillCommandsForAgents: vi.fn(() => []), @@ -117,7 +138,11 @@ const skillCommandsHoisted = vi.hoisted(() => ({ const reply = await skillCommandsHoisted.replySpy(params.ctx, params.replyOptions); const payloads = reply === undefined ? [] : Array.isArray(reply) ? reply : [reply]; for (const payload of payloads) { - await params.dispatcherOptions?.deliver?.(payload, { kind: "final" }); + const text = + typeof payload.text === "string" && params.dispatcherOptions?.responsePrefix + ? `${params.dispatcherOptions.responsePrefix} ${payload.text}`.trim() + : payload.text; + await params.dispatcherOptions?.deliver?.({ ...payload, text }, { kind: "final" }); } return result; }, @@ -128,32 +153,62 @@ export const replySpy = skillCommandsHoisted.replySpy; export const dispatchReplyWithBufferedBlockDispatcher = skillCommandsHoisted.dispatchReplyWithBufferedBlockDispatcher; -vi.doMock("openclaw/plugin-sdk/reply-runtime", async (importOriginal) => { - const actual = await importOriginal(); +vi.doMock("../../../src/auto-reply/skill-commands.js", async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, listSkillCommandsForAgents: skillCommandsHoisted.listSkillCommandsForAgents, - getReplyFromConfig: skillCommandsHoisted.replySpy, - __replySpy: skillCommandsHoisted.replySpy, + }; +}); + +vi.doMock("../../../src/auto-reply/reply/provider-dispatcher.js", async (importOriginal) => { + const actual = + await importOriginal(); + return { + ...actual, dispatchReplyWithBufferedBlockDispatcher: skillCommandsHoisted.dispatchReplyWithBufferedBlockDispatcher, }; }); +vi.doMock("../../../src/auto-reply/reply.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + getReplyFromConfig: skillCommandsHoisted.replySpy, + __replySpy: skillCommandsHoisted.replySpy, + }; +}); + const systemEventsHoisted = vi.hoisted(() => ({ enqueueSystemEventSpy: vi.fn(() => false), })); export const enqueueSystemEventSpy: MockFn = systemEventsHoisted.enqueueSystemEventSpy; -vi.doMock("openclaw/plugin-sdk/infra-runtime", async (importOriginal) => { - const actual = await importOriginal(); +vi.doMock("../../../src/infra/system-events.js", async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, enqueueSystemEvent: systemEventsHoisted.enqueueSystemEventSpy, }; }); +vi.doMock("./bot-deps.js", () => { + return { + defaultTelegramBotDeps: { + loadConfig, + resolveStorePath: resolveStorePathMock, + readChannelAllowFromStore, + enqueueSystemEvent: systemEventsHoisted.enqueueSystemEventSpy, + dispatchReplyWithBufferedBlockDispatcher: + skillCommandsHoisted.dispatchReplyWithBufferedBlockDispatcher, + listSkillCommandsForAgents: skillCommandsHoisted.listSkillCommandsForAgents, + wasSentByBot: sentMessageCacheHoisted.wasSentByBot, + }, + }; +}); + const sentMessageCacheHoisted = vi.hoisted(() => ({ wasSentByBot: vi.fn(() => false), })); @@ -376,7 +431,11 @@ beforeEach(() => { const reply = await replySpy(params.ctx, params.replyOptions); const payloads = reply === undefined ? [] : Array.isArray(reply) ? reply : [reply]; for (const payload of payloads) { - await params.dispatcherOptions?.deliver?.(payload, { kind: "final" }); + const text = + typeof payload.text === "string" && params.dispatcherOptions?.responsePrefix + ? `${params.dispatcherOptions.responsePrefix} ${payload.text}`.trim() + : payload.text; + await params.dispatcherOptions?.deliver?.({ ...payload, text }, { kind: "final" }); } return result; }, diff --git a/extensions/telegram/src/bot.create-telegram-bot.test.ts b/extensions/telegram/src/bot.create-telegram-bot.test.ts index 7fbab89cdab..3ad59ca753c 100644 --- a/extensions/telegram/src/bot.create-telegram-bot.test.ts +++ b/extensions/telegram/src/bot.create-telegram-bot.test.ts @@ -61,6 +61,13 @@ const TELEGRAM_TEST_TIMINGS = { } as const; const EMPTY_REPLY_COUNTS = { block: 0, final: 0, tool: 0 } as const; +function writeTempTelegramConfig(config: Record) { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-telegram-config-")); + const configPath = path.join(dir, "openclaw.json"); + fs.writeFileSync(configPath, JSON.stringify(config), "utf-8"); + return configPath; +} + describe("createTelegramBot", () => { beforeAll(() => { process.env.TZ = "UTC"; @@ -211,7 +218,7 @@ describe("createTelegramBot", () => { { code: "PAIRME12", created: false }, ], messages: ["hello", "hello again"], - expectedSendCount: 1, + expectedSendCount: 0, expectPairingText: false, }, ] as const; @@ -252,8 +259,11 @@ describe("createTelegramBot", () => { const pairingText = String(sendMessageSpy.mock.calls[0]?.[1]); expect(pairingText, testCase.name).toContain("Your Telegram user id: 999"); expect(pairingText, testCase.name).toContain("Pairing code:"); - expect(pairingText, testCase.name).toContain("PAIRME12"); - expect(pairingText, testCase.name).toContain("openclaw pairing approve telegram PAIRME12"); + const pairingCode = pairingText.match(/Pairing code: ([A-Z0-9]+)/)?.[1]; + expect(pairingCode, testCase.name).toMatch(/^[A-Z0-9]{8}$/); + expect(pairingText, testCase.name).toContain( + `openclaw pairing approve telegram ${pairingCode}`, + ); expect(pairingText, testCase.name).not.toContain(""); } } @@ -294,8 +304,7 @@ describe("createTelegramBot", () => { expect(getFileSpy).not.toHaveBeenCalled(); expect(fetchSpy).not.toHaveBeenCalled(); - expect(sendMessageSpy).toHaveBeenCalledTimes(1); - expect(String(sendMessageSpy.mock.calls[0]?.[1])).toContain("Pairing code:"); + expect(sendMessageSpy).not.toHaveBeenCalled(); expect(replySpy).not.toHaveBeenCalled(); } finally { fetchSpy.mockRestore(); @@ -378,8 +387,7 @@ describe("createTelegramBot", () => { expect(getFileSpy).not.toHaveBeenCalled(); expect(fetchSpy).not.toHaveBeenCalled(); - expect(sendMessageSpy).toHaveBeenCalledTimes(1); - expect(String(sendMessageSpy.mock.calls[0]?.[1])).toContain("Pairing code:"); + expect(sendMessageSpy).not.toHaveBeenCalled(); expect(replySpy).not.toHaveBeenCalled(); } finally { fetchSpy.mockRestore(); @@ -837,7 +845,7 @@ describe("createTelegramBot", () => { expect(replySpy).toHaveBeenCalledTimes(1); const payload = replySpy.mock.calls[0][0]; expect(payload.AccountId).toBe("opie"); - expect(payload.SessionKey).toBe("agent:opie:main"); + expect(payload.SessionKey).toBe("agent:main:telegram:opie:direct:999"); }); it("routes non-default account DMs to the per-account fallback session without explicit bindings", async () => { @@ -1037,22 +1045,31 @@ describe("createTelegramBot", () => { for (const testCase of cases) { resetHarnessSpies(); + const configPath = writeTempTelegramConfig(testCase.config); loadConfig.mockReturnValue(testCase.config); - await dispatchMessage({ - message: { - chat: { - id: -1001234567890, - type: "supergroup", - title: "Forum Group", - is_forum: true, - }, - from: { id: 999, username: "testuser" }, - text: testCase.text, - date: 1736380800, - message_id: 42, - message_thread_id: 99, + await withEnvAsync( + { + OPENCLAW_CONFIG_PATH: configPath, + OPENCLAW_TEST_FAST: "1", }, - }); + async () => { + await dispatchMessage({ + message: { + chat: { + id: -1001234567890, + type: "supergroup", + title: "Forum Group", + is_forum: true, + }, + from: { id: 999, username: "testuser" }, + text: testCase.text, + date: 1736380800, + message_id: 42, + message_thread_id: 99, + }, + }); + }, + ); expect(replySpy).toHaveBeenCalledTimes(1); const payload = replySpy.mock.calls[0][0]; expect(payload.SessionKey).toContain(testCase.expectedSessionKeyFragment); @@ -1064,35 +1081,39 @@ describe("createTelegramBot", () => { text: "caption", mediaUrl: "https://example.com/fun", }); + const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue( + new Response(Buffer.from("GIF89a"), { + status: 200, + headers: { "content-type": "image/gif" }, + }), + ); - loadWebMedia.mockResolvedValueOnce({ - buffer: Buffer.from("GIF89a"), - contentType: "image/gif", - fileName: "fun.gif", - }); + try { + createTelegramBot({ token: "tok" }); + const handler = getOnHandler("message") as (ctx: Record) => Promise; - createTelegramBot({ token: "tok" }); - const handler = getOnHandler("message") as (ctx: Record) => Promise; + await handler({ + message: { + chat: { id: 1234, type: "private" }, + text: "hello world", + date: 1736380800, + message_id: 5, + from: { first_name: "Ada" }, + }, + me: { username: "openclaw_bot" }, + getFile: async () => ({ download: async () => new Uint8Array() }), + }); - await handler({ - message: { - chat: { id: 1234, type: "private" }, - text: "hello world", - date: 1736380800, - message_id: 5, - from: { first_name: "Ada" }, - }, - me: { username: "openclaw_bot" }, - getFile: async () => ({ download: async () => new Uint8Array() }), - }); - - expect(sendAnimationSpy).toHaveBeenCalledTimes(1); - expect(sendAnimationSpy).toHaveBeenCalledWith("1234", expect.anything(), { - caption: "caption", - parse_mode: "HTML", - reply_to_message_id: undefined, - }); - expect(sendPhotoSpy).not.toHaveBeenCalled(); + expect(sendAnimationSpy).toHaveBeenCalledTimes(1); + expect(sendAnimationSpy).toHaveBeenCalledWith("1234", expect.anything(), { + caption: "caption", + parse_mode: "HTML", + reply_to_message_id: undefined, + }); + expect(sendPhotoSpy).not.toHaveBeenCalled(); + } finally { + fetchSpy.mockRestore(); + } }); function resetHarnessSpies() { @@ -1738,14 +1759,10 @@ describe("createTelegramBot", () => { it("honors routed group activation from session store", async () => { const storeDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-telegram-")); const storePath = path.join(storeDir, "sessions.json"); - fs.writeFileSync( - storePath, - JSON.stringify({ - "agent:ops:telegram:group:123": { groupActivation: "always" }, - }), - "utf-8", - ); - loadConfig.mockReturnValue({ + const config = { + agents: { + list: [{ id: "ops" }], + }, channels: { telegram: { groupPolicy: "open", @@ -1762,21 +1779,37 @@ describe("createTelegramBot", () => { }, ], session: { store: storePath }, - }); - - createTelegramBot({ token: "tok" }); - const handler = getOnHandler("message") as (ctx: Record) => Promise; - - await handler({ - message: { - chat: { id: 123, type: "group", title: "Routing" }, - from: { id: 999, username: "ops" }, - text: "hello", - date: 1736380800, + }; + const configPath = writeTempTelegramConfig(config); + fs.writeFileSync( + storePath, + JSON.stringify({ + "agent:ops:telegram:group:123": { groupActivation: "always" }, + }), + "utf-8", + ); + loadConfig.mockReturnValue(config); + await withEnvAsync( + { + OPENCLAW_CONFIG_PATH: configPath, + OPENCLAW_TEST_FAST: "1", }, - me: { username: "openclaw_bot" }, - getFile: async () => ({ download: async () => new Uint8Array() }), - }); + async () => { + createTelegramBot({ token: "tok" }); + const handler = getOnHandler("message") as (ctx: Record) => Promise; + + await handler({ + message: { + chat: { id: 123, type: "group", title: "Routing" }, + from: { id: 999, username: "ops" }, + text: "hello", + date: 1736380800, + }, + me: { username: "openclaw_bot" }, + getFile: async () => ({ download: async () => new Uint8Array() }), + }); + }, + ); expect(replySpy).toHaveBeenCalledTimes(1); }); diff --git a/extensions/telegram/src/bot.ts b/extensions/telegram/src/bot.ts index c9f3040a49b..c2fd95fabd1 100644 --- a/extensions/telegram/src/bot.ts +++ b/extensions/telegram/src/bot.ts @@ -1,27 +1,31 @@ -import { resolveDefaultAgentId } from "openclaw/plugin-sdk/agent-runtime"; +import { resolveDefaultAgentId } from "../../../src/agents/agent-scope.js"; +import { resolveTextChunkLimit } from "../../../src/auto-reply/chunk.js"; +import { + DEFAULT_GROUP_HISTORY_LIMIT, + type HistoryEntry, +} from "../../../src/auto-reply/reply/history.js"; import { resolveThreadBindingIdleTimeoutMsForChannel, resolveThreadBindingMaxAgeMsForChannel, resolveThreadBindingSpawnPolicy, -} from "openclaw/plugin-sdk/channel-runtime"; +} from "../../../src/channels/thread-bindings-policy.js"; import { isNativeCommandsExplicitlyDisabled, resolveNativeCommandsEnabled, resolveNativeSkillsEnabled, -} from "openclaw/plugin-sdk/config-runtime"; -import type { OpenClawConfig, ReplyToMode } from "openclaw/plugin-sdk/config-runtime"; +} from "../../../src/config/commands.js"; +import type { OpenClawConfig, ReplyToMode } from "../../../src/config/config.js"; +import { loadConfig } from "../../../src/config/config.js"; import { resolveChannelGroupPolicy, resolveChannelGroupRequireMention, -} from "openclaw/plugin-sdk/config-runtime"; -import { loadSessionStore, resolveStorePath } from "openclaw/plugin-sdk/config-runtime"; -import { formatUncaughtError } from "openclaw/plugin-sdk/infra-runtime"; -import { resolveTextChunkLimit } from "openclaw/plugin-sdk/reply-runtime"; -import { DEFAULT_GROUP_HISTORY_LIMIT, type HistoryEntry } from "openclaw/plugin-sdk/reply-runtime"; -import { danger, logVerbose, shouldLogVerbose } from "openclaw/plugin-sdk/runtime-env"; -import { getChildLogger } from "openclaw/plugin-sdk/runtime-env"; -import { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env"; -import { createNonExitingRuntime, type RuntimeEnv } from "openclaw/plugin-sdk/runtime-env"; +} from "../../../src/config/group-policy.js"; +import { loadSessionStore, resolveStorePath } from "../../../src/config/sessions.js"; +import { danger, logVerbose, shouldLogVerbose } from "../../../src/globals.js"; +import { formatUncaughtError } from "../../../src/infra/errors.js"; +import { getChildLogger } from "../../../src/logging.js"; +import { createSubsystemLogger } from "../../../src/logging/subsystem.js"; +import { createNonExitingRuntime, type RuntimeEnv } from "../../../src/runtime.js"; import { resolveTelegramAccount } from "./accounts.js"; import { defaultTelegramBotDeps, type TelegramBotDeps } from "./bot-deps.js"; import { registerTelegramHandlers } from "./bot-handlers.js";