mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 20:35:39 -06:00
d258c6651a
* test: replace assertion chains with typed fixture builders * test: avoid generic mock lint suppressions * test: preserve Telegram API mock signatures
325 lines
9.4 KiB
TypeScript
325 lines
9.4 KiB
TypeScript
// Telegram plugin module implements send harness behavior.
|
|
import type { Bot } from "grammy";
|
|
import { resolveMarkdownTableMode } from "openclaw/plugin-sdk/markdown-table-runtime";
|
|
import {
|
|
buildOutboundMediaLoadOptions,
|
|
normalizePollInput,
|
|
} from "openclaw/plugin-sdk/media-runtime";
|
|
import type { MockFn } from "openclaw/plugin-sdk/plugin-test-runtime";
|
|
import { beforeEach, vi } from "vitest";
|
|
import { markdownToTelegramHtml } from "./format.js";
|
|
import { inputRichBlocksToPlainText, type InputRichBlock } from "./rich-block-model.js";
|
|
|
|
type TelegramApiMethod = (...args: never[]) => unknown;
|
|
type TelegramApiTestOverrides = {
|
|
[Key in keyof Bot["api"] as Bot["api"][Key] extends TelegramApiMethod ? Key : never]?: MockFn<
|
|
Extract<Bot["api"][Key], TelegramApiMethod>
|
|
>;
|
|
};
|
|
|
|
export function makeTelegramApiTestMock<Overrides extends TelegramApiTestOverrides>(
|
|
overrides: Overrides,
|
|
): Overrides & Partial<Bot["api"]> {
|
|
return overrides as Overrides & Partial<Bot["api"]>;
|
|
}
|
|
|
|
export function makeTelegramInvalidApiResultMock<Key extends keyof Bot["api"]>(
|
|
_method: Key,
|
|
implementation: (...args: Parameters<Extract<Bot["api"][Key], TelegramApiMethod>>) => unknown,
|
|
): MockFn<Extract<Bot["api"][Key], TelegramApiMethod>> {
|
|
return vi.fn(implementation) as ReturnType<typeof vi.fn> &
|
|
MockFn<Extract<Bot["api"][Key], TelegramApiMethod>>;
|
|
}
|
|
|
|
function richMessagePlainTextForTest(richMessage: {
|
|
blocks?: InputRichBlock[];
|
|
markdown?: string;
|
|
html?: string;
|
|
}): string {
|
|
if (richMessage.blocks) {
|
|
return inputRichBlocksToPlainText(richMessage.blocks);
|
|
}
|
|
if (richMessage.markdown !== undefined) {
|
|
return markdownToTelegramHtml(richMessage.markdown);
|
|
}
|
|
return richMessage.html ?? "";
|
|
}
|
|
|
|
const { botApi, botRawApi, botConfigUseSpy, botCtorSpy } = vi.hoisted(() => ({
|
|
botConfigUseSpy: vi.fn(),
|
|
botRawApi: {
|
|
editMessageText: vi.fn(),
|
|
sendRichMessage: vi.fn(),
|
|
},
|
|
botApi: {
|
|
deleteMessage: vi.fn(),
|
|
editForumTopic: vi.fn(),
|
|
editMessageCaption: vi.fn(),
|
|
editMessageText: vi.fn(),
|
|
editMessageReplyMarkup: vi.fn(),
|
|
getChatMember: vi.fn(),
|
|
pinChatMessage: vi.fn(),
|
|
sendChatAction: vi.fn(),
|
|
sendMessage: vi.fn(),
|
|
sendPoll: vi.fn(),
|
|
sendPhoto: vi.fn(),
|
|
sendVoice: vi.fn(),
|
|
sendAudio: vi.fn(),
|
|
sendVideo: vi.fn(),
|
|
sendVideoNote: vi.fn(),
|
|
sendAnimation: vi.fn(),
|
|
setMessageReaction: vi.fn(),
|
|
sendSticker: vi.fn(),
|
|
unpinChatMessage: vi.fn(),
|
|
},
|
|
botCtorSpy: vi.fn(),
|
|
}));
|
|
|
|
const { loadWebMedia } = vi.hoisted(() => ({
|
|
loadWebMedia: vi.fn(),
|
|
}));
|
|
|
|
const { imageMetadata } = vi.hoisted(() => ({
|
|
imageMetadata: {
|
|
width: 1200 as number | undefined,
|
|
height: 800 as number | undefined,
|
|
},
|
|
}));
|
|
|
|
const { probeVideoDimensions } = vi.hoisted(() => ({
|
|
probeVideoDimensions: vi.fn(),
|
|
}));
|
|
|
|
const { loadConfig, resolveStorePath } = vi.hoisted(() => ({
|
|
loadConfig: vi.fn(() => ({})),
|
|
resolveStorePath: vi.fn(
|
|
(storePath?: string) => storePath ?? "/tmp/openclaw-telegram-send-tests.json",
|
|
),
|
|
}));
|
|
|
|
const { maybePersistResolvedTelegramTarget } = vi.hoisted(() => ({
|
|
maybePersistResolvedTelegramTarget: vi.fn(async () => {}),
|
|
}));
|
|
|
|
const {
|
|
undiciFetch,
|
|
undiciSetGlobalDispatcher,
|
|
undiciAgentCtor,
|
|
undiciEnvHttpProxyAgentCtor,
|
|
undiciProxyAgentCtor,
|
|
} = vi.hoisted(() => ({
|
|
undiciFetch: vi.fn(),
|
|
undiciSetGlobalDispatcher: vi.fn(),
|
|
undiciAgentCtor: vi.fn(function MockAgent(
|
|
this: { options?: Record<string, unknown> },
|
|
options?: Record<string, unknown>,
|
|
) {
|
|
this.options = options;
|
|
}),
|
|
undiciEnvHttpProxyAgentCtor: vi.fn(function MockEnvHttpProxyAgent(
|
|
this: { options?: Record<string, unknown> },
|
|
options?: Record<string, unknown>,
|
|
) {
|
|
this.options = options;
|
|
}),
|
|
undiciProxyAgentCtor: vi.fn(function MockProxyAgent(
|
|
this: { options?: Record<string, unknown> | string },
|
|
options?: Record<string, unknown> | string,
|
|
) {
|
|
this.options = options;
|
|
}),
|
|
}));
|
|
|
|
type TelegramSendTestMocks = {
|
|
botApi: typeof botApi;
|
|
botRawApi: typeof botRawApi;
|
|
botConfigUseSpy: MockFn;
|
|
botCtorSpy: MockFn;
|
|
loadConfig: MockFn;
|
|
resolveStorePath: MockFn;
|
|
loadWebMedia: MockFn;
|
|
maybePersistResolvedTelegramTarget: MockFn;
|
|
imageMetadata: { width: number | undefined; height: number | undefined };
|
|
probeVideoDimensions: MockFn;
|
|
};
|
|
|
|
vi.mock("openclaw/plugin-sdk/web-media", () => ({
|
|
loadWebMedia,
|
|
}));
|
|
|
|
vi.mock("grammy", () => ({
|
|
API_CONSTANTS: {
|
|
DEFAULT_UPDATE_TYPES: ["message"],
|
|
ALL_UPDATE_TYPES: ["message"],
|
|
},
|
|
Bot: class {
|
|
api = {
|
|
...botApi,
|
|
raw: botRawApi,
|
|
config: {
|
|
use: botConfigUseSpy,
|
|
},
|
|
};
|
|
catch = vi.fn();
|
|
constructor(
|
|
public token: string,
|
|
public options?: {
|
|
client?: { fetch?: typeof fetch; timeoutSeconds?: number };
|
|
},
|
|
) {
|
|
botCtorSpy(token, options);
|
|
}
|
|
},
|
|
HttpError: class HttpError extends Error {
|
|
constructor(
|
|
message = "HttpError",
|
|
public error?: unknown,
|
|
) {
|
|
super(message);
|
|
}
|
|
},
|
|
GrammyError: class GrammyError extends Error {
|
|
description = "";
|
|
},
|
|
InputFile: class InputFile {
|
|
constructor(
|
|
public readonly fileData: Buffer,
|
|
public readonly filename?: string,
|
|
) {}
|
|
},
|
|
}));
|
|
|
|
vi.mock("undici", async () => {
|
|
const actual = await vi.importActual<typeof import("undici")>("undici");
|
|
return {
|
|
...actual,
|
|
Agent: undiciAgentCtor,
|
|
EnvHttpProxyAgent: undiciEnvHttpProxyAgentCtor,
|
|
ProxyAgent: undiciProxyAgentCtor,
|
|
fetch: undiciFetch,
|
|
setGlobalDispatcher: undiciSetGlobalDispatcher,
|
|
};
|
|
});
|
|
|
|
vi.mock("openclaw/plugin-sdk/plugin-config-runtime", async () => {
|
|
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/plugin-config-runtime")>(
|
|
"openclaw/plugin-sdk/plugin-config-runtime",
|
|
);
|
|
return {
|
|
...actual,
|
|
requireRuntimeConfig: vi.fn((cfg: unknown) => cfg ?? loadConfig()),
|
|
};
|
|
});
|
|
|
|
vi.mock("./send.runtime.js", () => ({
|
|
buildOutboundMediaLoadOptions,
|
|
getImageMetadata: vi.fn(async () => ({ ...imageMetadata })),
|
|
loadConfig,
|
|
loadWebMedia,
|
|
normalizePollInput,
|
|
probeVideoDimensions,
|
|
requireRuntimeConfig: vi.fn((cfg: unknown) => cfg ?? loadConfig()),
|
|
resolveMarkdownTableMode,
|
|
resolveStorePath,
|
|
}));
|
|
|
|
vi.mock("./target-writeback.js", () => ({
|
|
maybePersistResolvedTelegramTarget,
|
|
}));
|
|
|
|
export function getTelegramSendTestMocks(): TelegramSendTestMocks {
|
|
return {
|
|
botApi,
|
|
botRawApi,
|
|
botConfigUseSpy,
|
|
botCtorSpy,
|
|
loadConfig,
|
|
resolveStorePath,
|
|
loadWebMedia,
|
|
maybePersistResolvedTelegramTarget,
|
|
imageMetadata,
|
|
probeVideoDimensions,
|
|
};
|
|
}
|
|
|
|
export function installTelegramSendTestHooks() {
|
|
beforeEach(() => {
|
|
loadConfig.mockReturnValue({});
|
|
resolveStorePath.mockReturnValue("/tmp/openclaw-telegram-send-tests.json");
|
|
loadWebMedia.mockReset();
|
|
probeVideoDimensions.mockReset();
|
|
probeVideoDimensions.mockResolvedValue(undefined);
|
|
imageMetadata.width = 1200;
|
|
imageMetadata.height = 800;
|
|
maybePersistResolvedTelegramTarget.mockReset();
|
|
maybePersistResolvedTelegramTarget.mockResolvedValue(undefined);
|
|
undiciFetch.mockReset();
|
|
undiciSetGlobalDispatcher.mockReset();
|
|
undiciAgentCtor.mockClear();
|
|
undiciEnvHttpProxyAgentCtor.mockClear();
|
|
undiciProxyAgentCtor.mockClear();
|
|
botCtorSpy.mockReset();
|
|
botConfigUseSpy.mockReset();
|
|
for (const fn of Object.values(botApi)) {
|
|
fn.mockReset();
|
|
}
|
|
for (const fn of Object.values(botRawApi)) {
|
|
fn.mockReset();
|
|
}
|
|
botRawApi.sendRichMessage.mockImplementation(
|
|
async (params: {
|
|
chat_id: string | number;
|
|
rich_message: { markdown?: string; html?: string; skip_entity_detection?: boolean };
|
|
[key: string]: unknown;
|
|
}) => {
|
|
const { chat_id, rich_message, ...richParams } = params;
|
|
const sendParams: Record<string, unknown> = {
|
|
parse_mode: "HTML",
|
|
...(rich_message.skip_entity_detection === true ? { skip_entity_detection: true } : {}),
|
|
...richParams,
|
|
};
|
|
const replyParameters = sendParams.reply_parameters;
|
|
if (
|
|
replyParameters &&
|
|
typeof replyParameters === "object" &&
|
|
!("quote" in replyParameters) &&
|
|
typeof (replyParameters as { message_id?: unknown }).message_id === "number"
|
|
) {
|
|
sendParams.reply_to_message_id = (replyParameters as { message_id: number }).message_id;
|
|
sendParams.allow_sending_without_reply = true;
|
|
delete sendParams.reply_parameters;
|
|
}
|
|
const text = richMessagePlainTextForTest(rich_message);
|
|
const options = Object.keys(sendParams).length > 0 ? sendParams : undefined;
|
|
return await botApi.sendMessage(chat_id, text, options);
|
|
},
|
|
);
|
|
botRawApi.editMessageText.mockImplementation(
|
|
async (params: {
|
|
chat_id?: string | number;
|
|
message_id?: number;
|
|
rich_message: {
|
|
blocks?: InputRichBlock[];
|
|
markdown?: string;
|
|
html?: string;
|
|
skip_entity_detection?: boolean;
|
|
};
|
|
[key: string]: unknown;
|
|
}) => {
|
|
const { chat_id, message_id, rich_message, ...editParams } = params;
|
|
const text = richMessagePlainTextForTest(rich_message);
|
|
const options = {
|
|
...(rich_message.skip_entity_detection === true ? { skip_entity_detection: true } : {}),
|
|
...editParams,
|
|
};
|
|
return await botApi.editMessageText(chat_id, message_id, text, options);
|
|
},
|
|
);
|
|
});
|
|
}
|
|
|
|
export async function importTelegramSendModule() {
|
|
vi.resetModules();
|
|
return await import("./send.js");
|
|
}
|