test: add per-account action gating tests for Discord and Telegram handlers

This commit is contained in:
El-Fitz
2026-02-16 14:37:35 +01:00
committed by Peter Steinberger
parent a03fec2a3f
commit 4640999e77
4 changed files with 239 additions and 2 deletions

View File

@@ -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();
});
});

View File

@@ -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" }),
);
});
});

View File

@@ -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,

View File

@@ -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;