diff --git a/extensions/telegram/src/bot-message-context.audio-transcript.test-support.ts b/extensions/telegram/src/bot-message-context.audio-transcript.test-support.ts new file mode 100644 index 00000000000..ef0d3503823 --- /dev/null +++ b/extensions/telegram/src/bot-message-context.audio-transcript.test-support.ts @@ -0,0 +1,159 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const transcribeFirstAudioMock = vi.fn(); +const DEFAULT_MODEL = "anthropic/claude-opus-4-5"; +const DEFAULT_WORKSPACE = "/tmp/openclaw"; +const DEFAULT_MENTION_PATTERN = "\\bbot\\b"; + +vi.mock("./media-understanding.runtime.js", () => ({ + transcribeFirstAudio: (...args: unknown[]) => transcribeFirstAudioMock(...args), +})); + +const { buildTelegramMessageContextForTest } = + await import("./bot-message-context.test-harness.js"); + +async function buildGroupVoiceContext(params: { + messageId: number; + chatId: number; + title: string; + date: number; + fromId: number; + firstName: string; + fileId: string; + mediaPath: string; + groupDisableAudioPreflight?: boolean; + topicDisableAudioPreflight?: boolean; +}) { + const groupConfig = { + requireMention: true, + ...(params.groupDisableAudioPreflight === undefined + ? {} + : { disableAudioPreflight: params.groupDisableAudioPreflight }), + }; + const topicConfig = + params.topicDisableAudioPreflight === undefined + ? undefined + : { disableAudioPreflight: params.topicDisableAudioPreflight }; + + return buildTelegramMessageContextForTest({ + message: { + message_id: params.messageId, + chat: { id: params.chatId, type: "supergroup", title: params.title }, + date: params.date, + text: undefined, + from: { id: params.fromId, first_name: params.firstName }, + voice: { file_id: params.fileId }, + }, + allMedia: [{ path: params.mediaPath, contentType: "audio/ogg" }], + options: { forceWasMentioned: true }, + cfg: { + agents: { defaults: { model: DEFAULT_MODEL, workspace: DEFAULT_WORKSPACE } }, + channels: { telegram: {} }, + messages: { groupChat: { mentionPatterns: [DEFAULT_MENTION_PATTERN] } }, + }, + resolveGroupActivation: () => true, + resolveGroupRequireMention: () => true, + resolveTelegramGroupConfig: () => ({ + groupConfig, + topicConfig, + }), + }); +} + +function expectTranscriptRendered( + ctx: Awaited>, + transcript: string, +) { + expect(ctx).not.toBeNull(); + expect(ctx?.ctxPayload?.BodyForAgent).toBe(transcript); + expect(ctx?.ctxPayload?.Body).toContain(transcript); + expect(ctx?.ctxPayload?.Body).not.toContain(""); +} + +function expectAudioPlaceholderRendered(ctx: Awaited>) { + expect(ctx).not.toBeNull(); + expect(ctx?.ctxPayload?.Body).toContain(""); +} + +describe("buildTelegramMessageContext audio transcript body", () => { + beforeEach(() => { + transcribeFirstAudioMock.mockReset(); + }); + + it("uses preflight transcript as BodyForAgent for mention-gated group voice messages", async () => { + transcribeFirstAudioMock.mockResolvedValueOnce("hey bot please help"); + + const ctx = await buildGroupVoiceContext({ + messageId: 1, + chatId: -1001234567890, + title: "Test Group", + date: 1700000000, + fromId: 42, + firstName: "Alice", + fileId: "voice-1", + mediaPath: "/tmp/voice.ogg", + }); + + expect(transcribeFirstAudioMock).toHaveBeenCalledTimes(1); + expectTranscriptRendered(ctx, "hey bot please help"); + }); + + it("skips preflight transcription when disableAudioPreflight is true", async () => { + transcribeFirstAudioMock.mockClear(); + + const ctx = await buildGroupVoiceContext({ + messageId: 2, + chatId: -1001234567891, + title: "Test Group 2", + date: 1700000100, + fromId: 43, + firstName: "Bob", + fileId: "voice-2", + mediaPath: "/tmp/voice2.ogg", + groupDisableAudioPreflight: true, + }); + + expect(transcribeFirstAudioMock).not.toHaveBeenCalled(); + expectAudioPlaceholderRendered(ctx); + }); + + it("uses topic disableAudioPreflight=false to override group disableAudioPreflight=true", async () => { + transcribeFirstAudioMock.mockResolvedValueOnce("topic override transcript"); + + const ctx = await buildGroupVoiceContext({ + messageId: 3, + chatId: -1001234567892, + title: "Test Group 3", + date: 1700000200, + fromId: 44, + firstName: "Cara", + fileId: "voice-3", + mediaPath: "/tmp/voice3.ogg", + groupDisableAudioPreflight: true, + topicDisableAudioPreflight: false, + }); + + expect(transcribeFirstAudioMock).toHaveBeenCalledTimes(1); + expectTranscriptRendered(ctx, "topic override transcript"); + }); + + it("uses topic disableAudioPreflight=true to override group disableAudioPreflight=false", async () => { + transcribeFirstAudioMock.mockClear(); + + const ctx = await buildGroupVoiceContext({ + messageId: 4, + chatId: -1001234567893, + title: "Test Group 4", + date: 1700000300, + fromId: 45, + firstName: "Dan", + fileId: "voice-4", + mediaPath: "/tmp/voice4.ogg", + groupDisableAudioPreflight: false, + topicDisableAudioPreflight: true, + }); + + expect(transcribeFirstAudioMock).not.toHaveBeenCalled(); + expectAudioPlaceholderRendered(ctx); + }); +}); diff --git a/extensions/telegram/src/bot-message-context.dm-session.test.ts b/extensions/telegram/src/bot-message-context.dm-session.test.ts new file mode 100644 index 00000000000..f109779d20f --- /dev/null +++ b/extensions/telegram/src/bot-message-context.dm-session.test.ts @@ -0,0 +1,2 @@ +import "./bot-message-context.named-account-dm.test-support.js"; +import "./bot-message-context.session-recreate.test-support.js"; diff --git a/extensions/telegram/src/bot-message-context.group-body.test.ts b/extensions/telegram/src/bot-message-context.group-body.test.ts new file mode 100644 index 00000000000..7badaa64911 --- /dev/null +++ b/extensions/telegram/src/bot-message-context.group-body.test.ts @@ -0,0 +1,4 @@ +import "./bot-message-context.audio-transcript.test-support.js"; +import "./bot-message-context.implicit-mention.test-support.js"; +import "./bot-message-context.sender-prefix.test-support.js"; +import "./bot-message-context.silent-ingest.test-support.js"; diff --git a/extensions/telegram/src/bot-message-context.implicit-mention.test-support.ts b/extensions/telegram/src/bot-message-context.implicit-mention.test-support.ts new file mode 100644 index 00000000000..4ed40719be5 --- /dev/null +++ b/extensions/telegram/src/bot-message-context.implicit-mention.test-support.ts @@ -0,0 +1,147 @@ +import { describe, expect, it } from "vitest"; +import { buildTelegramMessageContextForTest } from "./bot-message-context.test-harness.js"; +import { TELEGRAM_FORUM_SERVICE_FIELDS } from "./forum-service-message.js"; + +describe("buildTelegramMessageContext implicitMention forum service messages", () => { + /** + * Build a group message context where the user sends a message inside a + * forum topic that has `reply_to_message` pointing to a message from the + * bot. Callers control whether the reply target looks like a forum service + * message (carries `forum_topic_created` etc.) or a real bot reply. + */ + async function buildGroupReplyCtx(params: { + replyToMessageText?: string; + replyToMessageCaption?: string; + replyFromIsBot?: boolean; + replyFromId?: number; + /** Extra fields on reply_to_message (e.g. forum_topic_created). */ + replyToMessageExtra?: Record; + }) { + const BOT_ID = 7; // matches test harness primaryCtx.me.id + return await buildTelegramMessageContextForTest({ + message: { + message_id: 100, + chat: { id: -1001234567890, type: "supergroup", title: "Forum Group" }, + date: 1700000000, + text: "hello everyone", + from: { id: 42, first_name: "Alice" }, + reply_to_message: { + message_id: 1, + text: params.replyToMessageText ?? undefined, + ...(params.replyToMessageCaption != null + ? { caption: params.replyToMessageCaption } + : {}), + from: { + id: params.replyFromId ?? BOT_ID, + first_name: "OpenClaw", + is_bot: params.replyFromIsBot ?? true, + }, + ...params.replyToMessageExtra, + }, + }, + resolveGroupActivation: () => true, + resolveGroupRequireMention: () => true, + resolveTelegramGroupConfig: () => ({ + groupConfig: { requireMention: true }, + topicConfig: undefined, + }), + }); + } + + it("does NOT trigger implicitMention for forum_topic_created service message", async () => { + // Bot auto-generated "Topic created" message carries forum_topic_created. + const ctx = await buildGroupReplyCtx({ + replyToMessageText: undefined, + replyFromIsBot: true, + replyToMessageExtra: { + forum_topic_created: { name: "New Topic", icon_color: 0x6fb9f0 }, + }, + }); + + // With requireMention and no explicit @mention, the message should be + // skipped (null) because implicitMention should NOT fire. + expect(ctx).toBeNull(); + }); + + it.each(TELEGRAM_FORUM_SERVICE_FIELDS)( + "does NOT trigger implicitMention for %s service message", + async (field) => { + const ctx = await buildGroupReplyCtx({ + replyToMessageText: undefined, + replyFromIsBot: true, + replyToMessageExtra: { [field]: {} }, + }); + + expect(ctx).toBeNull(); + }, + ); + + it("does NOT trigger implicitMention for forum_topic_closed service message", async () => { + const ctx = await buildGroupReplyCtx({ + replyToMessageText: undefined, + replyFromIsBot: true, + replyToMessageExtra: { forum_topic_closed: {} }, + }); + + expect(ctx).toBeNull(); + }); + + it("does NOT trigger implicitMention for general_forum_topic_hidden service message", async () => { + const ctx = await buildGroupReplyCtx({ + replyToMessageText: undefined, + replyFromIsBot: true, + replyToMessageExtra: { general_forum_topic_hidden: {} }, + }); + + expect(ctx).toBeNull(); + }); + + it("DOES trigger implicitMention for real bot replies (non-empty text)", async () => { + const ctx = await buildGroupReplyCtx({ + replyToMessageText: "Here is my answer", + replyFromIsBot: true, + }); + + // Real bot reply → implicitMention fires → message is NOT skipped. + expect(ctx).not.toBeNull(); + expect(ctx?.ctxPayload?.WasMentioned).toBe(true); + }); + + it("DOES trigger implicitMention for bot media messages with caption", async () => { + // Media messages from the bot have caption but no text — they should + // still count as real bot replies, not service messages. + const ctx = await buildGroupReplyCtx({ + replyToMessageText: undefined, + replyToMessageCaption: "Check out this image", + replyFromIsBot: true, + }); + + expect(ctx).not.toBeNull(); + expect(ctx?.ctxPayload?.WasMentioned).toBe(true); + }); + + it("DOES trigger implicitMention for bot sticker/voice (no text, no caption, no service field)", async () => { + // Stickers, voice notes, and captionless photos have neither text nor + // caption, but they are NOT service messages — they are legitimate bot + // replies that should trigger implicitMention. + const ctx = await buildGroupReplyCtx({ + replyToMessageText: undefined, + replyFromIsBot: true, + // No forum_topic_* fields → not a service message + }); + + expect(ctx).not.toBeNull(); + expect(ctx?.ctxPayload?.WasMentioned).toBe(true); + }); + + it("does NOT trigger implicitMention when reply is from a different user", async () => { + const ctx = await buildGroupReplyCtx({ + replyToMessageText: "some message", + replyFromIsBot: false, + replyFromId: 999, + }); + + // Different user's message → not an implicit mention → skipped. + expect(ctx).toBeNull(); + }); +}); diff --git a/extensions/telegram/src/bot-message-context.named-account-dm.test-support.ts b/extensions/telegram/src/bot-message-context.named-account-dm.test-support.ts new file mode 100644 index 00000000000..c36b60f2ea6 --- /dev/null +++ b/extensions/telegram/src/bot-message-context.named-account-dm.test-support.ts @@ -0,0 +1,160 @@ +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { + getRecordedUpdateLastRoute, + loadTelegramMessageContextRouteHarness, + recordInboundSessionMock, +} from "./bot-message-context.route-test-support.js"; + +let buildTelegramMessageContextForTest: typeof import("./bot-message-context.test-harness.js").buildTelegramMessageContextForTest; +let clearRuntimeConfigSnapshot: typeof import("openclaw/plugin-sdk/config-runtime").clearRuntimeConfigSnapshot; +let setRuntimeConfigSnapshot: typeof import("openclaw/plugin-sdk/config-runtime").setRuntimeConfigSnapshot; + +describe("buildTelegramMessageContext named-account DM fallback", () => { + const baseCfg = { + agents: { defaults: { model: "anthropic/claude-opus-4-5", workspace: "/tmp/openclaw" } }, + channels: { telegram: {} }, + messages: { groupChat: { mentionPatterns: [] } }, + }; + + afterEach(() => { + clearRuntimeConfigSnapshot(); + }); + + beforeAll(async () => { + ({ clearRuntimeConfigSnapshot, setRuntimeConfigSnapshot, buildTelegramMessageContextForTest } = + await loadTelegramMessageContextRouteHarness()); + }); + + beforeEach(() => { + recordInboundSessionMock.mockClear(); + }); + + function getLastUpdateLastRoute(): { sessionKey?: string } | undefined { + return getRecordedUpdateLastRoute() as { sessionKey?: string } | undefined; + } + + function buildNamedAccountDmMessage(messageId = 1) { + return { + message_id: messageId, + chat: { id: 814912386, type: "private" as const }, + date: 1700000000 + messageId - 1, + text: "hello", + from: { id: 814912386, first_name: "Alice" }, + }; + } + + async function buildNamedAccountDmContext(accountId = "atlas", messageId = 1) { + setRuntimeConfigSnapshot(baseCfg); + return await buildTelegramMessageContextForTest({ + cfg: baseCfg, + accountId, + message: buildNamedAccountDmMessage(messageId), + }); + } + + it("allows DM through for a named account with no explicit binding", async () => { + setRuntimeConfigSnapshot(baseCfg); + + const ctx = await buildTelegramMessageContextForTest({ + cfg: baseCfg, + accountId: "atlas", + message: { + message_id: 1, + chat: { id: 814912386, type: "private" }, + date: 1700000000, + text: "hello", + from: { id: 814912386, first_name: "Alice" }, + }, + }); + + expect(ctx).not.toBeNull(); + expect(ctx?.route.matchedBy).toBe("default"); + expect(ctx?.route.accountId).toBe("atlas"); + }); + + it("uses a per-account session key for named-account DMs", async () => { + const ctx = await buildNamedAccountDmContext(); + + expect(ctx?.ctxPayload?.SessionKey).toBe("agent:main:telegram:atlas:direct:814912386"); + }); + + it("keeps named-account fallback lastRoute on the isolated DM session", async () => { + const ctx = await buildNamedAccountDmContext(); + + expect(ctx?.ctxPayload?.SessionKey).toBe("agent:main:telegram:atlas:direct:814912386"); + expect(getLastUpdateLastRoute()?.sessionKey).toBe("agent:main:telegram:atlas:direct:814912386"); + }); + + it("isolates sessions between named accounts that share the default agent", async () => { + const atlas = await buildNamedAccountDmContext("atlas", 1); + const skynet = await buildNamedAccountDmContext("skynet", 2); + + expect(atlas?.ctxPayload?.SessionKey).toBe("agent:main:telegram:atlas:direct:814912386"); + expect(skynet?.ctxPayload?.SessionKey).toBe("agent:main:telegram:skynet:direct:814912386"); + expect(atlas?.ctxPayload?.SessionKey).not.toBe(skynet?.ctxPayload?.SessionKey); + }); + + it("keeps identity-linked peer canonicalization in the named-account fallback path", async () => { + const cfg = { + ...baseCfg, + session: { + identityLinks: { + "alice-shared": ["telegram:814912386"], + }, + }, + }; + setRuntimeConfigSnapshot(cfg); + + const ctx = await buildTelegramMessageContextForTest({ + cfg, + accountId: "atlas", + message: { + message_id: 1, + chat: { id: 999999999, type: "private" }, + date: 1700000000, + text: "hello", + from: { id: 814912386, first_name: "Alice" }, + }, + }); + + expect(ctx?.ctxPayload?.SessionKey).toBe("agent:main:telegram:atlas:direct:alice-shared"); + }); + + it("still drops named-account group messages without an explicit binding", async () => { + setRuntimeConfigSnapshot(baseCfg); + + const ctx = await buildTelegramMessageContextForTest({ + cfg: baseCfg, + accountId: "atlas", + options: { forceWasMentioned: true }, + resolveGroupActivation: () => true, + message: { + message_id: 1, + chat: { id: -1001234567890, type: "supergroup", title: "Test Group" }, + date: 1700000000, + text: "@bot hello", + from: { id: 814912386, first_name: "Alice" }, + }, + }); + + expect(ctx).toBeNull(); + }); + + it("uses the main session key for default-account DMs", async () => { + setRuntimeConfigSnapshot(baseCfg); + + const ctx = await buildTelegramMessageContextForTest({ + cfg: baseCfg, + message: { + message_id: 1, + chat: { id: 42, type: "private" }, + date: 1700000000, + text: "hello", + from: { id: 42, first_name: "Alice" }, + }, + }); + + expect(ctx?.ctxPayload?.SessionKey).toBe("agent:main:main"); + expect(getLastUpdateLastRoute()?.sessionKey).toBe("agent:main:main"); + }); +}); diff --git a/extensions/telegram/src/bot-message-context.sender-prefix.test-support.ts b/extensions/telegram/src/bot-message-context.sender-prefix.test-support.ts new file mode 100644 index 00000000000..104eb64d5d9 --- /dev/null +++ b/extensions/telegram/src/bot-message-context.sender-prefix.test-support.ts @@ -0,0 +1,60 @@ +import { describe, expect, it } from "vitest"; +import { buildTelegramMessageContextForTest } from "./bot-message-context.test-harness.js"; +import { + isTelegramForumServiceMessage, + TELEGRAM_FORUM_SERVICE_FIELDS, +} from "./forum-service-message.js"; + +describe("isTelegramForumServiceMessage", () => { + it("returns true for any Telegram forum service field", () => { + for (const field of TELEGRAM_FORUM_SERVICE_FIELDS) { + expect(isTelegramForumServiceMessage({ [field]: {} })).toBe(true); + } + }); + + it("returns false for normal messages and non-objects", () => { + expect(isTelegramForumServiceMessage({ text: "hello" })).toBe(false); + expect(isTelegramForumServiceMessage(null)).toBe(false); + expect(isTelegramForumServiceMessage("topic created")).toBe(false); + }); +}); + +describe("buildTelegramMessageContext sender prefix", () => { + async function buildCtx(params: { messageId: number; options?: Record }) { + return await buildTelegramMessageContextForTest({ + message: { + message_id: params.messageId, + chat: { id: -99, type: "supergroup", title: "Dev Chat" }, + date: 1700000000, + text: "hello", + from: { id: 42, first_name: "Alice" }, + }, + options: params.options, + }); + } + + it("prefixes group bodies with sender label", async () => { + const ctx = await buildCtx({ messageId: 1 }); + + expect(ctx).not.toBeNull(); + const body = ctx?.ctxPayload?.Body ?? ""; + expect(body).toContain("Alice (42): hello"); + }); + + it("sets MessageSid from message_id", async () => { + const ctx = await buildCtx({ messageId: 12345 }); + + expect(ctx).not.toBeNull(); + expect(ctx?.ctxPayload?.MessageSid).toBe("12345"); + }); + + it("respects messageIdOverride option", async () => { + const ctx = await buildCtx({ + messageId: 12345, + options: { messageIdOverride: "67890" }, + }); + + expect(ctx).not.toBeNull(); + expect(ctx?.ctxPayload?.MessageSid).toBe("67890"); + }); +}); diff --git a/extensions/telegram/src/bot-message-context.session-recreate.test-support.ts b/extensions/telegram/src/bot-message-context.session-recreate.test-support.ts new file mode 100644 index 00000000000..85880dfbab5 --- /dev/null +++ b/extensions/telegram/src/bot-message-context.session-recreate.test-support.ts @@ -0,0 +1,99 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import { + clearRuntimeConfigSnapshot, + setRuntimeConfigSnapshot, +} from "openclaw/plugin-sdk/config-runtime"; +import { + clearSessionStoreCacheForTest, + loadSessionStore, + updateSessionStore, +} from "openclaw/plugin-sdk/config-runtime"; +import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest"; +import { createSuiteTempRootTracker } from "../../../src/test-helpers/temp-dir.js"; +import { buildTelegramMessageContextForTest } from "./bot-message-context.test-harness.js"; + +const TELEGRAM_DIRECT_KEY = "agent:main:telegram:direct:7463849194"; + +describe("Telegram direct session recreation after delete", () => { + const suiteRootTracker = createSuiteTempRootTracker({ + prefix: "openclaw-telegram-context-recreate-", + }); + + beforeAll(async () => { + await suiteRootTracker.setup(); + }); + + afterEach(() => { + clearRuntimeConfigSnapshot(); + clearSessionStoreCacheForTest(); + }); + + afterAll(async () => { + await suiteRootTracker.cleanup(); + }); + + it("records a deleted direct session again when the next DM is processed", async () => { + const tempDir = await suiteRootTracker.make("direct"); + const storePath = path.join(tempDir, "sessions.json"); + const cfg = { + agents: { + defaults: { + model: "openai/gpt-5.4", + workspace: "/tmp/openclaw", + }, + }, + channels: { telegram: {} }, + messages: { groupChat: { mentionPatterns: [] } }, + session: { + dmScope: "per-channel-peer" as const, + store: storePath, + }, + }; + setRuntimeConfigSnapshot(cfg as never); + await fs.writeFile( + storePath, + JSON.stringify( + { + [TELEGRAM_DIRECT_KEY]: { + sessionId: "old-session", + updatedAt: 1_700_000_000_000, + chatType: "direct", + channel: "telegram", + }, + }, + null, + 2, + ), + "utf-8", + ); + await updateSessionStore(storePath, (store) => { + delete store[TELEGRAM_DIRECT_KEY]; + }); + + const context = await buildTelegramMessageContextForTest({ + cfg, + message: { + message_id: 2, + chat: { id: 7463849194, type: "private" }, + date: 1_700_000_001, + text: "hello again", + from: { id: 7463849194, first_name: "Alice" }, + }, + sessionRuntime: null, + }); + + const store = loadSessionStore(storePath, { skipCache: true }); + expect(context?.ctxPayload?.SessionKey).toBe(TELEGRAM_DIRECT_KEY); + expect(store[TELEGRAM_DIRECT_KEY]).toEqual( + expect.objectContaining({ + lastChannel: "telegram", + lastTo: "telegram:7463849194", + origin: expect.objectContaining({ + provider: "telegram", + chatType: "direct", + }), + }), + ); + }); +}); diff --git a/extensions/telegram/src/bot-message-context.silent-ingest.test-support.ts b/extensions/telegram/src/bot-message-context.silent-ingest.test-support.ts new file mode 100644 index 00000000000..f82ceb6d384 --- /dev/null +++ b/extensions/telegram/src/bot-message-context.silent-ingest.test-support.ts @@ -0,0 +1,146 @@ +import { describe, expect, it, vi } from "vitest"; +import { buildTelegramMessageContextForTest } from "./bot-message-context.test-harness.js"; + +const internalHookMocks = vi.hoisted(() => ({ + createInternalHookEvent: vi.fn( + (type: string, action: string, sessionKey: string, context: Record) => ({ + type, + action, + sessionKey, + context, + timestamp: new Date(), + messages: [], + }), + ), + triggerInternalHook: vi.fn(async () => undefined), +})); + +vi.mock("openclaw/plugin-sdk/hook-runtime", () => { + return { + createInternalHookEvent: internalHookMocks.createInternalHookEvent, + fireAndForgetHook: (task: Promise) => void task, + toInternalMessageReceivedContext: (context: Record) => ({ + ...context, + metadata: { to: context.to }, + }), + triggerInternalHook: internalHookMocks.triggerInternalHook, + }; +}); + +function makeGroupMessage(text: string) { + return { + message_id: 42, + chat: { id: -1001234567890, type: "supergroup" as const, title: "Test Group" }, + date: 1_700_000_000, + text, + from: { id: 99, first_name: "Alice", username: "alice" }, + }; +} + +describe("telegram mention-skip silent ingest", () => { + it("emits internal message:received when ingest is enabled", async () => { + internalHookMocks.createInternalHookEvent.mockClear(); + internalHookMocks.triggerInternalHook.mockClear(); + + const result = await buildTelegramMessageContextForTest({ + message: makeGroupMessage("hello without mention"), + cfg: { + agents: { + defaults: { + model: "anthropic/sonnet-4.6", + workspace: "/tmp/openclaw", + }, + }, + channels: { + telegram: { + groups: { + "*": { + requireMention: true, + ingest: true, + }, + }, + }, + }, + messages: { + groupChat: { + mentionPatterns: ["@bot"], + }, + }, + } as never, + resolveGroupRequireMention: () => true, + resolveTelegramGroupConfig: () => ({ + groupConfig: { + requireMention: true, + ingest: true, + }, + topicConfig: undefined, + }), + }); + + expect(result).toBeNull(); + expect(internalHookMocks.createInternalHookEvent).toHaveBeenCalledWith( + "message", + "received", + expect.stringContaining("telegram"), + expect.objectContaining({ + channelId: "telegram", + content: "hello without mention", + }), + ); + expect(internalHookMocks.triggerInternalHook).toHaveBeenCalledTimes(1); + }); + + it("uses wildcard ingest when a specific group override omits ingest", async () => { + internalHookMocks.createInternalHookEvent.mockClear(); + internalHookMocks.triggerInternalHook.mockClear(); + + const result = await buildTelegramMessageContextForTest({ + message: makeGroupMessage("hello without mention"), + cfg: { + agents: { + defaults: { + model: "anthropic/sonnet-4.6", + workspace: "/tmp/openclaw", + }, + }, + channels: { + telegram: { + groups: { + "*": { + requireMention: true, + ingest: true, + }, + "-1001234567890": { + requireMention: true, + }, + }, + }, + }, + messages: { + groupChat: { + mentionPatterns: ["@bot"], + }, + }, + } as never, + resolveGroupRequireMention: () => true, + resolveTelegramGroupConfig: () => ({ + groupConfig: { + requireMention: true, + }, + topicConfig: undefined, + }), + }); + + expect(result).toBeNull(); + expect(internalHookMocks.createInternalHookEvent).toHaveBeenCalledWith( + "message", + "received", + expect.stringContaining("telegram"), + expect.objectContaining({ + channelId: "telegram", + content: "hello without mention", + }), + ); + expect(internalHookMocks.triggerInternalHook).toHaveBeenCalledTimes(1); + }); +}); diff --git a/tsconfig.extensions.json b/tsconfig.extensions.json index 5b596c0c185..7373b980906 100644 --- a/tsconfig.extensions.json +++ b/tsconfig.extensions.json @@ -4,5 +4,16 @@ "tsBuildInfoFile": ".artifacts/tsgo-cache/extensions.tsbuildinfo" }, "include": ["src/**/*.d.ts", "ui/src/**/*.d.ts", "extensions/**/*"], - "exclude": ["node_modules", "dist", "**/dist/**", "**/*.test.ts", "**/*.test.tsx", "test/**"] + "exclude": [ + "node_modules", + "dist", + "**/dist/**", + "**/*.test.ts", + "**/*.test.tsx", + "**/*test-helpers.ts", + "**/*test-harness.ts", + "**/*test-support.ts", + "extensions/**/src/test-support/**", + "test/**" + ] }