diff --git a/extensions/telegram/src/channel.ts b/extensions/telegram/src/channel.ts index f0cdfca63717..d222facee1b8 100644 --- a/extensions/telegram/src/channel.ts +++ b/extensions/telegram/src/channel.ts @@ -90,6 +90,7 @@ import { createTelegramPluginBase, findTelegramTokenOwnerAccountId, formatDuplicateTelegramTokenReason, + resolveTelegramConfigAccessorAccount, telegramConfigAdapter, } from "./shared.js"; import { withTelegramStartupProbeSlot } from "./startup-probe-limiter.js"; @@ -1246,7 +1247,8 @@ export const telegramPlugin = createChatChannelPlugin({ }, security: telegramSecurityAdapter, threading: { - topLevelReplyToMode: "telegram", + resolveReplyToMode: ({ cfg, accountId }) => + resolveTelegramConfigAccessorAccount({ cfg, accountId }).config.replyToMode ?? "off", buildToolContext: (params) => buildTelegramThreadingToolContext(params), resolveAutoThreadId: ({ to, toolContext }) => resolveTelegramAutoThreadId({ to, toolContext }), resolveCurrentChannelId: ({ to, threadId }) => { diff --git a/extensions/telegram/src/shared.ts b/extensions/telegram/src/shared.ts index 4c3e8ddf6a88..9ce58169ca7e 100644 --- a/extensions/telegram/src/shared.ts +++ b/extensions/telegram/src/shared.ts @@ -104,7 +104,7 @@ function isBlockedByMultiBotGuard(cfg: OpenClawConfig, accountId: string): boole return !resolveNormalizedAccountEntry(accounts, accountId, normalizeAccountId); } -function resolveTelegramConfigAccessorAccount(params: { +export function resolveTelegramConfigAccessorAccount(params: { cfg: OpenClawConfig; accountId?: string | null; }): TelegramConfigAccessorAccount { diff --git a/extensions/telegram/src/threading-tool-context.test.ts b/extensions/telegram/src/threading-tool-context.test.ts index f3169347c698..d0f6b01411ca 100644 --- a/extensions/telegram/src/threading-tool-context.test.ts +++ b/extensions/telegram/src/threading-tool-context.test.ts @@ -1,8 +1,67 @@ // Telegram tests cover threading tool context plugin behavior. import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; -import { describe, expect, it } from "vitest"; +import { describe, expect, it, vi } from "vitest"; +import { telegramPlugin } from "./channel.js"; import { buildTelegramThreadingToolContext } from "./threading-tool-context.js"; +const tryReadSecretFileSyncMock = vi.hoisted(() => + vi.fn(() => { + throw new Error("reply mode must not read Telegram credentials"); + }), +); + +vi.mock("openclaw/plugin-sdk/secret-file-runtime", async (importOriginal) => ({ + ...(await importOriginal()), + tryReadSecretFileSync: tryReadSecretFileSyncMock, +})); + +describe("telegramPlugin reply threading", () => { + it.each([ + { + name: "uses an account override", + telegram: { + accounts: { + sut: { + tokenFile: "/tmp/openclaw-telegram-reply-mode-must-not-read", + replyToMode: "first" as const, + }, + }, + }, + expected: "first", + }, + { + name: "inherits the top-level mode", + telegram: { + replyToMode: "all" as const, + accounts: { + sut: { + botToken: { source: "file", provider: "telegram_token", id: "value" }, + }, + }, + }, + expected: "all", + }, + { + name: "allows an account to disable replies", + telegram: { + replyToMode: "all" as const, + accounts: { sut: { replyToMode: "off" as const } }, + }, + expected: "off", + }, + ])("$name", ({ telegram, expected }) => { + tryReadSecretFileSyncMock.mockClear(); + const resolveReplyToMode = telegramPlugin.threading?.resolveReplyToMode; + if (!resolveReplyToMode) { + throw new Error("Telegram reply mode resolver is unavailable"); + } + + const cfg = { channels: { telegram } } as unknown as OpenClawConfig; + expect(resolveReplyToMode({ cfg, accountId: "sut" })).toBe(expected); + expect(tryReadSecretFileSyncMock).not.toHaveBeenCalled(); + }); +}); + describe("buildTelegramThreadingToolContext", () => { it("keeps topic thread state in plugin-owned tool context", () => { const hasRepliedRef = { value: false };