test(telegram): cover message context perf guards

This commit is contained in:
Vincent Koc
2026-05-06 20:14:55 -07:00
parent 91e324377c
commit 8a5170d1d9
4 changed files with 174 additions and 1 deletions

View File

@@ -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.

View File

@@ -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,

View File

@@ -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<BuildTelegramMessageContextParams["runtime"]>["createStatusReactionController"]
>;
type StatusReactionControllerParams = Parameters<CreateStatusReactionController>[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: "👍" }]);
});
});

View File

@@ -18,6 +18,8 @@ type BuildTelegramMessageContextForTestParams = {
options?: BuildTelegramMessageContextParams["options"];
cfg?: Record<string, unknown>;
accountId?: string;
ackReactionScope?: BuildTelegramMessageContextParams["ackReactionScope"];
botApi?: Record<string, unknown>;
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),