test: split telegram bot command menu coverage

This commit is contained in:
Peter Steinberger 2026-04-03 13:49:31 +01:00
parent da1980b923
commit 4f4aa46d00
No known key found for this signature in database
3 changed files with 221 additions and 151 deletions

View File

@ -0,0 +1,196 @@
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
const {
getLoadConfigMock,
listSkillCommandsForAgents,
setMyCommandsSpy,
telegramBotDepsForTest,
telegramBotRuntimeForTest,
} = await import("./bot.create-telegram-bot.test-harness.js");
let listNativeCommandSpecs: typeof import("../../../src/auto-reply/commands-registry.js").listNativeCommandSpecs;
let listNativeCommandSpecsForConfig: typeof import("../../../src/auto-reply/commands-registry.js").listNativeCommandSpecsForConfig;
let normalizeTelegramCommandName: typeof import("./command-config.js").normalizeTelegramCommandName;
let createTelegramBotBase: typeof import("./bot.js").createTelegramBot;
let setTelegramBotRuntimeForTest: typeof import("./bot.js").setTelegramBotRuntimeForTest;
let createTelegramBot: (
opts: Parameters<typeof import("./bot.js").createTelegramBot>[0],
) => ReturnType<typeof import("./bot.js").createTelegramBot>;
const loadConfig = getLoadConfigMock();
function createSignal() {
let resolve!: () => void;
const promise = new Promise<void>((res) => {
resolve = res;
});
return { promise, resolve };
}
function waitForNextSetMyCommands() {
const synced = createSignal();
setMyCommandsSpy.mockImplementationOnce(async () => {
synced.resolve();
return undefined;
});
return synced.promise;
}
function resolveSkillCommands(config: Parameters<typeof listNativeCommandSpecsForConfig>[0]) {
void config;
return listSkillCommandsForAgents() as NonNullable<
Parameters<typeof listNativeCommandSpecsForConfig>[1]
>["skillCommands"];
}
describe("createTelegramBot command menu", () => {
beforeAll(async () => {
({ listNativeCommandSpecs, listNativeCommandSpecsForConfig } =
await import("../../../src/auto-reply/commands-registry.js"));
({ normalizeTelegramCommandName } = await import("./command-config.js"));
({ createTelegramBot: createTelegramBotBase, setTelegramBotRuntimeForTest } =
await import("./bot.js"));
});
beforeEach(() => {
loadConfig.mockReturnValue({
agents: {
defaults: {
envelopeTimezone: "utc",
},
},
channels: {
telegram: { dmPolicy: "open", allowFrom: ["*"] },
},
});
setTelegramBotRuntimeForTest(
telegramBotRuntimeForTest as unknown as Parameters<typeof setTelegramBotRuntimeForTest>[0],
);
createTelegramBot = (opts) =>
createTelegramBotBase({
...opts,
telegramDeps: telegramBotDepsForTest,
});
});
it("merges custom commands with native commands", async () => {
const config = {
channels: {
telegram: {
customCommands: [
{ command: "custom_backup", description: "Git backup" },
{ command: "/Custom_Generate", description: "Create an image" },
],
},
},
};
loadConfig.mockReturnValue(config);
const commandsSynced = waitForNextSetMyCommands();
createTelegramBot({
token: "tok",
config: {
channels: {
telegram: {
dmPolicy: "open",
allowFrom: ["*"],
execApprovals: {
enabled: true,
approvers: ["9"],
target: "dm",
},
},
},
},
});
await commandsSynced;
const registered = setMyCommandsSpy.mock.calls.at(-1)?.[0] as Array<{
command: string;
description: string;
}>;
const skillCommands = resolveSkillCommands(config);
const native = listNativeCommandSpecsForConfig(config, { skillCommands }).map((command) => ({
command: normalizeTelegramCommandName(command.name),
description: command.description,
}));
expect(registered.slice(0, native.length)).toEqual(native);
});
it("ignores custom commands that collide with native commands", async () => {
const errorSpy = vi.fn();
const config = {
channels: {
telegram: {
customCommands: [
{ command: "status", description: "Custom status" },
{ command: "custom_backup", description: "Git backup" },
],
},
},
};
loadConfig.mockReturnValue(config);
const commandsSynced = waitForNextSetMyCommands();
createTelegramBot({
token: "tok",
runtime: {
log: vi.fn(),
error: errorSpy,
exit: ((code: number) => {
throw new Error(`exit ${code}`);
}) as (code: number) => never,
},
});
await commandsSynced;
const registered = setMyCommandsSpy.mock.calls[0]?.[0] as Array<{
command: string;
description: string;
}>;
const skillCommands = resolveSkillCommands(config);
const native = listNativeCommandSpecsForConfig(config, { skillCommands }).map((command) => ({
command: normalizeTelegramCommandName(command.name),
description: command.description,
}));
const nativeStatus = native.find((command) => command.command === "status");
expect(nativeStatus).toBeDefined();
expect(registered).toContainEqual({ command: "custom_backup", description: "Git backup" });
expect(registered).not.toContainEqual({ command: "status", description: "Custom status" });
expect(registered.filter((command) => command.command === "status")).toEqual([nativeStatus]);
expect(errorSpy).toHaveBeenCalled();
});
it("registers custom commands when native commands are disabled", async () => {
const config = {
commands: { native: false },
channels: {
telegram: {
customCommands: [
{ command: "custom_backup", description: "Git backup" },
{ command: "custom_generate", description: "Create an image" },
],
},
},
};
loadConfig.mockReturnValue(config);
const commandsSynced = waitForNextSetMyCommands();
createTelegramBot({ token: "tok" });
await commandsSynced;
const registered = setMyCommandsSpy.mock.calls[0]?.[0] as Array<{
command: string;
description: string;
}>;
expect(registered).toEqual([
{ command: "custom_backup", description: "Git backup" },
{ command: "custom_generate", description: "Create an image" },
]);
const reserved = new Set(listNativeCommandSpecs().map((command) => command.name));
expect(registered.some((command) => reserved.has(command.command))).toBe(false);
});
});

View File

@ -1,14 +1,11 @@
import { resolveDefaultModelForAgent } from "openclaw/plugin-sdk/agent-runtime";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
import {
createReplyDispatcher,
resetInboundDedupe,
type GetReplyOptions,
type MsgContext,
type ReplyPayload,
} from "openclaw/plugin-sdk/reply-runtime";
import type { MockFn } from "openclaw/plugin-sdk/testing";
import { beforeEach, vi } from "vitest";
import { resolveDefaultModelForAgent } from "../../../src/agents/model-selection.js";
import { resetInboundDedupe } from "../../../src/auto-reply/reply/inbound-dedupe.js";
import { createReplyDispatcher } from "../../../src/auto-reply/reply/reply-dispatcher.js";
import type { MsgContext } from "../../../src/auto-reply/templating.js";
import type { GetReplyOptions, ReplyPayload } from "../../../src/auto-reply/types.js";
import type { TelegramBotDeps } from "./bot-deps.js";
type AnyMock = ReturnType<typeof vi.fn>;
@ -179,6 +176,12 @@ const buildModelsProviderData = modelProviderDataHoisted.buildModelsProviderData
export const replySpy = replySpyHoisted.replySpy;
export const dispatchReplyWithBufferedBlockDispatcher =
dispatchReplyHoisted.dispatchReplyWithBufferedBlockDispatcher;
const menuSyncHoisted = vi.hoisted(() => ({
syncTelegramMenuCommands: vi.fn(async ({ bot, commandsToRegister }) => {
await bot.api.setMyCommands(commandsToRegister);
}),
}));
export const syncTelegramMenuCommands = menuSyncHoisted.syncTelegramMenuCommands;
function parseModelRef(raw: string): { provider?: string; model: string } {
const trimmed = raw.trim();
@ -368,6 +371,7 @@ export const telegramBotDepsForTest: TelegramBotDeps = {
buildModelsProviderData: buildModelsProviderData as TelegramBotDeps["buildModelsProviderData"],
listSkillCommandsForAgents:
listSkillCommandsForAgents as TelegramBotDeps["listSkillCommandsForAgents"],
syncTelegramMenuCommands: syncTelegramMenuCommands as TelegramBotDeps["syncTelegramMenuCommands"],
wasSentByBot: wasSentByBot as TelegramBotDeps["wasSentByBot"],
resolveExecApproval: resolveExecApprovalSpy as NonNullable<
TelegramBotDeps["resolveExecApproval"]
@ -477,6 +481,10 @@ beforeEach(() => {
return await replySpy(dispatchParams.ctx, dispatchParams.replyOptions);
}),
);
syncTelegramMenuCommands.mockReset();
syncTelegramMenuCommands.mockImplementation(async ({ bot, commandsToRegister }) => {
await bot.api.setMyCommands(commandsToRegister);
});
sendAnimationSpy.mockReset();
sendAnimationSpy.mockResolvedValue({ message_id: 78 });

View File

@ -5,8 +5,6 @@ import {
registerPluginInteractiveHandler,
} from "openclaw/plugin-sdk/plugin-runtime";
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { escapeRegExp, formatEnvelopeTimestamp } from "../../../test/helpers/envelope-timestamp.js";
import { expectChannelInboundContextContract as expectInboundContextContract } from "./test-support/inbound-context-contract.js";
const {
answerCallbackQuerySpy,
commandSpy,
@ -29,10 +27,7 @@ const {
wasSentByBot,
} = await import("./bot.create-telegram-bot.test-harness.js");
let listNativeCommandSpecs: typeof import("../../../src/auto-reply/commands-registry.js").listNativeCommandSpecs;
let listNativeCommandSpecsForConfig: typeof import("../../../src/auto-reply/commands-registry.js").listNativeCommandSpecsForConfig;
let loadSessionStore: typeof import("../../../src/config/sessions.js").loadSessionStore;
let normalizeTelegramCommandName: typeof import("openclaw/plugin-sdk/config-runtime").normalizeTelegramCommandName;
let createTelegramBotBase: typeof import("./bot.js").createTelegramBot;
let setTelegramBotRuntimeForTest: typeof import("./bot.js").setTelegramBotRuntimeForTest;
let createTelegramBot: (
@ -59,15 +54,6 @@ function createSignal() {
return { promise, resolve };
}
function waitForNextSetMyCommands() {
const synced = createSignal();
setMyCommandsSpy.mockImplementationOnce(async () => {
synced.resolve();
return undefined;
});
return synced.promise;
}
function waitForReplyCalls(count: number) {
const done = createSignal();
let seen = 0;
@ -82,20 +68,18 @@ function waitForReplyCalls(count: number) {
return done.promise;
}
function resolveSkillCommands(config: Parameters<typeof listNativeCommandSpecsForConfig>[0]) {
void config;
return listSkillCommandsForAgents() as NonNullable<
Parameters<typeof listNativeCommandSpecsForConfig>[1]
>["skillCommands"];
async function loadEnvelopeTimestampHelpers() {
return await import("../../../test/helpers/envelope-timestamp.js");
}
async function loadInboundContextContract() {
return await import("./test-support/inbound-context-contract.js");
}
const ORIGINAL_TZ = process.env.TZ;
describe("createTelegramBot", () => {
beforeAll(async () => {
({ listNativeCommandSpecs, listNativeCommandSpecsForConfig } =
await import("../../../src/auto-reply/commands-registry.js"));
({ loadSessionStore } = await import("../../../src/config/sessions.js"));
({ normalizeTelegramCommandName } = await import("openclaw/plugin-sdk/config-runtime"));
({ createTelegramBot: createTelegramBotBase, setTelegramBotRuntimeForTest } =
await import("./bot.js"));
});
@ -129,127 +113,6 @@ describe("createTelegramBot", () => {
});
});
it("merges custom commands with native commands", async () => {
const config = {
channels: {
telegram: {
customCommands: [
{ command: "custom_backup", description: "Git backup" },
{ command: "/Custom_Generate", description: "Create an image" },
],
},
},
};
loadConfig.mockReturnValue(config);
const commandsSynced = waitForNextSetMyCommands();
createTelegramBot({
token: "tok",
config: {
channels: {
telegram: {
dmPolicy: "open",
allowFrom: ["*"],
execApprovals: {
enabled: true,
approvers: ["9"],
target: "dm",
},
},
},
},
});
await commandsSynced;
const registered = setMyCommandsSpy.mock.calls.at(-1)?.[0] as Array<{
command: string;
description: string;
}>;
const skillCommands = resolveSkillCommands(config);
const native = listNativeCommandSpecsForConfig(config, { skillCommands }).map((command) => ({
command: normalizeTelegramCommandName(command.name),
description: command.description,
}));
expect(registered.slice(0, native.length)).toEqual(native);
});
it("ignores custom commands that collide with native commands", async () => {
const errorSpy = vi.fn();
const config = {
channels: {
telegram: {
customCommands: [
{ command: "status", description: "Custom status" },
{ command: "custom_backup", description: "Git backup" },
],
},
},
};
loadConfig.mockReturnValue(config);
const commandsSynced = waitForNextSetMyCommands();
createTelegramBot({
token: "tok",
runtime: {
log: vi.fn(),
error: errorSpy,
exit: ((code: number) => {
throw new Error(`exit ${code}`);
}) as (code: number) => never,
},
});
await commandsSynced;
const registered = setMyCommandsSpy.mock.calls[0]?.[0] as Array<{
command: string;
description: string;
}>;
const skillCommands = resolveSkillCommands(config);
const native = listNativeCommandSpecsForConfig(config, { skillCommands }).map((command) => ({
command: normalizeTelegramCommandName(command.name),
description: command.description,
}));
const nativeStatus = native.find((command) => command.command === "status");
expect(nativeStatus).toBeDefined();
expect(registered).toContainEqual({ command: "custom_backup", description: "Git backup" });
expect(registered).not.toContainEqual({ command: "status", description: "Custom status" });
expect(registered.filter((command) => command.command === "status")).toEqual([nativeStatus]);
expect(errorSpy).toHaveBeenCalled();
});
it("registers custom commands when native commands are disabled", async () => {
const config = {
commands: { native: false },
channels: {
telegram: {
customCommands: [
{ command: "custom_backup", description: "Git backup" },
{ command: "custom_generate", description: "Create an image" },
],
},
},
};
loadConfig.mockReturnValue(config);
const commandsSynced = waitForNextSetMyCommands();
createTelegramBot({ token: "tok" });
await commandsSynced;
const registered = setMyCommandsSpy.mock.calls[0]?.[0] as Array<{
command: string;
description: string;
}>;
expect(registered).toEqual([
{ command: "custom_backup", description: "Git backup" },
{ command: "custom_generate", description: "Create an image" },
]);
const reserved = new Set(listNativeCommandSpecs().map((command) => command.name));
expect(registered.some((command) => reserved.has(command.command))).toBe(false);
});
it("blocks callback_query when inline buttons are allowlist-only and sender not authorized", async () => {
onSpy.mockClear();
replySpy.mockClear();
@ -1195,6 +1058,9 @@ describe("createTelegramBot", () => {
expect(replySpy).toHaveBeenCalledTimes(1);
const payload = replySpy.mock.calls[0][0];
const { expectChannelInboundContextContract: expectInboundContextContract } =
await loadInboundContextContract();
const { escapeRegExp, formatEnvelopeTimestamp } = await loadEnvelopeTimestampHelpers();
expectInboundContextContract(payload);
const expectedTimestamp = formatEnvelopeTimestamp(new Date("2025-01-09T00:00:00Z"));
const timestampPattern = escapeRegExp(expectedTimestamp);