mirror of https://github.com/openclaw/openclaw.git
test: extract message action plugin dispatch coverage
This commit is contained in:
parent
a9fd34058f
commit
9044a10c5f
|
|
@ -0,0 +1,439 @@
|
||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { jsonResult } from "../../agents/tools/common.js";
|
||||||
|
import type { ChannelPlugin } from "../../channels/plugins/types.js";
|
||||||
|
import type { OpenClawConfig } from "../../config/config.js";
|
||||||
|
import { setActivePluginRegistry } from "../../plugins/runtime.js";
|
||||||
|
import { createOutboundTestPlugin, createTestRegistry } from "../../test-utils/channel-plugins.js";
|
||||||
|
import { runMessageAction } from "./message-action-runner.js";
|
||||||
|
|
||||||
|
function createAlwaysConfiguredPluginConfig(account: Record<string, unknown> = { enabled: true }) {
|
||||||
|
return {
|
||||||
|
listAccountIds: () => ["default"],
|
||||||
|
resolveAccount: () => account,
|
||||||
|
isConfigured: () => true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("runMessageAction plugin dispatch", () => {
|
||||||
|
describe("media caption behavior", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
setActivePluginRegistry(createTestRegistry([]));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("promotes caption to message for media sends when message is empty", async () => {
|
||||||
|
const sendMedia = vi.fn().mockResolvedValue({
|
||||||
|
channel: "testchat",
|
||||||
|
messageId: "m1",
|
||||||
|
chatId: "c1",
|
||||||
|
});
|
||||||
|
setActivePluginRegistry(
|
||||||
|
createTestRegistry([
|
||||||
|
{
|
||||||
|
pluginId: "testchat",
|
||||||
|
source: "test",
|
||||||
|
plugin: createOutboundTestPlugin({
|
||||||
|
id: "testchat",
|
||||||
|
outbound: {
|
||||||
|
deliveryMode: "direct",
|
||||||
|
sendText: vi.fn().mockResolvedValue({
|
||||||
|
channel: "testchat",
|
||||||
|
messageId: "t1",
|
||||||
|
chatId: "c1",
|
||||||
|
}),
|
||||||
|
sendMedia,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
const cfg = {
|
||||||
|
channels: {
|
||||||
|
testchat: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as OpenClawConfig;
|
||||||
|
|
||||||
|
const result = await runMessageAction({
|
||||||
|
cfg,
|
||||||
|
action: "send",
|
||||||
|
params: {
|
||||||
|
channel: "testchat",
|
||||||
|
target: "channel:abc",
|
||||||
|
media: "https://example.com/cat.png",
|
||||||
|
caption: "caption-only text",
|
||||||
|
},
|
||||||
|
dryRun: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.kind).toBe("send");
|
||||||
|
expect(sendMedia).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
text: "caption-only text",
|
||||||
|
mediaUrl: "https://example.com/cat.png",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("card-only send behavior", () => {
|
||||||
|
const handleAction = vi.fn(async ({ params }: { params: Record<string, unknown> }) =>
|
||||||
|
jsonResult({
|
||||||
|
ok: true,
|
||||||
|
card: params.card ?? null,
|
||||||
|
message: params.message ?? null,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const cardPlugin: ChannelPlugin = {
|
||||||
|
id: "cardchat",
|
||||||
|
meta: {
|
||||||
|
id: "cardchat",
|
||||||
|
label: "Card Chat",
|
||||||
|
selectionLabel: "Card Chat",
|
||||||
|
docsPath: "/channels/cardchat",
|
||||||
|
blurb: "Card-only send test plugin.",
|
||||||
|
},
|
||||||
|
capabilities: { chatTypes: ["direct"] },
|
||||||
|
config: createAlwaysConfiguredPluginConfig(),
|
||||||
|
actions: {
|
||||||
|
listActions: () => ["send"],
|
||||||
|
supportsAction: ({ action }) => action === "send",
|
||||||
|
handleAction,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
setActivePluginRegistry(
|
||||||
|
createTestRegistry([
|
||||||
|
{
|
||||||
|
pluginId: "cardchat",
|
||||||
|
source: "test",
|
||||||
|
plugin: cardPlugin,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
handleAction.mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
setActivePluginRegistry(createTestRegistry([]));
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("allows card-only sends without text or media", async () => {
|
||||||
|
const cfg = {
|
||||||
|
channels: {
|
||||||
|
cardchat: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as OpenClawConfig;
|
||||||
|
|
||||||
|
const card = {
|
||||||
|
type: "AdaptiveCard",
|
||||||
|
version: "1.4",
|
||||||
|
body: [{ type: "TextBlock", text: "Card-only payload" }],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await runMessageAction({
|
||||||
|
cfg,
|
||||||
|
action: "send",
|
||||||
|
params: {
|
||||||
|
channel: "cardchat",
|
||||||
|
target: "channel:test-card",
|
||||||
|
card,
|
||||||
|
},
|
||||||
|
dryRun: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.kind).toBe("send");
|
||||||
|
expect(result.handledBy).toBe("plugin");
|
||||||
|
expect(handleAction).toHaveBeenCalled();
|
||||||
|
expect(result.payload).toMatchObject({
|
||||||
|
ok: true,
|
||||||
|
card,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("telegram plugin poll forwarding", () => {
|
||||||
|
const handleAction = vi.fn(async ({ params }: { params: Record<string, unknown> }) =>
|
||||||
|
jsonResult({
|
||||||
|
ok: true,
|
||||||
|
forwarded: {
|
||||||
|
to: params.to ?? null,
|
||||||
|
pollQuestion: params.pollQuestion ?? null,
|
||||||
|
pollOption: params.pollOption ?? null,
|
||||||
|
pollDurationSeconds: params.pollDurationSeconds ?? null,
|
||||||
|
pollPublic: params.pollPublic ?? null,
|
||||||
|
threadId: params.threadId ?? null,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const telegramPollPlugin: ChannelPlugin = {
|
||||||
|
id: "telegram",
|
||||||
|
meta: {
|
||||||
|
id: "telegram",
|
||||||
|
label: "Telegram",
|
||||||
|
selectionLabel: "Telegram",
|
||||||
|
docsPath: "/channels/telegram",
|
||||||
|
blurb: "Telegram poll forwarding test plugin.",
|
||||||
|
},
|
||||||
|
capabilities: { chatTypes: ["direct"] },
|
||||||
|
config: createAlwaysConfiguredPluginConfig(),
|
||||||
|
messaging: {
|
||||||
|
targetResolver: {
|
||||||
|
looksLikeId: () => true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
listActions: () => ["poll"],
|
||||||
|
supportsAction: ({ action }) => action === "poll",
|
||||||
|
handleAction,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
setActivePluginRegistry(
|
||||||
|
createTestRegistry([
|
||||||
|
{
|
||||||
|
pluginId: "telegram",
|
||||||
|
source: "test",
|
||||||
|
plugin: telegramPollPlugin,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
handleAction.mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
setActivePluginRegistry(createTestRegistry([]));
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("forwards telegram poll params through plugin dispatch", async () => {
|
||||||
|
const result = await runMessageAction({
|
||||||
|
cfg: {
|
||||||
|
channels: {
|
||||||
|
telegram: {
|
||||||
|
botToken: "tok",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as OpenClawConfig,
|
||||||
|
action: "poll",
|
||||||
|
params: {
|
||||||
|
channel: "telegram",
|
||||||
|
target: "telegram:123",
|
||||||
|
pollQuestion: "Lunch?",
|
||||||
|
pollOption: ["Pizza", "Sushi"],
|
||||||
|
pollDurationSeconds: 120,
|
||||||
|
pollPublic: true,
|
||||||
|
threadId: "42",
|
||||||
|
},
|
||||||
|
dryRun: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.kind).toBe("poll");
|
||||||
|
expect(result.handledBy).toBe("plugin");
|
||||||
|
expect(handleAction).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
action: "poll",
|
||||||
|
channel: "telegram",
|
||||||
|
params: expect.objectContaining({
|
||||||
|
to: "telegram:123",
|
||||||
|
pollQuestion: "Lunch?",
|
||||||
|
pollOption: ["Pizza", "Sushi"],
|
||||||
|
pollDurationSeconds: 120,
|
||||||
|
pollPublic: true,
|
||||||
|
threadId: "42",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(result.payload).toMatchObject({
|
||||||
|
ok: true,
|
||||||
|
forwarded: {
|
||||||
|
to: "telegram:123",
|
||||||
|
pollQuestion: "Lunch?",
|
||||||
|
pollOption: ["Pizza", "Sushi"],
|
||||||
|
pollDurationSeconds: 120,
|
||||||
|
pollPublic: true,
|
||||||
|
threadId: "42",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("components parsing", () => {
|
||||||
|
const handleAction = vi.fn(async ({ params }: { params: Record<string, unknown> }) =>
|
||||||
|
jsonResult({
|
||||||
|
ok: true,
|
||||||
|
components: params.components ?? null,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const componentsPlugin: ChannelPlugin = {
|
||||||
|
id: "discord",
|
||||||
|
meta: {
|
||||||
|
id: "discord",
|
||||||
|
label: "Discord",
|
||||||
|
selectionLabel: "Discord",
|
||||||
|
docsPath: "/channels/discord",
|
||||||
|
blurb: "Discord components send test plugin.",
|
||||||
|
},
|
||||||
|
capabilities: { chatTypes: ["direct"] },
|
||||||
|
config: createAlwaysConfiguredPluginConfig({}),
|
||||||
|
actions: {
|
||||||
|
listActions: () => ["send"],
|
||||||
|
supportsAction: ({ action }) => action === "send",
|
||||||
|
handleAction,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
setActivePluginRegistry(
|
||||||
|
createTestRegistry([
|
||||||
|
{
|
||||||
|
pluginId: "discord",
|
||||||
|
source: "test",
|
||||||
|
plugin: componentsPlugin,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
handleAction.mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
setActivePluginRegistry(createTestRegistry([]));
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("parses components JSON strings before plugin dispatch", async () => {
|
||||||
|
const components = {
|
||||||
|
text: "hello",
|
||||||
|
buttons: [{ label: "A", customId: "a" }],
|
||||||
|
};
|
||||||
|
const result = await runMessageAction({
|
||||||
|
cfg: {} as OpenClawConfig,
|
||||||
|
action: "send",
|
||||||
|
params: {
|
||||||
|
channel: "discord",
|
||||||
|
target: "channel:123",
|
||||||
|
message: "hi",
|
||||||
|
components: JSON.stringify(components),
|
||||||
|
},
|
||||||
|
dryRun: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.kind).toBe("send");
|
||||||
|
expect(handleAction).toHaveBeenCalled();
|
||||||
|
expect(result.payload).toMatchObject({ ok: true, components });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws on invalid components JSON strings", async () => {
|
||||||
|
await expect(
|
||||||
|
runMessageAction({
|
||||||
|
cfg: {} as OpenClawConfig,
|
||||||
|
action: "send",
|
||||||
|
params: {
|
||||||
|
channel: "discord",
|
||||||
|
target: "channel:123",
|
||||||
|
message: "hi",
|
||||||
|
components: "{not-json}",
|
||||||
|
},
|
||||||
|
dryRun: false,
|
||||||
|
}),
|
||||||
|
).rejects.toThrow(/--components must be valid JSON/);
|
||||||
|
|
||||||
|
expect(handleAction).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("accountId defaults", () => {
|
||||||
|
const handleAction = vi.fn(async () => jsonResult({ ok: true }));
|
||||||
|
const accountPlugin: ChannelPlugin = {
|
||||||
|
id: "discord",
|
||||||
|
meta: {
|
||||||
|
id: "discord",
|
||||||
|
label: "Discord",
|
||||||
|
selectionLabel: "Discord",
|
||||||
|
docsPath: "/channels/discord",
|
||||||
|
blurb: "Discord test plugin.",
|
||||||
|
},
|
||||||
|
capabilities: { chatTypes: ["direct"] },
|
||||||
|
config: {
|
||||||
|
listAccountIds: () => ["default"],
|
||||||
|
resolveAccount: () => ({}),
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
listActions: () => ["send"],
|
||||||
|
handleAction,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
setActivePluginRegistry(
|
||||||
|
createTestRegistry([
|
||||||
|
{
|
||||||
|
pluginId: "discord",
|
||||||
|
source: "test",
|
||||||
|
plugin: accountPlugin,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
handleAction.mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
setActivePluginRegistry(createTestRegistry([]));
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
{
|
||||||
|
name: "uses defaultAccountId override",
|
||||||
|
args: {
|
||||||
|
cfg: {} as OpenClawConfig,
|
||||||
|
defaultAccountId: "ops",
|
||||||
|
},
|
||||||
|
expectedAccountId: "ops",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "falls back to agent binding account",
|
||||||
|
args: {
|
||||||
|
cfg: {
|
||||||
|
bindings: [
|
||||||
|
{ agentId: "agent-b", match: { channel: "discord", accountId: "account-b" } },
|
||||||
|
],
|
||||||
|
} as OpenClawConfig,
|
||||||
|
agentId: "agent-b",
|
||||||
|
},
|
||||||
|
expectedAccountId: "account-b",
|
||||||
|
},
|
||||||
|
])("$name", async ({ args, expectedAccountId }) => {
|
||||||
|
await runMessageAction({
|
||||||
|
...args,
|
||||||
|
action: "send",
|
||||||
|
params: {
|
||||||
|
channel: "discord",
|
||||||
|
target: "channel:123",
|
||||||
|
message: "hi",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(handleAction).toHaveBeenCalled();
|
||||||
|
const ctx = (handleAction.mock.calls as unknown as Array<[unknown]>)[0]?.[0] as
|
||||||
|
| {
|
||||||
|
accountId?: string | null;
|
||||||
|
params: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
| undefined;
|
||||||
|
if (!ctx) {
|
||||||
|
throw new Error("expected action context");
|
||||||
|
}
|
||||||
|
expect(ctx.accountId).toBe(expectedAccountId);
|
||||||
|
expect(ctx.params.accountId).toBe(expectedAccountId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -9,7 +9,7 @@ import { jsonResult } from "../../agents/tools/common.js";
|
||||||
import type { ChannelPlugin } from "../../channels/plugins/types.js";
|
import type { ChannelPlugin } from "../../channels/plugins/types.js";
|
||||||
import type { OpenClawConfig } from "../../config/config.js";
|
import type { OpenClawConfig } from "../../config/config.js";
|
||||||
import { setActivePluginRegistry } from "../../plugins/runtime.js";
|
import { setActivePluginRegistry } from "../../plugins/runtime.js";
|
||||||
import { createOutboundTestPlugin, createTestRegistry } from "../../test-utils/channel-plugins.js";
|
import { createTestRegistry } from "../../test-utils/channel-plugins.js";
|
||||||
import { createIMessageTestPlugin } from "../../test-utils/imessage-test-plugin.js";
|
import { createIMessageTestPlugin } from "../../test-utils/imessage-test-plugin.js";
|
||||||
import { loadWebMedia } from "../../web/media.js";
|
import { loadWebMedia } from "../../web/media.js";
|
||||||
import { resolvePreferredOpenClawTmpDir } from "../tmp-openclaw-dir.js";
|
import { resolvePreferredOpenClawTmpDir } from "../tmp-openclaw-dir.js";
|
||||||
|
|
@ -105,14 +105,6 @@ async function expectSandboxMediaRewrite(params: {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function createAlwaysConfiguredPluginConfig(account: Record<string, unknown> = { enabled: true }) {
|
|
||||||
return {
|
|
||||||
listAccountIds: () => ["default"],
|
|
||||||
resolveAccount: () => account,
|
|
||||||
isConfigured: () => true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let createPluginRuntime: typeof import("../../plugins/runtime/index.js").createPluginRuntime;
|
let createPluginRuntime: typeof import("../../plugins/runtime/index.js").createPluginRuntime;
|
||||||
let setSlackRuntime: typeof import("../../../extensions/slack/src/runtime.js").setSlackRuntime;
|
let setSlackRuntime: typeof import("../../../extensions/slack/src/runtime.js").setSlackRuntime;
|
||||||
let setTelegramRuntime: typeof import("../../../extensions/telegram/src/runtime.js").setTelegramRuntime;
|
let setTelegramRuntime: typeof import("../../../extensions/telegram/src/runtime.js").setTelegramRuntime;
|
||||||
|
|
@ -825,433 +817,3 @@ describe("runMessageAction sandboxed media validation", () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("runMessageAction media caption behavior", () => {
|
|
||||||
afterEach(() => {
|
|
||||||
setActivePluginRegistry(createTestRegistry([]));
|
|
||||||
});
|
|
||||||
|
|
||||||
it("promotes caption to message for media sends when message is empty", async () => {
|
|
||||||
const sendMedia = vi.fn().mockResolvedValue({
|
|
||||||
channel: "testchat",
|
|
||||||
messageId: "m1",
|
|
||||||
chatId: "c1",
|
|
||||||
});
|
|
||||||
setActivePluginRegistry(
|
|
||||||
createTestRegistry([
|
|
||||||
{
|
|
||||||
pluginId: "testchat",
|
|
||||||
source: "test",
|
|
||||||
plugin: createOutboundTestPlugin({
|
|
||||||
id: "testchat",
|
|
||||||
outbound: {
|
|
||||||
deliveryMode: "direct",
|
|
||||||
sendText: vi.fn().mockResolvedValue({
|
|
||||||
channel: "testchat",
|
|
||||||
messageId: "t1",
|
|
||||||
chatId: "c1",
|
|
||||||
}),
|
|
||||||
sendMedia,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
const cfg = {
|
|
||||||
channels: {
|
|
||||||
testchat: {
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as OpenClawConfig;
|
|
||||||
|
|
||||||
const result = await runMessageAction({
|
|
||||||
cfg,
|
|
||||||
action: "send",
|
|
||||||
params: {
|
|
||||||
channel: "testchat",
|
|
||||||
target: "channel:abc",
|
|
||||||
media: "https://example.com/cat.png",
|
|
||||||
caption: "caption-only text",
|
|
||||||
},
|
|
||||||
dryRun: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.kind).toBe("send");
|
|
||||||
expect(sendMedia).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
text: "caption-only text",
|
|
||||||
mediaUrl: "https://example.com/cat.png",
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("runMessageAction card-only send behavior", () => {
|
|
||||||
const handleAction = vi.fn(async ({ params }: { params: Record<string, unknown> }) =>
|
|
||||||
jsonResult({
|
|
||||||
ok: true,
|
|
||||||
card: params.card ?? null,
|
|
||||||
message: params.message ?? null,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const cardPlugin: ChannelPlugin = {
|
|
||||||
id: "cardchat",
|
|
||||||
meta: {
|
|
||||||
id: "cardchat",
|
|
||||||
label: "Card Chat",
|
|
||||||
selectionLabel: "Card Chat",
|
|
||||||
docsPath: "/channels/cardchat",
|
|
||||||
blurb: "Card-only send test plugin.",
|
|
||||||
},
|
|
||||||
capabilities: { chatTypes: ["direct"] },
|
|
||||||
config: createAlwaysConfiguredPluginConfig(),
|
|
||||||
actions: {
|
|
||||||
listActions: () => ["send"],
|
|
||||||
supportsAction: ({ action }) => action === "send",
|
|
||||||
handleAction,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
setActivePluginRegistry(
|
|
||||||
createTestRegistry([
|
|
||||||
{
|
|
||||||
pluginId: "cardchat",
|
|
||||||
source: "test",
|
|
||||||
plugin: cardPlugin,
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
handleAction.mockClear();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
setActivePluginRegistry(createTestRegistry([]));
|
|
||||||
vi.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("allows card-only sends without text or media", async () => {
|
|
||||||
const cfg = {
|
|
||||||
channels: {
|
|
||||||
cardchat: {
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as OpenClawConfig;
|
|
||||||
|
|
||||||
const card = {
|
|
||||||
type: "AdaptiveCard",
|
|
||||||
version: "1.4",
|
|
||||||
body: [{ type: "TextBlock", text: "Card-only payload" }],
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = await runMessageAction({
|
|
||||||
cfg,
|
|
||||||
action: "send",
|
|
||||||
params: {
|
|
||||||
channel: "cardchat",
|
|
||||||
target: "channel:test-card",
|
|
||||||
card,
|
|
||||||
},
|
|
||||||
dryRun: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.kind).toBe("send");
|
|
||||||
expect(result.handledBy).toBe("plugin");
|
|
||||||
expect(handleAction).toHaveBeenCalled();
|
|
||||||
expect(result.payload).toMatchObject({
|
|
||||||
ok: true,
|
|
||||||
card,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("runMessageAction telegram plugin poll forwarding", () => {
|
|
||||||
const handleAction = vi.fn(async ({ params }: { params: Record<string, unknown> }) =>
|
|
||||||
jsonResult({
|
|
||||||
ok: true,
|
|
||||||
forwarded: {
|
|
||||||
to: params.to ?? null,
|
|
||||||
pollQuestion: params.pollQuestion ?? null,
|
|
||||||
pollOption: params.pollOption ?? null,
|
|
||||||
pollDurationSeconds: params.pollDurationSeconds ?? null,
|
|
||||||
pollPublic: params.pollPublic ?? null,
|
|
||||||
threadId: params.threadId ?? null,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const telegramPollPlugin: ChannelPlugin = {
|
|
||||||
id: "telegram",
|
|
||||||
meta: {
|
|
||||||
id: "telegram",
|
|
||||||
label: "Telegram",
|
|
||||||
selectionLabel: "Telegram",
|
|
||||||
docsPath: "/channels/telegram",
|
|
||||||
blurb: "Telegram poll forwarding test plugin.",
|
|
||||||
},
|
|
||||||
capabilities: { chatTypes: ["direct"] },
|
|
||||||
config: createAlwaysConfiguredPluginConfig(),
|
|
||||||
messaging: {
|
|
||||||
targetResolver: {
|
|
||||||
looksLikeId: () => true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
actions: {
|
|
||||||
listActions: () => ["poll"],
|
|
||||||
supportsAction: ({ action }) => action === "poll",
|
|
||||||
handleAction,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
setActivePluginRegistry(
|
|
||||||
createTestRegistry([
|
|
||||||
{
|
|
||||||
pluginId: "telegram",
|
|
||||||
source: "test",
|
|
||||||
plugin: telegramPollPlugin,
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
handleAction.mockClear();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
setActivePluginRegistry(createTestRegistry([]));
|
|
||||||
vi.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("forwards telegram poll params through plugin dispatch", async () => {
|
|
||||||
const result = await runMessageAction({
|
|
||||||
cfg: {
|
|
||||||
channels: {
|
|
||||||
telegram: {
|
|
||||||
botToken: "tok",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as OpenClawConfig,
|
|
||||||
action: "poll",
|
|
||||||
params: {
|
|
||||||
channel: "telegram",
|
|
||||||
target: "telegram:123",
|
|
||||||
pollQuestion: "Lunch?",
|
|
||||||
pollOption: ["Pizza", "Sushi"],
|
|
||||||
pollDurationSeconds: 120,
|
|
||||||
pollPublic: true,
|
|
||||||
threadId: "42",
|
|
||||||
},
|
|
||||||
dryRun: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.kind).toBe("poll");
|
|
||||||
expect(result.handledBy).toBe("plugin");
|
|
||||||
expect(handleAction).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
action: "poll",
|
|
||||||
channel: "telegram",
|
|
||||||
params: expect.objectContaining({
|
|
||||||
to: "telegram:123",
|
|
||||||
pollQuestion: "Lunch?",
|
|
||||||
pollOption: ["Pizza", "Sushi"],
|
|
||||||
pollDurationSeconds: 120,
|
|
||||||
pollPublic: true,
|
|
||||||
threadId: "42",
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
expect(result.payload).toMatchObject({
|
|
||||||
ok: true,
|
|
||||||
forwarded: {
|
|
||||||
to: "telegram:123",
|
|
||||||
pollQuestion: "Lunch?",
|
|
||||||
pollOption: ["Pizza", "Sushi"],
|
|
||||||
pollDurationSeconds: 120,
|
|
||||||
pollPublic: true,
|
|
||||||
threadId: "42",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("runMessageAction components parsing", () => {
|
|
||||||
const handleAction = vi.fn(async ({ params }: { params: Record<string, unknown> }) =>
|
|
||||||
jsonResult({
|
|
||||||
ok: true,
|
|
||||||
components: params.components ?? null,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const componentsPlugin: ChannelPlugin = {
|
|
||||||
id: "discord",
|
|
||||||
meta: {
|
|
||||||
id: "discord",
|
|
||||||
label: "Discord",
|
|
||||||
selectionLabel: "Discord",
|
|
||||||
docsPath: "/channels/discord",
|
|
||||||
blurb: "Discord components send test plugin.",
|
|
||||||
},
|
|
||||||
capabilities: { chatTypes: ["direct"] },
|
|
||||||
config: createAlwaysConfiguredPluginConfig({}),
|
|
||||||
actions: {
|
|
||||||
listActions: () => ["send"],
|
|
||||||
supportsAction: ({ action }) => action === "send",
|
|
||||||
handleAction,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
setActivePluginRegistry(
|
|
||||||
createTestRegistry([
|
|
||||||
{
|
|
||||||
pluginId: "discord",
|
|
||||||
source: "test",
|
|
||||||
plugin: componentsPlugin,
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
handleAction.mockClear();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
setActivePluginRegistry(createTestRegistry([]));
|
|
||||||
vi.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("parses components JSON strings before plugin dispatch", async () => {
|
|
||||||
const components = {
|
|
||||||
text: "hello",
|
|
||||||
buttons: [{ label: "A", customId: "a" }],
|
|
||||||
};
|
|
||||||
const result = await runMessageAction({
|
|
||||||
cfg: {} as OpenClawConfig,
|
|
||||||
action: "send",
|
|
||||||
params: {
|
|
||||||
channel: "discord",
|
|
||||||
target: "channel:123",
|
|
||||||
message: "hi",
|
|
||||||
components: JSON.stringify(components),
|
|
||||||
},
|
|
||||||
dryRun: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.kind).toBe("send");
|
|
||||||
expect(handleAction).toHaveBeenCalled();
|
|
||||||
expect(result.payload).toMatchObject({ ok: true, components });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("throws on invalid components JSON strings", async () => {
|
|
||||||
await expect(
|
|
||||||
runMessageAction({
|
|
||||||
cfg: {} as OpenClawConfig,
|
|
||||||
action: "send",
|
|
||||||
params: {
|
|
||||||
channel: "discord",
|
|
||||||
target: "channel:123",
|
|
||||||
message: "hi",
|
|
||||||
components: "{not-json}",
|
|
||||||
},
|
|
||||||
dryRun: false,
|
|
||||||
}),
|
|
||||||
).rejects.toThrow(/--components must be valid JSON/);
|
|
||||||
|
|
||||||
expect(handleAction).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("runMessageAction accountId defaults", () => {
|
|
||||||
const handleAction = vi.fn(async () => jsonResult({ ok: true }));
|
|
||||||
const accountPlugin: ChannelPlugin = {
|
|
||||||
id: "discord",
|
|
||||||
meta: {
|
|
||||||
id: "discord",
|
|
||||||
label: "Discord",
|
|
||||||
selectionLabel: "Discord",
|
|
||||||
docsPath: "/channels/discord",
|
|
||||||
blurb: "Discord test plugin.",
|
|
||||||
},
|
|
||||||
capabilities: { chatTypes: ["direct"] },
|
|
||||||
config: {
|
|
||||||
listAccountIds: () => ["default"],
|
|
||||||
resolveAccount: () => ({}),
|
|
||||||
},
|
|
||||||
actions: {
|
|
||||||
listActions: () => ["send"],
|
|
||||||
handleAction,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
setActivePluginRegistry(
|
|
||||||
createTestRegistry([
|
|
||||||
{
|
|
||||||
pluginId: "discord",
|
|
||||||
source: "test",
|
|
||||||
plugin: accountPlugin,
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
handleAction.mockClear();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
setActivePluginRegistry(createTestRegistry([]));
|
|
||||||
vi.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("propagates defaultAccountId into params", async () => {
|
|
||||||
await runMessageAction({
|
|
||||||
cfg: {} as OpenClawConfig,
|
|
||||||
action: "send",
|
|
||||||
params: {
|
|
||||||
channel: "discord",
|
|
||||||
target: "channel:123",
|
|
||||||
message: "hi",
|
|
||||||
},
|
|
||||||
defaultAccountId: "ops",
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(handleAction).toHaveBeenCalled();
|
|
||||||
const ctx = (handleAction.mock.calls as unknown as Array<[unknown]>)[0]?.[0] as
|
|
||||||
| {
|
|
||||||
accountId?: string | null;
|
|
||||||
params: Record<string, unknown>;
|
|
||||||
}
|
|
||||||
| undefined;
|
|
||||||
if (!ctx) {
|
|
||||||
throw new Error("expected action context");
|
|
||||||
}
|
|
||||||
expect(ctx.accountId).toBe("ops");
|
|
||||||
expect(ctx.params.accountId).toBe("ops");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("falls back to the agent's bound account when accountId is omitted", async () => {
|
|
||||||
await runMessageAction({
|
|
||||||
cfg: {
|
|
||||||
bindings: [{ agentId: "agent-b", match: { channel: "discord", accountId: "account-b" } }],
|
|
||||||
} as OpenClawConfig,
|
|
||||||
action: "send",
|
|
||||||
params: {
|
|
||||||
channel: "discord",
|
|
||||||
target: "channel:123",
|
|
||||||
message: "hi",
|
|
||||||
},
|
|
||||||
agentId: "agent-b",
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(handleAction).toHaveBeenCalled();
|
|
||||||
const ctx = (handleAction.mock.calls as unknown as Array<[unknown]>)[0]?.[0] as
|
|
||||||
| {
|
|
||||||
accountId?: string | null;
|
|
||||||
params: Record<string, unknown>;
|
|
||||||
}
|
|
||||||
| undefined;
|
|
||||||
if (!ctx) {
|
|
||||||
throw new Error("expected action context");
|
|
||||||
}
|
|
||||||
expect(ctx.accountId).toBe("account-b");
|
|
||||||
expect(ctx.params.accountId).toBe("account-b");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue