From 4640999e7778109448933dacd0fab702d72e3484 Mon Sep 17 00:00:00 2001 From: El-Fitz <8971906+El-Fitz@users.noreply.github.com> Date: Mon, 16 Feb 2026 14:37:35 +0100 Subject: [PATCH] test: add per-account action gating tests for Discord and Telegram handlers --- src/agents/tools/discord-actions.e2e.test.ts | 65 ++++++++++- src/agents/tools/telegram-actions.e2e.test.ts | 67 +++++++++++ src/agents/tools/telegram-actions.ts | 2 +- src/channels/plugins/actions/actions.test.ts | 107 ++++++++++++++++++ 4 files changed, 239 insertions(+), 2 deletions(-) diff --git a/src/agents/tools/discord-actions.e2e.test.ts b/src/agents/tools/discord-actions.e2e.test.ts index 1452c0626ca..b7ed18685c4 100644 --- a/src/agents/tools/discord-actions.e2e.test.ts +++ b/src/agents/tools/discord-actions.e2e.test.ts @@ -1,8 +1,9 @@ import { describe, expect, it, vi } from "vitest"; -import type { DiscordActionConfig } from "../../config/config.js"; +import type { DiscordActionConfig, OpenClawConfig } from "../../config/config.js"; import { handleDiscordGuildAction } from "./discord-actions-guild.js"; import { handleDiscordMessagingAction } from "./discord-actions-messaging.js"; import { handleDiscordModerationAction } from "./discord-actions-moderation.js"; +import { handleDiscordAction } from "./discord-actions.js"; const createChannelDiscord = vi.fn(async () => ({ id: "new-channel", @@ -596,3 +597,65 @@ describe("handleDiscordModerationAction", () => { ); }); }); + +describe("handleDiscordAction per-account gating", () => { + it("allows moderation when account config enables it", async () => { + const cfg = { + channels: { + discord: { + accounts: { + ops: { token: "tok-ops", actions: { moderation: true } }, + }, + }, + }, + } as OpenClawConfig; + + await handleDiscordAction( + { action: "timeout", guildId: "G1", userId: "U1", durationMinutes: 5, accountId: "ops" }, + cfg, + ); + expect(timeoutMemberDiscord).toHaveBeenCalledWith( + expect.objectContaining({ guildId: "G1", userId: "U1" }), + { accountId: "ops" }, + ); + }); + + it("blocks moderation when account omits it", async () => { + const cfg = { + channels: { + discord: { + accounts: { + chat: { token: "tok-chat" }, + }, + }, + }, + } as OpenClawConfig; + + await expect( + handleDiscordAction( + { action: "timeout", guildId: "G1", userId: "U1", durationMinutes: 5, accountId: "chat" }, + cfg, + ), + ).rejects.toThrow(/Discord moderation is disabled/); + }); + + it("uses account-merged config, not top-level config", async () => { + // Top-level has no moderation, but the account does + const cfg = { + channels: { + discord: { + token: "tok-base", + accounts: { + ops: { token: "tok-ops", actions: { moderation: true } }, + }, + }, + }, + } as OpenClawConfig; + + await handleDiscordAction( + { action: "kick", guildId: "G1", userId: "U1", accountId: "ops" }, + cfg, + ); + expect(kickMemberDiscord).toHaveBeenCalled(); + }); +}); diff --git a/src/agents/tools/telegram-actions.e2e.test.ts b/src/agents/tools/telegram-actions.e2e.test.ts index f9b9ffcc877..37688b6fd5a 100644 --- a/src/agents/tools/telegram-actions.e2e.test.ts +++ b/src/agents/tools/telegram-actions.e2e.test.ts @@ -589,3 +589,70 @@ describe("readTelegramButtons", () => { ).toThrow(/style must be one of danger, success, primary/i); }); }); + +describe("handleTelegramAction per-account gating", () => { + it("allows sticker when account config enables it", async () => { + const cfg = { + channels: { + telegram: { + accounts: { + media: { botToken: "tok-media", actions: { sticker: true } }, + }, + }, + }, + } as OpenClawConfig; + + await handleTelegramAction( + { action: "sendSticker", to: "123", fileId: "sticker-id", accountId: "media" }, + cfg, + ); + expect(sendStickerTelegram).toHaveBeenCalledWith( + "123", + "sticker-id", + expect.objectContaining({ token: "tok-media" }), + ); + }); + + it("blocks sticker when account omits it", async () => { + const cfg = { + channels: { + telegram: { + accounts: { + chat: { botToken: "tok-chat" }, + }, + }, + }, + } as OpenClawConfig; + + await expect( + handleTelegramAction( + { action: "sendSticker", to: "123", fileId: "sticker-id", accountId: "chat" }, + cfg, + ), + ).rejects.toThrow(/sticker actions are disabled/i); + }); + + it("uses account-merged config, not top-level config", async () => { + // Top-level has no sticker enabled, but the account does + const cfg = { + channels: { + telegram: { + botToken: "tok-base", + accounts: { + media: { botToken: "tok-media", actions: { sticker: true } }, + }, + }, + }, + } as OpenClawConfig; + + await handleTelegramAction( + { action: "sendSticker", to: "123", fileId: "sticker-id", accountId: "media" }, + cfg, + ); + expect(sendStickerTelegram).toHaveBeenCalledWith( + "123", + "sticker-id", + expect.objectContaining({ token: "tok-media" }), + ); + }); +}); diff --git a/src/agents/tools/telegram-actions.ts b/src/agents/tools/telegram-actions.ts index 269d439144b..3f01392ad3c 100644 --- a/src/agents/tools/telegram-actions.ts +++ b/src/agents/tools/telegram-actions.ts @@ -1,7 +1,7 @@ import type { AgentToolResult } from "@mariozechner/pi-agent-core"; import type { OpenClawConfig } from "../../config/config.js"; -import { resolveTelegramAccount } from "../../telegram/accounts.js"; import type { TelegramButtonStyle, TelegramInlineButtons } from "../../telegram/button-types.js"; +import { resolveTelegramAccount } from "../../telegram/accounts.js"; import { resolveTelegramInlineButtonsScope, resolveTelegramTargetChatType, diff --git a/src/channels/plugins/actions/actions.test.ts b/src/channels/plugins/actions/actions.test.ts index 563c38ce898..423c3c79a0b 100644 --- a/src/channels/plugins/actions/actions.test.ts +++ b/src/channels/plugins/actions/actions.test.ts @@ -52,6 +52,80 @@ describe("discord message actions", () => { expect(actions).not.toContain("channel-create"); }); + + it("lists moderation actions when per-account config enables them", () => { + const cfg = { + channels: { + discord: { + accounts: { + vime: { token: "d1", actions: { moderation: true } }, + }, + }, + }, + } as OpenClawConfig; + const actions = discordMessageActions.listActions?.({ cfg }) ?? []; + + expect(actions).toContain("timeout"); + expect(actions).toContain("kick"); + expect(actions).toContain("ban"); + }); + + it("lists moderation when one account enables and another omits", () => { + const cfg = { + channels: { + discord: { + accounts: { + ops: { token: "d1", actions: { moderation: true } }, + chat: { token: "d2" }, + }, + }, + }, + } as OpenClawConfig; + const actions = discordMessageActions.listActions?.({ cfg }) ?? []; + + expect(actions).toContain("timeout"); + expect(actions).toContain("kick"); + expect(actions).toContain("ban"); + }); + + it("omits moderation when all accounts omit it", () => { + const cfg = { + channels: { + discord: { + accounts: { + ops: { token: "d1" }, + chat: { token: "d2" }, + }, + }, + }, + } as OpenClawConfig; + const actions = discordMessageActions.listActions?.({ cfg }) ?? []; + + // moderation defaults to false, so without explicit true it stays hidden + expect(actions).not.toContain("timeout"); + expect(actions).not.toContain("kick"); + expect(actions).not.toContain("ban"); + }); + + it("shallow merge: account actions object replaces base entirely", () => { + // Base has reactions: false, account has actions: { moderation: true } + // Shallow merge replaces the whole actions object, so reactions defaults to true + const cfg = { + channels: { + discord: { + actions: { reactions: false }, + accounts: { + vime: { token: "d1", actions: { moderation: true } }, + }, + }, + }, + } as OpenClawConfig; + const actions = discordMessageActions.listActions?.({ cfg }) ?? []; + + // vime's actions override replaces entire actions object; reactions defaults to true + expect(actions).toContain("react"); + expect(actions).toContain("timeout"); + }); }); describe("handleDiscordMessageAction", () => { @@ -325,6 +399,39 @@ describe("telegramMessageActions", () => { expect(handleTelegramAction).not.toHaveBeenCalled(); }); + it("lists sticker actions when per-account config enables them", () => { + const cfg = { + channels: { + telegram: { + accounts: { + media: { botToken: "tok", actions: { sticker: true } }, + }, + }, + }, + } as OpenClawConfig; + const actions = telegramMessageActions.listActions({ cfg }); + + expect(actions).toContain("sticker"); + expect(actions).toContain("sticker-search"); + }); + + it("omits sticker when all accounts omit it", () => { + const cfg = { + channels: { + telegram: { + accounts: { + a: { botToken: "tok1" }, + b: { botToken: "tok2" }, + }, + }, + }, + } as OpenClawConfig; + const actions = telegramMessageActions.listActions({ cfg }); + + expect(actions).not.toContain("sticker"); + expect(actions).not.toContain("sticker-search"); + }); + it("accepts numeric messageId and channelId for reactions", async () => { const cfg = { channels: { telegram: { botToken: "tok" } } } as OpenClawConfig;