refactor(telegram): unify action normalization

This commit is contained in:
Ayaan Zaidi 2026-03-18 09:14:50 +05:30
parent 4c9028439c
commit a89cb3e10e
No known key found for this signature in database
5 changed files with 312 additions and 393 deletions

View File

@ -236,6 +236,31 @@ describe("handleTelegramAction", () => {
);
});
it("accepts shared sticker action aliases", async () => {
const cfg = {
channels: { telegram: { botToken: "tok", actions: { sticker: true } } },
} as OpenClawConfig;
await handleTelegramAction(
{
action: "sticker",
target: "123",
stickerId: ["sticker"],
replyTo: 9,
threadId: 11,
},
cfg,
);
expect(sendStickerTelegram).toHaveBeenCalledWith(
"123",
"sticker",
expect.objectContaining({
token: "tok",
replyToMessageId: 9,
messageThreadId: 11,
}),
);
});
it("removes reactions when remove flag set", async () => {
const cfg = reactionConfig("extensive");
await handleTelegramAction(
@ -320,6 +345,26 @@ describe("handleTelegramAction", () => {
});
});
it("accepts shared send action aliases", async () => {
await handleTelegramAction(
{
action: "send",
to: "@testchannel",
message: "Hello from alias",
media: "https://example.com/image.jpg",
},
telegramConfig(),
);
expect(sendMessageTelegram).toHaveBeenCalledWith(
"@testchannel",
"Hello from alias",
expect.objectContaining({
token: "tok",
mediaUrl: "https://example.com/image.jpg",
}),
);
});
it("sends a poll", async () => {
const result = await handleTelegramAction(
{
@ -357,6 +402,41 @@ describe("handleTelegramAction", () => {
});
});
it("accepts shared poll action aliases", async () => {
await handleTelegramAction(
{
action: "poll",
to: "@testchannel",
pollQuestion: "Ready?",
pollOption: ["Yes", "No"],
pollMulti: "true",
pollPublic: "true",
pollDurationSeconds: 60,
replyTo: 55,
threadId: 77,
silent: "true",
},
telegramConfig(),
);
expect(sendPollTelegram).toHaveBeenCalledWith(
"@testchannel",
{
question: "Ready?",
options: ["Yes", "No"],
maxSelections: 2,
durationSeconds: 60,
durationHours: undefined,
},
expect.objectContaining({
token: "tok",
isAnonymous: false,
replyToMessageId: 55,
messageThreadId: 77,
silent: true,
}),
);
});
it("parses string booleans for poll flags", async () => {
await handleTelegramAction(
{

View File

@ -1,5 +1,7 @@
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
import { readBooleanParam } from "openclaw/plugin-sdk/boolean-param";
import { resolveReactionMessageId } from "openclaw/plugin-sdk/channel-runtime";
import { resolveTelegramPollVisibility } from "openclaw/plugin-sdk/telegram";
import {
jsonResult,
readNumberParam,
@ -13,6 +15,7 @@ import {
} from "openclaw/plugin-sdk/telegram-core";
import { createTelegramActionGate, resolveTelegramPollActionGateState } from "./accounts.js";
import type { TelegramButtonStyle, TelegramInlineButtons } from "./button-types.js";
import { resolveTelegramInlineButtons } from "./button-types.js";
import {
resolveTelegramInlineButtonsScope,
resolveTelegramTargetChatType,
@ -45,6 +48,27 @@ export const telegramActionRuntime = {
};
const TELEGRAM_BUTTON_STYLES: readonly TelegramButtonStyle[] = ["danger", "success", "primary"];
const TELEGRAM_ACTION_ALIASES = {
createForumTopic: "createForumTopic",
delete: "deleteMessage",
deleteMessage: "deleteMessage",
edit: "editMessage",
editForumTopic: "editForumTopic",
editMessage: "editMessage",
poll: "poll",
react: "react",
searchSticker: "searchSticker",
send: "sendMessage",
sendMessage: "sendMessage",
sendSticker: "sendSticker",
sticker: "sendSticker",
stickerCacheStats: "stickerCacheStats",
"sticker-search": "searchSticker",
"topic-create": "createForumTopic",
"topic-edit": "editForumTopic",
} as const;
type TelegramActionName = (typeof TELEGRAM_ACTION_ALIASES)[keyof typeof TELEGRAM_ACTION_ALIASES];
export function readTelegramButtons(
params: Record<string, unknown>,
@ -101,6 +125,58 @@ export function readTelegramButtons(
return filtered.length > 0 ? filtered : undefined;
}
function normalizeTelegramActionName(action: string): TelegramActionName {
const normalized = TELEGRAM_ACTION_ALIASES[action as keyof typeof TELEGRAM_ACTION_ALIASES];
if (!normalized) {
throw new Error(`Unsupported Telegram action: ${action}`);
}
return normalized;
}
function readTelegramChatId(params: Record<string, unknown>) {
return (
readStringOrNumberParam(params, "chatId") ??
readStringOrNumberParam(params, "channelId") ??
readStringOrNumberParam(params, "to", { required: true })
);
}
function readTelegramThreadId(params: Record<string, unknown>) {
return (
readNumberParam(params, "messageThreadId", { integer: true }) ??
readNumberParam(params, "threadId", { integer: true })
);
}
function readTelegramReplyToMessageId(params: Record<string, unknown>) {
return (
readNumberParam(params, "replyToMessageId", { integer: true }) ??
readNumberParam(params, "replyTo", { integer: true })
);
}
function resolveTelegramButtonsFromParams(params: Record<string, unknown>) {
return resolveTelegramInlineButtons({
buttons: readTelegramButtons(params),
interactive: params.interactive,
});
}
function readTelegramSendContent(params: {
args: Record<string, unknown>;
mediaUrl?: string;
hasButtons: boolean;
}) {
const content =
readStringParam(params.args, "content", { allowEmpty: true }) ??
readStringParam(params.args, "message", { allowEmpty: true }) ??
readStringParam(params.args, "caption", { allowEmpty: true });
if (content == null && !params.mediaUrl && !params.hasButtons) {
throw new Error("content required.");
}
return content ?? "";
}
export async function handleTelegramAction(
params: Record<string, unknown>,
cfg: OpenClawConfig,
@ -109,7 +185,7 @@ export async function handleTelegramAction(
},
): Promise<AgentToolResult<unknown>> {
const { action, accountId } = {
action: readStringParam(params, "action", { required: true }),
action: normalizeTelegramActionName(readStringParam(params, "action", { required: true })),
accountId: readStringParam(params, "accountId"),
};
const isActionEnabled = createTelegramActionGate({
@ -139,12 +215,10 @@ export async function handleTelegramAction(
hint: "Telegram reactions are disabled via actions.reactions. Do not retry.",
});
}
const chatId = readStringOrNumberParam(params, "chatId", {
required: true,
});
const messageId = readNumberParam(params, "messageId", {
integer: true,
});
const chatId = readTelegramChatId(params);
const messageId =
readNumberParam(params, "messageId", { integer: true }) ??
resolveReactionMessageId({ args: params });
if (typeof messageId !== "number" || !Number.isFinite(messageId) || messageId <= 0) {
return jsonResult({
ok: false,
@ -205,14 +279,17 @@ export async function handleTelegramAction(
throw new Error("Telegram sendMessage is disabled.");
}
const to = readStringParam(params, "to", { required: true });
const mediaUrl = readStringParam(params, "mediaUrl");
// Allow content to be omitted when sending media-only (e.g., voice notes)
const content =
readStringParam(params, "content", {
required: !mediaUrl,
allowEmpty: true,
}) ?? "";
const buttons = readTelegramButtons(params);
const mediaUrl =
readStringParam(params, "mediaUrl") ??
readStringParam(params, "media", {
trim: false,
});
const buttons = resolveTelegramButtonsFromParams(params);
const content = readTelegramSendContent({
args: params,
mediaUrl: mediaUrl ?? undefined,
hasButtons: Array.isArray(buttons) && buttons.length > 0,
});
if (buttons) {
const inlineButtonsScope = resolveTelegramInlineButtonsScope({
cfg,
@ -241,12 +318,8 @@ export async function handleTelegramAction(
}
}
// Optional threading parameters for forum topics and reply chains
const replyToMessageId = readNumberParam(params, "replyToMessageId", {
integer: true,
});
const messageThreadId = readNumberParam(params, "messageThreadId", {
integer: true,
});
const replyToMessageId = readTelegramReplyToMessageId(params);
const messageThreadId = readTelegramThreadId(params);
const quoteText = readStringParam(params, "quoteText");
const token = resolveTelegramToken(cfg, { accountId }).token;
if (!token) {
@ -284,18 +357,34 @@ export async function handleTelegramAction(
throw new Error("Telegram polls are disabled.");
}
const to = readStringParam(params, "to", { required: true });
const question = readStringParam(params, "question", { required: true });
const answers = readStringArrayParam(params, "answers", { required: true });
const allowMultiselect = readBooleanParam(params, "allowMultiselect") ?? false;
const durationSeconds = readNumberParam(params, "durationSeconds", { integer: true });
const durationHours = readNumberParam(params, "durationHours", { integer: true });
const replyToMessageId = readNumberParam(params, "replyToMessageId", {
integer: true,
});
const messageThreadId = readNumberParam(params, "messageThreadId", {
integer: true,
});
const isAnonymous = readBooleanParam(params, "isAnonymous");
const question =
readStringParam(params, "question") ??
readStringParam(params, "pollQuestion", { required: true });
const answers =
readStringArrayParam(params, "answers") ??
readStringArrayParam(params, "pollOption", { required: true });
const allowMultiselect =
readBooleanParam(params, "allowMultiselect") ?? readBooleanParam(params, "pollMulti");
const durationSeconds =
readNumberParam(params, "durationSeconds", { integer: true }) ??
readNumberParam(params, "pollDurationSeconds", {
integer: true,
strict: true,
});
const durationHours =
readNumberParam(params, "durationHours", { integer: true }) ??
readNumberParam(params, "pollDurationHours", {
integer: true,
strict: true,
});
const replyToMessageId = readTelegramReplyToMessageId(params);
const messageThreadId = readTelegramThreadId(params);
const isAnonymous =
readBooleanParam(params, "isAnonymous") ??
resolveTelegramPollVisibility({
pollAnonymous: readBooleanParam(params, "pollAnonymous"),
pollPublic: readBooleanParam(params, "pollPublic"),
});
const silent = readBooleanParam(params, "silent");
const token = resolveTelegramToken(cfg, { accountId }).token;
if (!token) {
@ -308,7 +397,7 @@ export async function handleTelegramAction(
{
question,
options: answers,
maxSelections: resolvePollMaxSelections(answers.length, allowMultiselect),
maxSelections: resolvePollMaxSelections(answers.length, allowMultiselect ?? false),
durationSeconds: durationSeconds ?? undefined,
durationHours: durationHours ?? undefined,
},
@ -334,9 +423,7 @@ export async function handleTelegramAction(
if (!isActionEnabled("deleteMessage")) {
throw new Error("Telegram deleteMessage is disabled.");
}
const chatId = readStringOrNumberParam(params, "chatId", {
required: true,
});
const chatId = readTelegramChatId(params);
const messageId = readNumberParam(params, "messageId", {
required: true,
integer: true,
@ -359,18 +446,15 @@ export async function handleTelegramAction(
if (!isActionEnabled("editMessage")) {
throw new Error("Telegram editMessage is disabled.");
}
const chatId = readStringOrNumberParam(params, "chatId", {
required: true,
});
const chatId = readTelegramChatId(params);
const messageId = readNumberParam(params, "messageId", {
required: true,
integer: true,
});
const content = readStringParam(params, "content", {
required: true,
allowEmpty: false,
});
const buttons = readTelegramButtons(params);
const content =
readStringParam(params, "content", { allowEmpty: false }) ??
readStringParam(params, "message", { required: true, allowEmpty: false });
const buttons = resolveTelegramButtonsFromParams(params);
if (buttons) {
const inlineButtonsScope = resolveTelegramInlineButtonsScope({
cfg,
@ -412,14 +496,15 @@ export async function handleTelegramAction(
"Telegram sticker actions are disabled. Set channels.telegram.actions.sticker to true.",
);
}
const to = readStringParam(params, "to", { required: true });
const fileId = readStringParam(params, "fileId", { required: true });
const replyToMessageId = readNumberParam(params, "replyToMessageId", {
integer: true,
});
const messageThreadId = readNumberParam(params, "messageThreadId", {
integer: true,
});
const to =
readStringParam(params, "to") ?? readStringParam(params, "target", { required: true });
const fileId =
readStringParam(params, "fileId") ?? readStringArrayParam(params, "stickerId")?.[0];
if (!fileId) {
throw new Error("fileId is required.");
}
const replyToMessageId = readTelegramReplyToMessageId(params);
const messageThreadId = readTelegramThreadId(params);
const token = resolveTelegramToken(cfg, { accountId }).token;
if (!token) {
throw new Error(
@ -470,9 +555,7 @@ export async function handleTelegramAction(
if (!isActionEnabled("createForumTopic")) {
throw new Error("Telegram createForumTopic is disabled.");
}
const chatId = readStringOrNumberParam(params, "chatId", {
required: true,
});
const chatId = readTelegramChatId(params);
const name = readStringParam(params, "name", { required: true });
const iconColor = readNumberParam(params, "iconColor", { integer: true });
const iconCustomEmojiId = readStringParam(params, "iconCustomEmojiId");
@ -501,12 +584,8 @@ export async function handleTelegramAction(
if (!isActionEnabled("editForumTopic")) {
throw new Error("Telegram editForumTopic is disabled.");
}
const chatId = readStringOrNumberParam(params, "chatId", {
required: true,
});
const messageThreadId =
readNumberParam(params, "messageThreadId", { integer: true }) ??
readNumberParam(params, "threadId", { integer: true });
const chatId = readTelegramChatId(params);
const messageThreadId = readTelegramThreadId(params);
if (typeof messageThreadId !== "number") {
throw new Error("messageThreadId or threadId is required.");
}

View File

@ -42,8 +42,14 @@ describe("telegramMessageActions", () => {
expect.objectContaining({
action: "sendMessage",
to: "123456",
content: "",
buttons: [[{ text: "Approve", callback_data: "approve", style: "success" }]],
interactive: {
blocks: [
{
type: "buttons",
buttons: [{ label: "Approve", value: "approve", style: "success" }],
},
],
},
accountId: "default",
}),
expect.anything(),

View File

@ -1,25 +1,15 @@
import {
readNumberParam,
readStringArrayParam,
readStringOrNumberParam,
readStringParam,
} from "openclaw/plugin-sdk/agent-runtime";
import { readBooleanParam } from "openclaw/plugin-sdk/boolean-param";
import { resolveReactionMessageId } from "openclaw/plugin-sdk/channel-runtime";
import {
createMessageToolButtonsSchema,
createTelegramPollExtraToolSchemas,
createUnionActionGate,
listTokenSourcedAccounts,
} from "openclaw/plugin-sdk/channel-runtime";
import type {
ChannelMessageActionAdapter,
ChannelMessageActionName,
ChannelMessageToolDiscovery,
ChannelMessageToolSchemaContribution,
resolveReactionMessageId,
type ChannelMessageActionAdapter,
type ChannelMessageActionName,
type ChannelMessageToolDiscovery,
type ChannelMessageToolSchemaContribution,
} from "openclaw/plugin-sdk/channel-runtime";
import type { TelegramActionConfig } from "openclaw/plugin-sdk/config-runtime";
import { resolveTelegramPollVisibility } from "openclaw/plugin-sdk/telegram";
import { extractToolSend } from "openclaw/plugin-sdk/tool-send";
import {
createTelegramActionGate,
@ -27,15 +17,28 @@ import {
resolveTelegramPollActionGateState,
} from "./accounts.js";
import { handleTelegramAction } from "./action-runtime.js";
import { resolveTelegramInlineButtons } from "./button-types.js";
import { isTelegramInlineButtonsEnabled } from "./inline-buttons.js";
const providerId = "telegram";
export const telegramMessageActionRuntime = {
handleTelegramAction,
};
const TELEGRAM_MESSAGE_ACTION_MAP = {
delete: "deleteMessage",
edit: "editMessage",
poll: "poll",
react: "react",
send: "sendMessage",
sticker: "sendSticker",
"sticker-search": "searchSticker",
"topic-create": "createForumTopic",
"topic-edit": "editForumTopic",
} as const satisfies Partial<Record<ChannelMessageActionName, string>>;
function resolveTelegramMessageActionName(action: ChannelMessageActionName) {
return TELEGRAM_MESSAGE_ACTION_MAP[action as keyof typeof TELEGRAM_MESSAGE_ACTION_MAP];
}
function resolveTelegramActionDiscovery(cfg: Parameters<typeof listEnabledTelegramAccounts>[0]) {
const accounts = listTokenSourcedAccounts(listEnabledTelegramAccounts(cfg));
if (accounts.length === 0) {
@ -122,249 +125,29 @@ function describeTelegramMessageTool({
};
}
function readTelegramSendParams(params: Record<string, unknown>) {
const to = readStringParam(params, "to", { required: true });
const mediaUrl = readStringParam(params, "media", { trim: false });
const buttons = resolveTelegramInlineButtons({
buttons: params.buttons as ReturnType<typeof resolveTelegramInlineButtons>,
interactive: params.interactive,
});
const hasButtons = Array.isArray(buttons) && buttons.length > 0;
const message = readStringParam(params, "message", {
required: !mediaUrl && !hasButtons,
allowEmpty: true,
});
const caption = readStringParam(params, "caption", { allowEmpty: true });
const content = message || caption || "";
const replyTo = readStringParam(params, "replyTo");
const threadId = readStringParam(params, "threadId");
const asVoice = readBooleanParam(params, "asVoice");
const silent = readBooleanParam(params, "silent");
const forceDocument = readBooleanParam(params, "forceDocument");
const quoteText = readStringParam(params, "quoteText");
return {
to,
content,
mediaUrl: mediaUrl ?? undefined,
replyToMessageId: replyTo ?? undefined,
messageThreadId: threadId ?? undefined,
buttons,
asVoice,
silent,
forceDocument,
quoteText: quoteText ?? undefined,
};
}
function readTelegramChatIdParam(params: Record<string, unknown>): string | number {
return (
readStringOrNumberParam(params, "chatId") ??
readStringOrNumberParam(params, "channelId") ??
readStringParam(params, "to", { required: true })
);
}
function readTelegramMessageIdParam(params: Record<string, unknown>): number {
const messageId = readNumberParam(params, "messageId", {
required: true,
integer: true,
});
if (typeof messageId !== "number") {
throw new Error("messageId is required.");
}
return messageId;
}
export const telegramMessageActions: ChannelMessageActionAdapter = {
describeMessageTool: describeTelegramMessageTool,
extractToolSend: ({ args }) => {
return extractToolSend(args, "sendMessage");
},
handleAction: async ({ action, params, cfg, accountId, mediaLocalRoots, toolContext }) => {
if (action === "send") {
const sendParams = readTelegramSendParams(params);
return await telegramMessageActionRuntime.handleTelegramAction(
{
action: "sendMessage",
...sendParams,
accountId: accountId ?? undefined,
},
cfg,
{ mediaLocalRoots },
);
const telegramAction = resolveTelegramMessageActionName(action);
if (!telegramAction) {
throw new Error(`Unsupported Telegram action: ${action}`);
}
if (action === "react") {
const messageId = resolveReactionMessageId({ args: params, toolContext });
const emoji = readStringParam(params, "emoji", { allowEmpty: true });
const remove = readBooleanParam(params, "remove");
return await telegramMessageActionRuntime.handleTelegramAction(
{
action: "react",
chatId: readTelegramChatIdParam(params),
messageId,
emoji,
remove,
accountId: accountId ?? undefined,
},
cfg,
{ mediaLocalRoots },
);
}
if (action === "poll") {
const to = readStringParam(params, "to", { required: true });
const question = readStringParam(params, "pollQuestion", { required: true });
const answers = readStringArrayParam(params, "pollOption", { required: true });
const durationHours = readNumberParam(params, "pollDurationHours", {
integer: true,
strict: true,
});
const durationSeconds = readNumberParam(params, "pollDurationSeconds", {
integer: true,
strict: true,
});
const replyToMessageId = readNumberParam(params, "replyTo", { integer: true });
const messageThreadId = readNumberParam(params, "threadId", { integer: true });
const allowMultiselect = readBooleanParam(params, "pollMulti");
const pollAnonymous = readBooleanParam(params, "pollAnonymous");
const pollPublic = readBooleanParam(params, "pollPublic");
const isAnonymous = resolveTelegramPollVisibility({ pollAnonymous, pollPublic });
const silent = readBooleanParam(params, "silent");
return await telegramMessageActionRuntime.handleTelegramAction(
{
action: "poll",
to,
question,
answers,
allowMultiselect,
durationHours: durationHours ?? undefined,
durationSeconds: durationSeconds ?? undefined,
replyToMessageId: replyToMessageId ?? undefined,
messageThreadId: messageThreadId ?? undefined,
isAnonymous,
silent,
accountId: accountId ?? undefined,
},
cfg,
{ mediaLocalRoots },
);
}
if (action === "delete") {
const chatId = readTelegramChatIdParam(params);
const messageId = readTelegramMessageIdParam(params);
return await telegramMessageActionRuntime.handleTelegramAction(
{
action: "deleteMessage",
chatId,
messageId,
accountId: accountId ?? undefined,
},
cfg,
{ mediaLocalRoots },
);
}
if (action === "edit") {
const chatId = readTelegramChatIdParam(params);
const messageId = readTelegramMessageIdParam(params);
const message = readStringParam(params, "message", { required: true, allowEmpty: false });
const buttons = params.buttons;
return await telegramMessageActionRuntime.handleTelegramAction(
{
action: "editMessage",
chatId,
messageId,
content: message,
buttons,
accountId: accountId ?? undefined,
},
cfg,
{ mediaLocalRoots },
);
}
if (action === "sticker") {
const to =
readStringParam(params, "to") ?? readStringParam(params, "target", { required: true });
// Accept stickerId (array from shared schema) and use first element as fileId
const stickerIds = readStringArrayParam(params, "stickerId");
const fileId = stickerIds?.[0] ?? readStringParam(params, "fileId", { required: true });
const replyToMessageId = readNumberParam(params, "replyTo", { integer: true });
const messageThreadId = readNumberParam(params, "threadId", { integer: true });
return await telegramMessageActionRuntime.handleTelegramAction(
{
action: "sendSticker",
to,
fileId,
replyToMessageId: replyToMessageId ?? undefined,
messageThreadId: messageThreadId ?? undefined,
accountId: accountId ?? undefined,
},
cfg,
{ mediaLocalRoots },
);
}
if (action === "sticker-search") {
const query = readStringParam(params, "query", { required: true });
const limit = readNumberParam(params, "limit", { integer: true });
return await telegramMessageActionRuntime.handleTelegramAction(
{
action: "searchSticker",
query,
limit: limit ?? undefined,
accountId: accountId ?? undefined,
},
cfg,
{ mediaLocalRoots },
);
}
if (action === "topic-create") {
const chatId = readTelegramChatIdParam(params);
const name = readStringParam(params, "name", { required: true });
const iconColor = readNumberParam(params, "iconColor", { integer: true });
const iconCustomEmojiId = readStringParam(params, "iconCustomEmojiId");
return await telegramMessageActionRuntime.handleTelegramAction(
{
action: "createForumTopic",
chatId,
name,
iconColor: iconColor ?? undefined,
iconCustomEmojiId: iconCustomEmojiId ?? undefined,
accountId: accountId ?? undefined,
},
cfg,
{ mediaLocalRoots },
);
}
if (action === "topic-edit") {
const chatId = readTelegramChatIdParam(params);
const messageThreadId =
readNumberParam(params, "messageThreadId", { integer: true }) ??
readNumberParam(params, "threadId", { integer: true });
if (typeof messageThreadId !== "number") {
throw new Error("messageThreadId or threadId is required.");
}
const name = readStringParam(params, "name");
const iconCustomEmojiId = readStringParam(params, "iconCustomEmojiId");
return await telegramMessageActionRuntime.handleTelegramAction(
{
action: "editForumTopic",
chatId,
messageThreadId,
name: name ?? undefined,
iconCustomEmojiId: iconCustomEmojiId ?? undefined,
accountId: accountId ?? undefined,
},
cfg,
{ mediaLocalRoots },
);
}
throw new Error(`Action ${action} is not supported for provider ${providerId}.`);
return await telegramMessageActionRuntime.handleTelegramAction(
{
...params,
action: telegramAction,
accountId: accountId ?? undefined,
...(action === "react"
? {
messageId: resolveReactionMessageId({ args: params, toolContext }),
}
: {}),
},
cfg,
{ mediaLocalRoots },
);
},
};

View File

@ -25,9 +25,9 @@ vi.mock("../../../../extensions/slack/src/action-runtime.js", () => ({
handleSlackAction,
}));
let discordMessageActions: typeof import("./discord.js").discordMessageActions;
let handleDiscordMessageAction: typeof import("./discord/handle-action.js").handleDiscordMessageAction;
let telegramMessageActions: typeof import("./telegram.js").telegramMessageActions;
let discordMessageActions: typeof import("../../../../extensions/discord/src/channel-actions.js").discordMessageActions;
let handleDiscordMessageAction: typeof import("../../../../extensions/discord/src/actions/handle-action.js").handleDiscordMessageAction;
let telegramMessageActions: typeof import("../../../../extensions/telegram/src/channel-actions.js").telegramMessageActions;
let signalMessageActions: typeof import("../../../../extensions/signal/src/message-actions.js").signalMessageActions;
let createSlackActions: typeof import("../../../../extensions/slack/src/channel-actions.js").createSlackActions;
@ -201,9 +201,12 @@ async function expectSlackSendRejected(params: Record<string, unknown>, error: R
beforeEach(async () => {
vi.resetModules();
({ discordMessageActions } = await import("./discord.js"));
({ handleDiscordMessageAction } = await import("./discord/handle-action.js"));
({ telegramMessageActions } = await import("./telegram.js"));
({ discordMessageActions } =
await import("../../../../extensions/discord/src/channel-actions.js"));
({ handleDiscordMessageAction } =
await import("../../../../extensions/discord/src/actions/handle-action.js"));
({ telegramMessageActions } =
await import("../../../../extensions/telegram/src/channel-actions.js"));
({ signalMessageActions } = await import("../../../../extensions/signal/src/message-actions.js"));
({ createSlackActions } = await import("../../../../extensions/slack/src/channel-actions.js"));
vi.clearAllMocks();
@ -708,7 +711,7 @@ describe("telegramMessageActions", () => {
}
});
it("maps action params into telegram actions", async () => {
it("forwards telegram action aliases into the runtime seam", async () => {
const cases = [
{
name: "media-only send preserves asVoice",
@ -721,8 +724,7 @@ describe("telegramMessageActions", () => {
expectedPayload: expect.objectContaining({
action: "sendMessage",
to: "123",
content: "",
mediaUrl: "https://example.com/voice.ogg",
media: "https://example.com/voice.ogg",
asVoice: true,
}),
},
@ -737,7 +739,7 @@ describe("telegramMessageActions", () => {
expectedPayload: expect.objectContaining({
action: "sendMessage",
to: "456",
content: "Silent notification test",
message: "Silent notification test",
silent: true,
}),
},
@ -754,7 +756,7 @@ describe("telegramMessageActions", () => {
action: "editMessage",
chatId: "123",
messageId: 42,
content: "Updated",
message: "Updated",
buttons: [],
accountId: undefined,
},
@ -776,20 +778,19 @@ describe("telegramMessageActions", () => {
expectedPayload: {
action: "poll",
to: "123",
question: "Ready?",
answers: ["Yes", "No"],
allowMultiselect: true,
durationHours: undefined,
durationSeconds: 60,
replyToMessageId: 55,
messageThreadId: 77,
isAnonymous: false,
pollQuestion: "Ready?",
pollOption: ["Yes", "No"],
pollMulti: true,
pollDurationSeconds: 60,
pollPublic: true,
replyTo: 55,
threadId: 77,
silent: true,
accountId: undefined,
},
},
{
name: "poll parses string booleans before telegram action handoff",
name: "poll forwards raw alias flags to telegram runtime",
action: "poll" as const,
params: {
to: "123",
@ -802,20 +803,16 @@ describe("telegramMessageActions", () => {
expectedPayload: {
action: "poll",
to: "123",
question: "Ready?",
answers: ["Yes", "No"],
allowMultiselect: true,
durationHours: undefined,
durationSeconds: undefined,
replyToMessageId: undefined,
messageThreadId: undefined,
isAnonymous: false,
silent: true,
pollQuestion: "Ready?",
pollOption: ["Yes", "No"],
pollMulti: "true",
pollPublic: "true",
silent: "true",
accountId: undefined,
},
},
{
name: "poll rejects partially numeric duration strings before telegram action handoff",
name: "poll forwards duration strings for runtime validation",
action: "poll" as const,
params: {
to: "123",
@ -826,15 +823,9 @@ describe("telegramMessageActions", () => {
expectedPayload: {
action: "poll",
to: "123",
question: "Ready?",
answers: ["Yes", "No"],
allowMultiselect: undefined,
durationHours: undefined,
durationSeconds: undefined,
replyToMessageId: undefined,
messageThreadId: undefined,
isAnonymous: undefined,
silent: undefined,
pollQuestion: "Ready?",
pollOption: ["Yes", "No"],
pollDurationSeconds: "60s",
accountId: undefined,
},
},
@ -847,10 +838,8 @@ describe("telegramMessageActions", () => {
},
expectedPayload: {
action: "createForumTopic",
chatId: "telegram:group:-1001234567890:topic:271",
to: "telegram:group:-1001234567890:topic:271",
name: "Build Updates",
iconColor: undefined,
iconCustomEmojiId: undefined,
accountId: undefined,
},
},
@ -865,8 +854,8 @@ describe("telegramMessageActions", () => {
},
expectedPayload: {
action: "editForumTopic",
chatId: "telegram:group:-1001234567890:topic:271",
messageThreadId: 271,
to: "telegram:group:-1001234567890:topic:271",
threadId: 271,
name: "Build Updates",
iconCustomEmojiId: "emoji-123",
accountId: undefined,
@ -885,30 +874,6 @@ describe("telegramMessageActions", () => {
}
});
it("rejects non-integer messageId for edit before reaching telegram-actions", async () => {
const cfg = telegramCfg();
const handleAction = telegramMessageActions.handleAction;
if (!handleAction) {
throw new Error("telegram handleAction unavailable");
}
await expect(
handleAction({
channel: "telegram",
action: "edit",
params: {
chatId: "123",
messageId: "nope",
message: "Updated",
},
cfg,
accountId: undefined,
}),
).rejects.toThrow();
expect(handleTelegramAction).not.toHaveBeenCalled();
});
it("inherits top-level reaction gate when account overrides sticker only", () => {
const cfg = {
channels: {
@ -941,7 +906,8 @@ describe("telegramMessageActions", () => {
emoji: "ok",
},
toolContext: undefined,
expectedChatId: "123",
expectedChannelField: "channelId",
expectedChannelValue: "123",
expectedMessageId: "456",
},
{
@ -952,7 +918,8 @@ describe("telegramMessageActions", () => {
emoji: "ok",
},
toolContext: undefined,
expectedChatId: "123",
expectedChannelField: "channelId",
expectedChannelValue: "123",
expectedMessageId: "456",
},
{
@ -962,7 +929,8 @@ describe("telegramMessageActions", () => {
emoji: "ok",
},
toolContext: { currentMessageId: "9001" },
expectedChatId: "123",
expectedChannelField: "chatId",
expectedChannelValue: "123",
expectedMessageId: "9001",
},
{
@ -972,7 +940,8 @@ describe("telegramMessageActions", () => {
emoji: "ok",
},
toolContext: undefined,
expectedChatId: "123",
expectedChannelField: "chatId",
expectedChannelValue: "123",
expectedMessageId: undefined,
},
] as const) {
@ -995,7 +964,9 @@ describe("telegramMessageActions", () => {
}
const callPayload = call as Record<string, unknown>;
expect(callPayload.action, testCase.name).toBe("react");
expect(String(callPayload.chatId), testCase.name).toBe(testCase.expectedChatId);
expect(String(callPayload[testCase.expectedChannelField]), testCase.name).toBe(
testCase.expectedChannelValue,
);
if (testCase.expectedMessageId === undefined) {
expect(callPayload.messageId, testCase.name).toBeUndefined();
} else {
@ -1048,7 +1019,7 @@ it("forwards trusted mediaLocalRoots for send actions", async () => {
expect(handleTelegramAction).toHaveBeenCalledWith(
expect.objectContaining({
action: "sendMessage",
mediaUrl: "/tmp/voice.ogg",
media: "/tmp/voice.ogg",
}),
expect.any(Object),
expect.objectContaining({ mediaLocalRoots: ["/tmp/agent-root"] }),