Files
openclaw/extensions/telegram/src/bot-message.test.ts
2026-05-08 18:30:26 +05:30

242 lines
7.7 KiB
TypeScript

import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import type { TelegramBotDeps } from "./bot-deps.js";
const buildTelegramMessageContext = vi.hoisted(() => vi.fn());
const dispatchTelegramMessage = vi.hoisted(() => vi.fn());
const telegramInboundInfo = vi.hoisted(() => vi.fn());
const upsertChannelPairingRequest = vi.hoisted(() =>
vi.fn(async () => ({ code: "PAIRCODE", created: true })),
);
vi.mock("openclaw/plugin-sdk/runtime-env", () => ({
createSubsystemLogger: () => ({
child: () => ({
info: telegramInboundInfo,
}),
}),
danger: (message: string) => message,
logVerbose: vi.fn(),
shouldLogVerbose: () => false,
}));
vi.mock("./bot-message-context.js", () => ({
buildTelegramMessageContext,
}));
vi.mock("./bot-message-dispatch.js", () => ({
dispatchTelegramMessage,
}));
let createTelegramMessageProcessor: typeof import("./bot-message.js").createTelegramMessageProcessor;
let formatTelegramInboundLogLine: typeof import("./bot-message.js").formatTelegramInboundLogLine;
describe("telegram bot message processor", () => {
beforeAll(async () => {
({ createTelegramMessageProcessor, formatTelegramInboundLogLine } =
await import("./bot-message.js"));
});
beforeEach(() => {
buildTelegramMessageContext.mockClear();
dispatchTelegramMessage.mockClear();
telegramInboundInfo.mockClear();
upsertChannelPairingRequest.mockClear();
});
const telegramDepsForTest = {
upsertChannelPairingRequest,
} as unknown as TelegramBotDeps;
const baseDeps = {
bot: {},
cfg: {},
account: {},
telegramCfg: {},
historyLimit: 0,
groupHistories: {},
dmPolicy: {},
allowFrom: [],
groupAllowFrom: [],
ackReactionScope: "none",
logger: {},
resolveGroupActivation: () => true,
resolveGroupRequireMention: () => false,
resolveTelegramGroupConfig: () => ({}),
runtime: {},
replyToMode: "auto",
streamMode: "partial",
textLimit: 4096,
telegramDeps: telegramDepsForTest,
opts: {},
} as unknown as Parameters<typeof createTelegramMessageProcessor>[0];
async function processSampleMessage(
processMessage: ReturnType<typeof createTelegramMessageProcessor>,
) {
await processMessage(
{
message: {
chat: { id: 123, type: "private", title: "chat" },
message_id: 456,
},
} as unknown as Parameters<typeof processMessage>[0],
[],
[],
{},
);
}
function createDispatchFailureHarness(
context: Record<string, unknown>,
sendMessage: ReturnType<typeof vi.fn>,
) {
const runtimeError = vi.fn();
buildTelegramMessageContext.mockResolvedValue(createMessageContext(context));
dispatchTelegramMessage.mockRejectedValue(new Error("dispatch exploded"));
const processMessage = createTelegramMessageProcessor({
...baseDeps,
bot: { api: { sendMessage } },
runtime: { error: runtimeError },
} as unknown as Parameters<typeof createTelegramMessageProcessor>[0]);
return { processMessage, runtimeError };
}
function createMessageContext(context: Record<string, unknown> = {}) {
return {
chatId: 123,
ctxPayload: {
From: "telegram:123",
To: "telegram:123",
ChatType: "direct",
RawBody: "hello there",
},
primaryCtx: { me: { username: "openclaw_bot" } },
route: { sessionKey: "agent:main:main" },
sendTyping: vi.fn().mockResolvedValue(undefined),
...context,
};
}
it("dispatches when context is available", async () => {
const sendTyping = vi.fn().mockResolvedValue(undefined);
buildTelegramMessageContext.mockResolvedValue(
createMessageContext({
sendTyping,
}),
);
const processMessage = createTelegramMessageProcessor(baseDeps);
await processSampleMessage(processMessage);
expect(sendTyping).toHaveBeenCalledTimes(1);
expect(dispatchTelegramMessage).toHaveBeenCalledTimes(1);
expect(sendTyping.mock.invocationCallOrder[0]).toBeLessThan(
dispatchTelegramMessage.mock.invocationCallOrder[0],
);
expect(telegramInboundInfo).toHaveBeenCalledWith(
"Inbound message telegram:123 -> @openclaw_bot (direct, 11 chars)",
);
});
it("skips dispatch when no context is produced", async () => {
buildTelegramMessageContext.mockResolvedValue(null);
const processMessage = createTelegramMessageProcessor(baseDeps);
await processSampleMessage(processMessage);
expect(dispatchTelegramMessage).not.toHaveBeenCalled();
expect(telegramInboundInfo).not.toHaveBeenCalled();
});
it("formats Telegram inbound summaries without message content", () => {
expect(
formatTelegramInboundLogLine({
from: "telegram:123",
to: "@openclaw_bot",
chatType: "direct",
body: "secret message",
}),
).toBe("Inbound message telegram:123 -> @openclaw_bot (direct, 14 chars)");
expect(
formatTelegramInboundLogLine({
from: "telegram:group:-100",
to: "@openclaw_bot",
chatType: "group",
body: "<media:image>",
mediaType: "image/jpeg",
}),
).toBe("Inbound message telegram:group:-100 -> @openclaw_bot (group, image/jpeg, 13 chars)");
});
it("keeps dispatch running when the early typing cue fails", async () => {
const sendTyping = vi.fn().mockRejectedValue(new Error("typing failed"));
buildTelegramMessageContext.mockResolvedValue(
createMessageContext({
sendTyping,
}),
);
const processMessage = createTelegramMessageProcessor(baseDeps);
await processSampleMessage(processMessage);
expect(sendTyping).toHaveBeenCalledTimes(1);
expect(dispatchTelegramMessage).toHaveBeenCalledTimes(1);
});
it("sends user-visible fallback when dispatch throws", async () => {
const sendMessage = vi.fn().mockResolvedValue(undefined);
const { processMessage, runtimeError } = createDispatchFailureHarness(
{
chatId: 123,
threadSpec: { id: 456, scope: "forum" },
route: { sessionKey: "agent:main:main" },
},
sendMessage,
);
await expect(processSampleMessage(processMessage)).resolves.toBeUndefined();
expect(sendMessage).toHaveBeenCalledWith(
123,
"Something went wrong while processing your request. Please try again.",
{ message_thread_id: 456 },
);
expect(runtimeError).toHaveBeenCalledWith(expect.stringContaining("dispatch exploded"));
});
it("omits message_thread_id for General-topic fallback replies", async () => {
const sendMessage = vi.fn().mockResolvedValue(undefined);
const { processMessage } = createDispatchFailureHarness(
{
chatId: 123,
threadSpec: { id: 1, scope: "forum" },
route: { sessionKey: "agent:main:main" },
},
sendMessage,
);
await expect(processSampleMessage(processMessage)).resolves.toBeUndefined();
expect(sendMessage).toHaveBeenCalledWith(
123,
"Something went wrong while processing your request. Please try again.",
undefined,
);
});
it("swallows fallback delivery failures after dispatch throws", async () => {
const sendMessage = vi.fn().mockRejectedValue(new Error("blocked by user"));
const { processMessage, runtimeError } = createDispatchFailureHarness(
{
chatId: 123,
route: { sessionKey: "agent:main:main" },
},
sendMessage,
);
await expect(processSampleMessage(processMessage)).resolves.toBeUndefined();
expect(sendMessage).toHaveBeenCalledWith(
123,
"Something went wrong while processing your request. Please try again.",
undefined,
);
expect(runtimeError).toHaveBeenCalledWith(expect.stringContaining("dispatch exploded"));
});
});