mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-24 16:32:29 +00:00
fix(test): split discord monitor agent components
This commit is contained in:
253
extensions/discord/src/monitor/monitor.agent-components.test.ts
Normal file
253
extensions/discord/src/monitor/monitor.agent-components.test.ts
Normal file
@@ -0,0 +1,253 @@
|
||||
import type { ButtonInteraction, ComponentData, StringSelectMenuInteraction } from "@buape/carbon";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import type { DiscordAccountConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { buildAgentSessionKey } from "openclaw/plugin-sdk/routing";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { peekSystemEvents, resetSystemEventsForTest } from "../../../../src/infra/system-events.ts";
|
||||
import { createAgentComponentButton, createAgentSelectMenu } from "./agent-components.js";
|
||||
|
||||
const readAllowFromStoreMock = vi.hoisted(() => vi.fn());
|
||||
const upsertPairingRequestMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/security-runtime", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/security-runtime")>();
|
||||
return {
|
||||
...actual,
|
||||
readStoreAllowFromForDmPolicy: async (params: {
|
||||
provider: string;
|
||||
accountId: string;
|
||||
dmPolicy?: string | null;
|
||||
shouldRead?: boolean | null;
|
||||
}) => {
|
||||
if (params.shouldRead === false || params.dmPolicy === "allowlist") {
|
||||
return [];
|
||||
}
|
||||
return await readAllowFromStoreMock(params.provider, params.accountId);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/conversation-runtime", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/conversation-runtime")>();
|
||||
return {
|
||||
...actual,
|
||||
upsertChannelPairingRequest: (...args: unknown[]) => upsertPairingRequestMock(...args),
|
||||
};
|
||||
});
|
||||
vi.mock("openclaw/plugin-sdk/conversation-runtime.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/conversation-runtime")>();
|
||||
return {
|
||||
...actual,
|
||||
upsertChannelPairingRequest: (...args: unknown[]) => upsertPairingRequestMock(...args),
|
||||
};
|
||||
});
|
||||
|
||||
describe("agent components", () => {
|
||||
const defaultDmSessionKey = buildAgentSessionKey({
|
||||
agentId: "main",
|
||||
channel: "discord",
|
||||
accountId: "default",
|
||||
peer: { kind: "direct", id: "123456789" },
|
||||
});
|
||||
|
||||
const createCfg = (): OpenClawConfig => ({}) as OpenClawConfig;
|
||||
|
||||
const createBaseDmInteraction = (overrides: Record<string, unknown> = {}) => {
|
||||
const reply = vi.fn().mockResolvedValue(undefined);
|
||||
const defer = vi.fn().mockResolvedValue(undefined);
|
||||
const interaction = {
|
||||
rawData: { channel_id: "dm-channel" },
|
||||
user: { id: "123456789", username: "Alice", discriminator: "1234" },
|
||||
defer,
|
||||
reply,
|
||||
...overrides,
|
||||
};
|
||||
return { interaction, defer, reply };
|
||||
};
|
||||
|
||||
const createDmButtonInteraction = (overrides: Partial<ButtonInteraction> = {}) => {
|
||||
const { interaction, defer, reply } = createBaseDmInteraction(
|
||||
overrides as Record<string, unknown>,
|
||||
);
|
||||
return {
|
||||
interaction: interaction as unknown as ButtonInteraction,
|
||||
defer,
|
||||
reply,
|
||||
};
|
||||
};
|
||||
|
||||
const createDmSelectInteraction = (overrides: Partial<StringSelectMenuInteraction> = {}) => {
|
||||
const { interaction, defer, reply } = createBaseDmInteraction({
|
||||
values: ["alpha"],
|
||||
...(overrides as Record<string, unknown>),
|
||||
});
|
||||
return {
|
||||
interaction: interaction as unknown as StringSelectMenuInteraction,
|
||||
defer,
|
||||
reply,
|
||||
};
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
readAllowFromStoreMock.mockClear().mockResolvedValue([]);
|
||||
upsertPairingRequestMock.mockClear().mockResolvedValue({ code: "PAIRCODE", created: true });
|
||||
resetSystemEventsForTest();
|
||||
});
|
||||
|
||||
it("sends pairing reply when DM sender is not allowlisted", async () => {
|
||||
const button = createAgentComponentButton({
|
||||
cfg: createCfg(),
|
||||
accountId: "default",
|
||||
dmPolicy: "pairing",
|
||||
});
|
||||
const { interaction, defer, reply } = createDmButtonInteraction();
|
||||
|
||||
await button.run(interaction, { componentId: "hello" } as ComponentData);
|
||||
|
||||
expect(defer).not.toHaveBeenCalled();
|
||||
expect(reply).toHaveBeenCalledTimes(1);
|
||||
const pairingText = String(reply.mock.calls[0]?.[0]?.content ?? "");
|
||||
expect(pairingText).toContain("Pairing code:");
|
||||
const code = pairingText.match(/Pairing code:\s*([A-Z2-9]{8})/)?.[1];
|
||||
expect(code).toBeDefined();
|
||||
expect(pairingText).toContain(`openclaw pairing approve discord ${code}`);
|
||||
expect(peekSystemEvents(defaultDmSessionKey)).toEqual([]);
|
||||
expect(readAllowFromStoreMock).toHaveBeenCalledWith("discord", "default");
|
||||
});
|
||||
|
||||
it("blocks DM interactions in allowlist mode when sender is not in configured allowFrom", async () => {
|
||||
const button = createAgentComponentButton({
|
||||
cfg: createCfg(),
|
||||
accountId: "default",
|
||||
dmPolicy: "allowlist",
|
||||
});
|
||||
const { interaction, defer, reply } = createDmButtonInteraction();
|
||||
|
||||
await button.run(interaction, { componentId: "hello" } as ComponentData);
|
||||
|
||||
expect(defer).not.toHaveBeenCalled();
|
||||
expect(reply).toHaveBeenCalledWith({
|
||||
content: "You are not authorized to use this button.",
|
||||
ephemeral: true,
|
||||
});
|
||||
expect(peekSystemEvents(defaultDmSessionKey)).toEqual([]);
|
||||
expect(readAllowFromStoreMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("authorizes DM interactions from pairing-store entries in pairing mode", async () => {
|
||||
readAllowFromStoreMock.mockResolvedValue(["123456789"]);
|
||||
const button = createAgentComponentButton({
|
||||
cfg: createCfg(),
|
||||
accountId: "default",
|
||||
dmPolicy: "pairing",
|
||||
});
|
||||
const { interaction, defer, reply } = createDmButtonInteraction();
|
||||
|
||||
await button.run(interaction, { componentId: "hello" } as ComponentData);
|
||||
|
||||
expect(defer).not.toHaveBeenCalled();
|
||||
expect(reply).toHaveBeenCalledWith({ content: "✓", ephemeral: true });
|
||||
expect(peekSystemEvents(defaultDmSessionKey)).toEqual([
|
||||
"[Discord component: hello clicked by Alice#1234 (123456789)]",
|
||||
]);
|
||||
expect(upsertPairingRequestMock).not.toHaveBeenCalled();
|
||||
expect(readAllowFromStoreMock).toHaveBeenCalledWith("discord", "default");
|
||||
});
|
||||
|
||||
it("allows DM component interactions in open mode without reading pairing store", async () => {
|
||||
readAllowFromStoreMock.mockResolvedValue(["123456789"]);
|
||||
const button = createAgentComponentButton({
|
||||
cfg: createCfg(),
|
||||
accountId: "default",
|
||||
dmPolicy: "open",
|
||||
});
|
||||
const { interaction, defer, reply } = createDmButtonInteraction();
|
||||
|
||||
await button.run(interaction, { componentId: "hello" } as ComponentData);
|
||||
|
||||
expect(defer).not.toHaveBeenCalled();
|
||||
expect(reply).toHaveBeenCalledWith({ content: "✓", ephemeral: true });
|
||||
expect(peekSystemEvents(defaultDmSessionKey)).toEqual([
|
||||
"[Discord component: hello clicked by Alice#1234 (123456789)]",
|
||||
]);
|
||||
expect(readAllowFromStoreMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("blocks DM component interactions in disabled mode without reading pairing store", async () => {
|
||||
readAllowFromStoreMock.mockResolvedValue(["123456789"]);
|
||||
const button = createAgentComponentButton({
|
||||
cfg: createCfg(),
|
||||
accountId: "default",
|
||||
dmPolicy: "disabled",
|
||||
});
|
||||
const { interaction, defer, reply } = createDmButtonInteraction();
|
||||
|
||||
await button.run(interaction, { componentId: "hello" } as ComponentData);
|
||||
|
||||
expect(defer).not.toHaveBeenCalled();
|
||||
expect(reply).toHaveBeenCalledWith({
|
||||
content: "DM interactions are disabled.",
|
||||
ephemeral: true,
|
||||
});
|
||||
expect(peekSystemEvents(defaultDmSessionKey)).toEqual([]);
|
||||
expect(readAllowFromStoreMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("matches tag-based allowlist entries for DM select menus", async () => {
|
||||
const select = createAgentSelectMenu({
|
||||
cfg: createCfg(),
|
||||
accountId: "default",
|
||||
discordConfig: { dangerouslyAllowNameMatching: true } as DiscordAccountConfig,
|
||||
dmPolicy: "allowlist",
|
||||
allowFrom: ["Alice#1234"],
|
||||
});
|
||||
const { interaction, defer, reply } = createDmSelectInteraction();
|
||||
|
||||
await select.run(interaction, { componentId: "hello" } as ComponentData);
|
||||
|
||||
expect(defer).not.toHaveBeenCalled();
|
||||
expect(reply).toHaveBeenCalledWith({ content: "✓", ephemeral: true });
|
||||
expect(peekSystemEvents(defaultDmSessionKey)).toEqual([
|
||||
"[Discord select menu: hello interacted by Alice#1234 (123456789) (selected: alpha)]",
|
||||
]);
|
||||
expect(readAllowFromStoreMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("accepts cid payloads for agent button interactions", async () => {
|
||||
const button = createAgentComponentButton({
|
||||
cfg: createCfg(),
|
||||
accountId: "default",
|
||||
dmPolicy: "allowlist",
|
||||
allowFrom: ["123456789"],
|
||||
});
|
||||
const { interaction, defer, reply } = createDmButtonInteraction();
|
||||
|
||||
await button.run(interaction, { cid: "hello_cid" } as ComponentData);
|
||||
|
||||
expect(defer).not.toHaveBeenCalled();
|
||||
expect(reply).toHaveBeenCalledWith({ content: "✓", ephemeral: true });
|
||||
expect(peekSystemEvents(defaultDmSessionKey)).toEqual([
|
||||
"[Discord component: hello_cid clicked by Alice#1234 (123456789)]",
|
||||
]);
|
||||
expect(readAllowFromStoreMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps malformed percent cid values without throwing", async () => {
|
||||
const button = createAgentComponentButton({
|
||||
cfg: createCfg(),
|
||||
accountId: "default",
|
||||
dmPolicy: "allowlist",
|
||||
allowFrom: ["123456789"],
|
||||
});
|
||||
const { interaction, defer, reply } = createDmButtonInteraction();
|
||||
|
||||
await button.run(interaction, { cid: "hello%2G" } as ComponentData);
|
||||
|
||||
expect(defer).not.toHaveBeenCalled();
|
||||
expect(reply).toHaveBeenCalledWith({ content: "✓", ephemeral: true });
|
||||
expect(peekSystemEvents(defaultDmSessionKey)).toEqual([
|
||||
"[Discord component: hello%2G clicked by Alice#1234 (123456789)]",
|
||||
]);
|
||||
expect(readAllowFromStoreMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -10,9 +10,7 @@ import type { GatewayPresenceUpdate } from "discord-api-types/v10";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import type { DiscordAccountConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { buildPluginBindingApprovalCustomId } from "openclaw/plugin-sdk/conversation-runtime";
|
||||
import { buildAgentSessionKey } from "openclaw/plugin-sdk/routing";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { peekSystemEvents, resetSystemEventsForTest } from "../../../../src/infra/system-events.ts";
|
||||
import {
|
||||
clearDiscordComponentEntries,
|
||||
registerDiscordComponentEntries,
|
||||
@@ -22,8 +20,6 @@ import {
|
||||
import type { DiscordComponentEntry, DiscordModalEntry } from "../components.js";
|
||||
import * as sendComponents from "../send.components.js";
|
||||
import {
|
||||
createAgentComponentButton,
|
||||
createAgentSelectMenu,
|
||||
createDiscordComponentButton,
|
||||
createDiscordComponentStringSelect,
|
||||
createDiscordComponentModal,
|
||||
@@ -158,216 +154,6 @@ vi.mock("openclaw/plugin-sdk/plugin-runtime", async (importOriginal) => {
|
||||
};
|
||||
});
|
||||
|
||||
describe("agent components", () => {
|
||||
const defaultDmSessionKey = buildAgentSessionKey({
|
||||
agentId: "main",
|
||||
channel: "discord",
|
||||
accountId: "default",
|
||||
peer: { kind: "direct", id: "123456789" },
|
||||
});
|
||||
|
||||
const createCfg = (): OpenClawConfig => ({}) as OpenClawConfig;
|
||||
|
||||
const createBaseDmInteraction = (overrides: Record<string, unknown> = {}) => {
|
||||
const reply = vi.fn().mockResolvedValue(undefined);
|
||||
const defer = vi.fn().mockResolvedValue(undefined);
|
||||
const interaction = {
|
||||
rawData: { channel_id: "dm-channel" },
|
||||
user: { id: "123456789", username: "Alice", discriminator: "1234" },
|
||||
defer,
|
||||
reply,
|
||||
...overrides,
|
||||
};
|
||||
return { interaction, defer, reply };
|
||||
};
|
||||
|
||||
const createDmButtonInteraction = (overrides: Partial<ButtonInteraction> = {}) => {
|
||||
const { interaction, defer, reply } = createBaseDmInteraction(
|
||||
overrides as Record<string, unknown>,
|
||||
);
|
||||
return {
|
||||
interaction: interaction as unknown as ButtonInteraction,
|
||||
defer,
|
||||
reply,
|
||||
};
|
||||
};
|
||||
|
||||
const createDmSelectInteraction = (overrides: Partial<StringSelectMenuInteraction> = {}) => {
|
||||
const { interaction, defer, reply } = createBaseDmInteraction({
|
||||
values: ["alpha"],
|
||||
...(overrides as Record<string, unknown>),
|
||||
});
|
||||
return {
|
||||
interaction: interaction as unknown as StringSelectMenuInteraction,
|
||||
defer,
|
||||
reply,
|
||||
};
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
readAllowFromStoreMock.mockClear().mockResolvedValue([]);
|
||||
upsertPairingRequestMock.mockClear().mockResolvedValue({ code: "PAIRCODE", created: true });
|
||||
resetSystemEventsForTest();
|
||||
});
|
||||
|
||||
it("sends pairing reply when DM sender is not allowlisted", async () => {
|
||||
const button = createAgentComponentButton({
|
||||
cfg: createCfg(),
|
||||
accountId: "default",
|
||||
dmPolicy: "pairing",
|
||||
});
|
||||
const { interaction, defer, reply } = createDmButtonInteraction();
|
||||
|
||||
await button.run(interaction, { componentId: "hello" } as ComponentData);
|
||||
|
||||
expect(defer).not.toHaveBeenCalled();
|
||||
expect(reply).toHaveBeenCalledTimes(1);
|
||||
const pairingText = String(reply.mock.calls[0]?.[0]?.content ?? "");
|
||||
expect(pairingText).toContain("Pairing code:");
|
||||
const code = pairingText.match(/Pairing code:\s*([A-Z2-9]{8})/)?.[1];
|
||||
expect(code).toBeDefined();
|
||||
expect(pairingText).toContain(`openclaw pairing approve discord ${code}`);
|
||||
expect(peekSystemEvents(defaultDmSessionKey)).toEqual([]);
|
||||
expect(readAllowFromStoreMock).toHaveBeenCalledWith("discord", "default");
|
||||
});
|
||||
|
||||
it("blocks DM interactions in allowlist mode when sender is not in configured allowFrom", async () => {
|
||||
const button = createAgentComponentButton({
|
||||
cfg: createCfg(),
|
||||
accountId: "default",
|
||||
dmPolicy: "allowlist",
|
||||
});
|
||||
const { interaction, defer, reply } = createDmButtonInteraction();
|
||||
|
||||
await button.run(interaction, { componentId: "hello" } as ComponentData);
|
||||
|
||||
expect(defer).not.toHaveBeenCalled();
|
||||
expect(reply).toHaveBeenCalledWith({
|
||||
content: "You are not authorized to use this button.",
|
||||
ephemeral: true,
|
||||
});
|
||||
expect(peekSystemEvents(defaultDmSessionKey)).toEqual([]);
|
||||
expect(readAllowFromStoreMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("authorizes DM interactions from pairing-store entries in pairing mode", async () => {
|
||||
readAllowFromStoreMock.mockResolvedValue(["123456789"]);
|
||||
const button = createAgentComponentButton({
|
||||
cfg: createCfg(),
|
||||
accountId: "default",
|
||||
dmPolicy: "pairing",
|
||||
});
|
||||
const { interaction, defer, reply } = createDmButtonInteraction();
|
||||
|
||||
await button.run(interaction, { componentId: "hello" } as ComponentData);
|
||||
|
||||
expect(defer).not.toHaveBeenCalled();
|
||||
expect(reply).toHaveBeenCalledWith({ content: "✓", ephemeral: true });
|
||||
expect(peekSystemEvents(defaultDmSessionKey)).toEqual([
|
||||
"[Discord component: hello clicked by Alice#1234 (123456789)]",
|
||||
]);
|
||||
expect(upsertPairingRequestMock).not.toHaveBeenCalled();
|
||||
expect(readAllowFromStoreMock).toHaveBeenCalledWith("discord", "default");
|
||||
});
|
||||
|
||||
it("allows DM component interactions in open mode without reading pairing store", async () => {
|
||||
readAllowFromStoreMock.mockResolvedValue(["123456789"]);
|
||||
const button = createAgentComponentButton({
|
||||
cfg: createCfg(),
|
||||
accountId: "default",
|
||||
dmPolicy: "open",
|
||||
});
|
||||
const { interaction, defer, reply } = createDmButtonInteraction();
|
||||
|
||||
await button.run(interaction, { componentId: "hello" } as ComponentData);
|
||||
|
||||
expect(defer).not.toHaveBeenCalled();
|
||||
expect(reply).toHaveBeenCalledWith({ content: "✓", ephemeral: true });
|
||||
expect(peekSystemEvents(defaultDmSessionKey)).toEqual([
|
||||
"[Discord component: hello clicked by Alice#1234 (123456789)]",
|
||||
]);
|
||||
expect(readAllowFromStoreMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("blocks DM component interactions in disabled mode without reading pairing store", async () => {
|
||||
readAllowFromStoreMock.mockResolvedValue(["123456789"]);
|
||||
const button = createAgentComponentButton({
|
||||
cfg: createCfg(),
|
||||
accountId: "default",
|
||||
dmPolicy: "disabled",
|
||||
});
|
||||
const { interaction, defer, reply } = createDmButtonInteraction();
|
||||
|
||||
await button.run(interaction, { componentId: "hello" } as ComponentData);
|
||||
|
||||
expect(defer).not.toHaveBeenCalled();
|
||||
expect(reply).toHaveBeenCalledWith({
|
||||
content: "DM interactions are disabled.",
|
||||
ephemeral: true,
|
||||
});
|
||||
expect(peekSystemEvents(defaultDmSessionKey)).toEqual([]);
|
||||
expect(readAllowFromStoreMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("matches tag-based allowlist entries for DM select menus", async () => {
|
||||
const select = createAgentSelectMenu({
|
||||
cfg: createCfg(),
|
||||
accountId: "default",
|
||||
discordConfig: { dangerouslyAllowNameMatching: true } as DiscordAccountConfig,
|
||||
dmPolicy: "allowlist",
|
||||
allowFrom: ["Alice#1234"],
|
||||
});
|
||||
const { interaction, defer, reply } = createDmSelectInteraction();
|
||||
|
||||
await select.run(interaction, { componentId: "hello" } as ComponentData);
|
||||
|
||||
expect(defer).not.toHaveBeenCalled();
|
||||
expect(reply).toHaveBeenCalledWith({ content: "✓", ephemeral: true });
|
||||
expect(peekSystemEvents(defaultDmSessionKey)).toEqual([
|
||||
"[Discord select menu: hello interacted by Alice#1234 (123456789) (selected: alpha)]",
|
||||
]);
|
||||
expect(readAllowFromStoreMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("accepts cid payloads for agent button interactions", async () => {
|
||||
const button = createAgentComponentButton({
|
||||
cfg: createCfg(),
|
||||
accountId: "default",
|
||||
dmPolicy: "allowlist",
|
||||
allowFrom: ["123456789"],
|
||||
});
|
||||
const { interaction, defer, reply } = createDmButtonInteraction();
|
||||
|
||||
await button.run(interaction, { cid: "hello_cid" } as ComponentData);
|
||||
|
||||
expect(defer).not.toHaveBeenCalled();
|
||||
expect(reply).toHaveBeenCalledWith({ content: "✓", ephemeral: true });
|
||||
expect(peekSystemEvents(defaultDmSessionKey)).toEqual([
|
||||
"[Discord component: hello_cid clicked by Alice#1234 (123456789)]",
|
||||
]);
|
||||
expect(readAllowFromStoreMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps malformed percent cid values without throwing", async () => {
|
||||
const button = createAgentComponentButton({
|
||||
cfg: createCfg(),
|
||||
accountId: "default",
|
||||
dmPolicy: "allowlist",
|
||||
allowFrom: ["123456789"],
|
||||
});
|
||||
const { interaction, defer, reply } = createDmButtonInteraction();
|
||||
|
||||
await button.run(interaction, { cid: "hello%2G" } as ComponentData);
|
||||
|
||||
expect(defer).not.toHaveBeenCalled();
|
||||
expect(reply).toHaveBeenCalledWith({ content: "✓", ephemeral: true });
|
||||
expect(peekSystemEvents(defaultDmSessionKey)).toEqual([
|
||||
"[Discord component: hello%2G clicked by Alice#1234 (123456789)]",
|
||||
]);
|
||||
expect(readAllowFromStoreMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("discord component interactions", () => {
|
||||
let editDiscordComponentMessageMock: ReturnType<typeof vi.spyOn>;
|
||||
const createCfg = (): OpenClawConfig =>
|
||||
|
||||
Reference in New Issue
Block a user