From 8a5170d1d910ef109caf2e03946aefb1decaf4df Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Wed, 6 May 2026 20:14:55 -0700 Subject: [PATCH] test(telegram): cover message context perf guards --- CHANGELOG.md | 1 + .../bot-message-context.dm-threads.test.ts | 23 +++ .../src/bot-message-context.reactions.test.ts | 146 ++++++++++++++++++ .../src/bot-message-context.test-harness.ts | 5 +- 4 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 extensions/telegram/src/bot-message-context.reactions.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index ecf0a68ebfa..ab6fa3f49d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ Docs: https://docs.openclaw.ai - Docker/Gateway: harden the gateway container by dropping `NET_RAW` and `NET_ADMIN` capabilities and enabling `no-new-privileges` in the bundled `docker-compose.yml`. Thanks @VintageAyu. - Telegram: accept plugin-owned numeric forum-topic targets in the agent message tool and keep reply-dispatch provider chunks behind a real stable runtime alias during in-place package updates. Fixes #77137. Thanks @richardmqq. - Telegram/streaming: keep draft preview rotation from reusing a pre-tool assistant preview after visible tool or media output lands between compaction replay and the next assistant message. Thanks @vincentkoc. +- Telegram/performance: skip non-forum topic-cache setup, defer status reaction variant work until reactions are needed, and reuse ack reaction gating during message context assembly. Thanks @vincentkoc. - Channels/WhatsApp: support explicit WhatsApp Channel/Newsletter `@newsletter` outbound message targets with channel session metadata instead of DM routing. Fixes #13417; carries forward the narrow outbound target idea from #13424. Thanks @vincentkoc and @agentz-manfred. - TTS/telephony: honor provider voice/model overrides in telephony synthesis providers so Google Meet agent speech logs match the backend that actually produced the audio. Thanks @vincentkoc. - Voice Call/realtime: bound the paced Twilio audio queue and close overloaded realtime streams before provider audio can pile up behind the websocket backpressure guard. Thanks @vincentkoc. diff --git a/extensions/telegram/src/bot-message-context.dm-threads.test.ts b/extensions/telegram/src/bot-message-context.dm-threads.test.ts index 044b3e870ed..c42ea232b21 100644 --- a/extensions/telegram/src/bot-message-context.dm-threads.test.ts +++ b/extensions/telegram/src/bot-message-context.dm-threads.test.ts @@ -244,6 +244,29 @@ describe("buildTelegramMessageContext group sessions without forum", () => { expect(ctxWithThread?.ctxPayload?.SessionKey).toBe(ctxWithoutThread?.ctxPayload?.SessionKey); }); + it("does not add a topic-cache store lookup for non-forum group reply threads", async () => { + const resolveStorePath = vi.fn(() => "/tmp/openclaw/session-store.json"); + + const ctx = await buildTelegramMessageContextForTest({ + message: { + message_id: 9, + chat: { id: -1001234567890, type: "supergroup", title: "Test Group" }, + date: 1700000008, + text: "@bot hello", + message_thread_id: 42, + from: { id: 42, first_name: "Alice" }, + }, + options: { forceWasMentioned: true }, + resolveGroupActivation: () => true, + sessionRuntime: { resolveStorePath }, + }); + + expect(ctx).not.toBeNull(); + expect(ctx?.isForum).toBe(false); + expect(ctx?.ctxPayload?.MessageThreadId).toBeUndefined(); + expect(resolveStorePath).toHaveBeenCalledTimes(1); + }); + it("uses topic session for forum groups with message_thread_id", async () => { const ctx = await buildContext({ message_id: 1, diff --git a/extensions/telegram/src/bot-message-context.reactions.test.ts b/extensions/telegram/src/bot-message-context.reactions.test.ts new file mode 100644 index 00000000000..951b61dd486 --- /dev/null +++ b/extensions/telegram/src/bot-message-context.reactions.test.ts @@ -0,0 +1,146 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { BuildTelegramMessageContextParams } from "./bot-message-context.types.js"; + +const inboundBodyMock = vi.hoisted(() => + vi.fn(async () => ({ + bodyText: "hello", + rawBody: "hello", + historyKey: undefined, + commandAuthorized: false, + effectiveWasMentioned: false, + canDetectMention: true, + shouldBypassMention: false, + stickerCacheHit: false, + locationData: undefined, + })), +); + +vi.mock("./bot-message-context.body.js", () => ({ + resolveTelegramInboundBody: (...args: unknown[]) => inboundBodyMock(...args), +})); + +const { buildTelegramMessageContextForTest } = + await import("./bot-message-context.test-harness.js"); + +type CreateStatusReactionController = NonNullable< + NonNullable["createStatusReactionController"] +>; +type StatusReactionControllerParams = Parameters[0]; + +function createStatusReactionControllerStub() { + const controller = { + setQueued: vi.fn(async () => undefined), + setThinking: vi.fn(async () => undefined), + setTool: vi.fn(async () => undefined), + setCompacting: vi.fn(async () => undefined), + cancelPending: vi.fn(), + setDone: vi.fn(async () => undefined), + setError: vi.fn(async () => undefined), + clear: vi.fn(async () => undefined), + restoreInitial: vi.fn(async () => undefined), + }; + const createStatusReactionController = vi.fn((params: StatusReactionControllerParams) => { + return controller; + }); + return { controller, createStatusReactionController }; +} + +describe("buildTelegramMessageContext reactions", () => { + beforeEach(() => { + inboundBodyMock.mockClear(); + }); + + it("does not create status reactions when the ack gate blocks an unmentioned group message", async () => { + const setMessageReaction = vi.fn(async () => undefined); + const { createStatusReactionController } = createStatusReactionControllerStub(); + + const ctx = await buildTelegramMessageContextForTest({ + message: { + message_id: 12, + chat: { id: -1001234567890, type: "group", title: "Ops" }, + date: 1_700_000_000, + text: "hello", + from: { id: 42, first_name: "Alice" }, + }, + cfg: { + agents: { + defaults: { model: "anthropic/claude-opus-4-5", workspace: "/tmp/openclaw" }, + }, + channels: { + telegram: { + groupPolicy: "open", + groups: { "*": { requireMention: true } }, + }, + }, + messages: { + ackReaction: "👀", + groupChat: { mentionPatterns: [] }, + statusReactions: { enabled: true }, + }, + }, + ackReactionScope: "group-mentions", + botApi: { setMessageReaction }, + runtime: { createStatusReactionController }, + resolveGroupActivation: () => true, + resolveGroupRequireMention: () => true, + resolveTelegramGroupConfig: () => ({ + groupConfig: { requireMention: true }, + topicConfig: undefined, + }), + }); + + expect(ctx).not.toBeNull(); + expect(ctx?.ackReactionPromise).toBeNull(); + expect(ctx?.statusReactionController).toBeNull(); + expect(createStatusReactionController).not.toHaveBeenCalled(); + expect(setMessageReaction).not.toHaveBeenCalled(); + }); + + it("keeps Telegram status reaction variants available for configured emoji fallbacks", async () => { + const setMessageReaction = vi.fn(async () => undefined); + const { controller, createStatusReactionController } = createStatusReactionControllerStub(); + + const ctx = await buildTelegramMessageContextForTest({ + message: { + message_id: 34, + chat: { + id: 1234, + type: "private", + available_reactions: [{ type: "emoji", emoji: "👍" }], + }, + date: 1_700_000_000, + text: "hello", + from: { id: 42, first_name: "Alice" }, + }, + cfg: { + agents: { + defaults: { model: "anthropic/claude-opus-4-5", workspace: "/tmp/openclaw" }, + }, + channels: { telegram: { dmPolicy: "open", allowFrom: ["*"] } }, + messages: { + ackReaction: "👀", + groupChat: { mentionPatterns: [] }, + statusReactions: { + enabled: true, + emojis: { done: "✅" }, + }, + }, + }, + ackReactionScope: "direct", + botApi: { setMessageReaction }, + runtime: { createStatusReactionController }, + }); + + await expect(ctx?.ackReactionPromise).resolves.toBe(true); + expect(controller.setQueued).toHaveBeenCalledTimes(1); + expect(createStatusReactionController).toHaveBeenCalledTimes(1); + + const params = createStatusReactionController.mock.calls[0]?.[0]; + expect(params?.initialEmoji).toBe("👀"); + expect(params?.emojis?.done).toBe("✅"); + + await params?.adapter.setReaction("✅"); + + expect(setMessageReaction).toHaveBeenCalledWith(1234, 34, [{ type: "emoji", emoji: "👍" }]); + }); +}); diff --git a/extensions/telegram/src/bot-message-context.test-harness.ts b/extensions/telegram/src/bot-message-context.test-harness.ts index 28f3888b324..ffb278ff182 100644 --- a/extensions/telegram/src/bot-message-context.test-harness.ts +++ b/extensions/telegram/src/bot-message-context.test-harness.ts @@ -18,6 +18,8 @@ type BuildTelegramMessageContextForTestParams = { options?: BuildTelegramMessageContextParams["options"]; cfg?: Record; accountId?: string; + ackReactionScope?: BuildTelegramMessageContextParams["ackReactionScope"]; + botApi?: Record; runtime?: BuildTelegramMessageContextParams["runtime"]; sessionRuntime?: BuildTelegramMessageContextParams["sessionRuntime"] | null; resolveGroupActivation?: BuildTelegramMessageContextParams["resolveGroupActivation"]; @@ -67,6 +69,7 @@ export async function buildTelegramMessageContextForTest( api: { sendChatAction: vi.fn(), setMessageReaction: vi.fn(), + ...params.botApi, }, } as never, cfg: (params.cfg ?? baseTelegramMessageContextConfig) as never, @@ -82,7 +85,7 @@ export async function buildTelegramMessageContextForTest( dmPolicy: "open", allowFrom: ["*"], groupAllowFrom: [], - ackReactionScope: "off", + ackReactionScope: params.ackReactionScope ?? "off", logger: { info: vi.fn() }, resolveGroupActivation: params.resolveGroupActivation ?? (() => undefined), resolveGroupRequireMention: params.resolveGroupRequireMention ?? (() => false),