mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:00:43 +00:00
test: use synthetic message action fixtures
This commit is contained in:
@@ -4,7 +4,13 @@ import { normalizeMessageActionInput } from "./message-action-normalization.js";
|
||||
vi.mock("../../channels/plugins/bootstrap-registry.js", async () => ({
|
||||
getBootstrapChannelPlugin: (
|
||||
await import("./message-action-test-fixtures.js")
|
||||
).createFeishuMessageActionBootstrapRegistryMock(),
|
||||
).createPinboardMessageActionBootstrapRegistryMock(),
|
||||
}));
|
||||
|
||||
vi.mock("../../utils/message-channel.js", () => ({
|
||||
isDeliverableMessageChannel: (value: string) => ["workspace", "forum"].includes(value),
|
||||
normalizeMessageChannel: (value?: string | null) =>
|
||||
typeof value === "string" ? value.trim().toLowerCase() : undefined,
|
||||
}));
|
||||
|
||||
describe("normalizeMessageActionInput", () => {
|
||||
@@ -66,10 +72,10 @@ describe("normalizeMessageActionInput", () => {
|
||||
},
|
||||
toolContext: {
|
||||
currentChannelId: "C1",
|
||||
currentChannelProvider: "slack",
|
||||
currentChannelProvider: "workspace",
|
||||
},
|
||||
},
|
||||
expectedFields: { channel: "slack" },
|
||||
expectedFields: { channel: "workspace" },
|
||||
},
|
||||
{
|
||||
input: {
|
||||
@@ -110,7 +116,7 @@ describe("normalizeMessageActionInput", () => {
|
||||
input: {
|
||||
action: "pin",
|
||||
args: {
|
||||
channel: "feishu",
|
||||
channel: "pinboard",
|
||||
messageId: "om_123",
|
||||
},
|
||||
},
|
||||
@@ -121,7 +127,7 @@ describe("normalizeMessageActionInput", () => {
|
||||
input: {
|
||||
action: "list-pins",
|
||||
args: {
|
||||
channel: "feishu",
|
||||
channel: "pinboard",
|
||||
chatId: "oc_123",
|
||||
},
|
||||
},
|
||||
@@ -132,12 +138,12 @@ describe("normalizeMessageActionInput", () => {
|
||||
input: {
|
||||
action: "read",
|
||||
args: {
|
||||
channel: "slack",
|
||||
channel: "workspace",
|
||||
messageId: "123.456",
|
||||
},
|
||||
toolContext: {
|
||||
currentChannelId: "C12345678",
|
||||
currentChannelProvider: "slack",
|
||||
currentChannelProvider: "workspace",
|
||||
},
|
||||
},
|
||||
expectedFields: { target: "C12345678", messageId: "123.456" },
|
||||
|
||||
@@ -35,9 +35,9 @@ describe("message action media helpers", () => {
|
||||
resolveExtraActionMediaSourceParamKeys({
|
||||
cfg,
|
||||
action: "send",
|
||||
channel: "slack",
|
||||
channel: "workspace",
|
||||
args: {
|
||||
channel: "slack",
|
||||
channel: "workspace",
|
||||
target: "#C12345678",
|
||||
message: "hi",
|
||||
media: "https://example.com/photo.png",
|
||||
@@ -157,7 +157,7 @@ describe("message action media helpers", () => {
|
||||
}
|
||||
});
|
||||
|
||||
maybeIt("normalizes Discord event image sandbox media params", async () => {
|
||||
maybeIt("normalizes extension event image sandbox media params", async () => {
|
||||
const sandboxRoot = await fs.mkdtemp(path.join(os.tmpdir(), "msg-params-image-"));
|
||||
try {
|
||||
const args: Record<string, unknown> = {
|
||||
@@ -180,7 +180,7 @@ describe("message action media helpers", () => {
|
||||
}
|
||||
});
|
||||
|
||||
maybeIt("normalizes Matrix avatarPath and avatarUrl sandbox media params", async () => {
|
||||
maybeIt("normalizes extension avatarPath and avatarUrl sandbox media params", async () => {
|
||||
const sandboxRoot = await fs.mkdtemp(path.join(os.tmpdir(), "msg-params-avatar-"));
|
||||
try {
|
||||
const args: Record<string, unknown> = {
|
||||
@@ -227,7 +227,7 @@ describe("message action media helpers", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
maybeIt("normalizes Matrix snake_case avatar_path and avatar_url aliases", async () => {
|
||||
maybeIt("normalizes extension snake_case avatar_path and avatar_url aliases", async () => {
|
||||
const sandboxRoot = await fs.mkdtemp(path.join(os.tmpdir(), "msg-params-avatar-snake-"));
|
||||
try {
|
||||
const args: Record<string, unknown> = {
|
||||
@@ -253,7 +253,7 @@ describe("message action media helpers", () => {
|
||||
}
|
||||
});
|
||||
|
||||
maybeIt("prefers canonical Matrix media params over invalid snake_case aliases", async () => {
|
||||
maybeIt("prefers canonical extension media params over invalid snake_case aliases", async () => {
|
||||
const sandboxRoot = await fs.mkdtemp(path.join(os.tmpdir(), "msg-params-avatar-canonical-"));
|
||||
try {
|
||||
const args: Record<string, unknown> = {
|
||||
@@ -369,7 +369,7 @@ describe("message action media helpers", () => {
|
||||
};
|
||||
await hydrateAttachmentParamsForAction({
|
||||
cfg,
|
||||
channel: "slack",
|
||||
channel: "workspace",
|
||||
args: mediaArgs,
|
||||
action: "sendAttachment",
|
||||
dryRun: true,
|
||||
@@ -382,7 +382,7 @@ describe("message action media helpers", () => {
|
||||
};
|
||||
await hydrateAttachmentParamsForAction({
|
||||
cfg,
|
||||
channel: "slack",
|
||||
channel: "workspace",
|
||||
args: fileArgs,
|
||||
action: "sendAttachment",
|
||||
dryRun: true,
|
||||
@@ -398,7 +398,7 @@ describe("message action media helpers", () => {
|
||||
|
||||
await hydrateAttachmentParamsForAction({
|
||||
cfg,
|
||||
channel: "slack",
|
||||
channel: "workspace",
|
||||
args,
|
||||
action: "sendAttachment",
|
||||
dryRun: true,
|
||||
@@ -441,7 +441,7 @@ describe("message action sandbox media hydration", () => {
|
||||
await expect(
|
||||
hydrateAttachmentParamsForAction({
|
||||
cfg,
|
||||
channel: "slack",
|
||||
channel: "workspace",
|
||||
args,
|
||||
action: "sendAttachment",
|
||||
mediaPolicy,
|
||||
|
||||
@@ -7,30 +7,30 @@ import {
|
||||
createTestRegistry,
|
||||
} from "../../test-utils/channel-plugins.js";
|
||||
import {
|
||||
directChatConfig,
|
||||
directChatTestPlugin,
|
||||
directOutbound,
|
||||
forumTestPlugin,
|
||||
runDryAction,
|
||||
runDrySend,
|
||||
slackConfig,
|
||||
slackTestPlugin,
|
||||
telegramTestPlugin,
|
||||
whatsappConfig,
|
||||
whatsappTestPlugin,
|
||||
workspaceConfig,
|
||||
workspaceTestPlugin,
|
||||
} from "./message-action-runner.test-helpers.js";
|
||||
|
||||
const imessageTestPlugin: ChannelPlugin = {
|
||||
const localChatTestPlugin: ChannelPlugin = {
|
||||
...createChannelTestPluginBase({
|
||||
id: "imessage",
|
||||
label: "iMessage",
|
||||
docsPath: "/channels/imessage",
|
||||
id: "localchat",
|
||||
label: "Local Chat",
|
||||
docsPath: "/channels/localchat",
|
||||
capabilities: { chatTypes: ["direct", "group"], media: true },
|
||||
}),
|
||||
meta: {
|
||||
id: "imessage",
|
||||
label: "iMessage",
|
||||
selectionLabel: "iMessage (imsg)",
|
||||
docsPath: "/channels/imessage",
|
||||
blurb: "iMessage test stub.",
|
||||
aliases: ["imsg"],
|
||||
id: "localchat",
|
||||
label: "Local Chat",
|
||||
selectionLabel: "Local Chat (local)",
|
||||
docsPath: "/channels/localchat",
|
||||
blurb: "Local chat test stub.",
|
||||
aliases: ["local"],
|
||||
},
|
||||
outbound: directOutbound,
|
||||
messaging: {
|
||||
@@ -47,24 +47,24 @@ describe("runMessageAction context isolation", () => {
|
||||
setActivePluginRegistry(
|
||||
createTestRegistry([
|
||||
{
|
||||
pluginId: "slack",
|
||||
pluginId: "workspace",
|
||||
source: "test",
|
||||
plugin: slackTestPlugin,
|
||||
plugin: workspaceTestPlugin,
|
||||
},
|
||||
{
|
||||
pluginId: "whatsapp",
|
||||
pluginId: "directchat",
|
||||
source: "test",
|
||||
plugin: whatsappTestPlugin,
|
||||
plugin: directChatTestPlugin,
|
||||
},
|
||||
{
|
||||
pluginId: "telegram",
|
||||
pluginId: "forum",
|
||||
source: "test",
|
||||
plugin: telegramTestPlugin,
|
||||
plugin: forumTestPlugin,
|
||||
},
|
||||
{
|
||||
pluginId: "imessage",
|
||||
pluginId: "localchat",
|
||||
source: "test",
|
||||
plugin: imessageTestPlugin,
|
||||
plugin: localChatTestPlugin,
|
||||
},
|
||||
]),
|
||||
);
|
||||
@@ -77,9 +77,9 @@ describe("runMessageAction context isolation", () => {
|
||||
it.each([
|
||||
{
|
||||
name: "allows send when target matches current channel",
|
||||
cfg: slackConfig,
|
||||
cfg: workspaceConfig,
|
||||
actionParams: {
|
||||
channel: "slack",
|
||||
channel: "workspace",
|
||||
target: "#C12345678",
|
||||
message: "hi",
|
||||
},
|
||||
@@ -87,27 +87,27 @@ describe("runMessageAction context isolation", () => {
|
||||
},
|
||||
{
|
||||
name: "accepts legacy to parameter for send",
|
||||
cfg: slackConfig,
|
||||
cfg: workspaceConfig,
|
||||
actionParams: {
|
||||
channel: "slack",
|
||||
channel: "workspace",
|
||||
to: "#C12345678",
|
||||
message: "hi",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "defaults to current channel when target is omitted",
|
||||
cfg: slackConfig,
|
||||
cfg: workspaceConfig,
|
||||
actionParams: {
|
||||
channel: "slack",
|
||||
channel: "workspace",
|
||||
message: "hi",
|
||||
},
|
||||
toolContext: { currentChannelId: "C12345678" },
|
||||
},
|
||||
{
|
||||
name: "allows media-only send when target matches current channel",
|
||||
cfg: slackConfig,
|
||||
cfg: workspaceConfig,
|
||||
actionParams: {
|
||||
channel: "slack",
|
||||
channel: "workspace",
|
||||
target: "#C12345678",
|
||||
media: "https://example.com/note.ogg",
|
||||
},
|
||||
@@ -115,9 +115,9 @@ describe("runMessageAction context isolation", () => {
|
||||
},
|
||||
{
|
||||
name: "allows send when poll booleans are explicitly false",
|
||||
cfg: slackConfig,
|
||||
cfg: workspaceConfig,
|
||||
actionParams: {
|
||||
channel: "slack",
|
||||
channel: "workspace",
|
||||
target: "#C12345678",
|
||||
message: "hi",
|
||||
pollMulti: false,
|
||||
@@ -138,31 +138,31 @@ describe("runMessageAction context isolation", () => {
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "send when target differs from current slack channel",
|
||||
name: "send when target differs from current workspace channel",
|
||||
run: () =>
|
||||
runDrySend({
|
||||
cfg: slackConfig,
|
||||
cfg: workspaceConfig,
|
||||
actionParams: {
|
||||
channel: "slack",
|
||||
channel: "workspace",
|
||||
target: "channel:C99999999",
|
||||
message: "hi",
|
||||
},
|
||||
toolContext: { currentChannelId: "C12345678", currentChannelProvider: "slack" },
|
||||
toolContext: { currentChannelId: "C12345678", currentChannelProvider: "workspace" },
|
||||
}),
|
||||
expectedKind: "send",
|
||||
},
|
||||
{
|
||||
name: "thread-reply when channelId differs from current slack channel",
|
||||
name: "thread-reply when channelId differs from current workspace channel",
|
||||
run: () =>
|
||||
runDryAction({
|
||||
cfg: slackConfig,
|
||||
cfg: workspaceConfig,
|
||||
action: "thread-reply",
|
||||
actionParams: {
|
||||
channel: "slack",
|
||||
channel: "workspace",
|
||||
target: "C99999999",
|
||||
message: "hi",
|
||||
},
|
||||
toolContext: { currentChannelId: "C12345678", currentChannelProvider: "slack" },
|
||||
toolContext: { currentChannelId: "C12345678", currentChannelProvider: "workspace" },
|
||||
}),
|
||||
expectedKind: "action",
|
||||
},
|
||||
@@ -173,34 +173,34 @@ describe("runMessageAction context isolation", () => {
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "whatsapp match",
|
||||
channel: "whatsapp",
|
||||
name: "direct chat match",
|
||||
channel: "directchat",
|
||||
target: "123@g.us",
|
||||
currentChannelId: "123@g.us",
|
||||
},
|
||||
{
|
||||
name: "imessage match",
|
||||
channel: "imessage",
|
||||
target: "imessage:+15551234567",
|
||||
currentChannelId: "imessage:+15551234567",
|
||||
name: "local chat match",
|
||||
channel: "localchat",
|
||||
target: "localchat:+15551234567",
|
||||
currentChannelId: "localchat:+15551234567",
|
||||
},
|
||||
{
|
||||
name: "whatsapp mismatch",
|
||||
channel: "whatsapp",
|
||||
name: "direct chat mismatch",
|
||||
channel: "directchat",
|
||||
target: "456@g.us",
|
||||
currentChannelId: "123@g.us",
|
||||
currentChannelProvider: "whatsapp",
|
||||
currentChannelProvider: "directchat",
|
||||
},
|
||||
{
|
||||
name: "imessage mismatch",
|
||||
channel: "imessage",
|
||||
target: "imessage:+15551230000",
|
||||
currentChannelId: "imessage:+15551234567",
|
||||
currentChannelProvider: "imessage",
|
||||
name: "local chat mismatch",
|
||||
channel: "localchat",
|
||||
target: "localchat:+15551230000",
|
||||
currentChannelId: "localchat:+15551234567",
|
||||
currentChannelProvider: "localchat",
|
||||
},
|
||||
] as const)("$name", async (testCase) => {
|
||||
const result = await runDrySend({
|
||||
cfg: whatsappConfig,
|
||||
cfg: directChatConfig,
|
||||
actionParams: {
|
||||
channel: testCase.channel,
|
||||
target: testCase.target,
|
||||
@@ -222,12 +222,12 @@ describe("runMessageAction context isolation", () => {
|
||||
name: "infers channel + target from tool context when missing",
|
||||
cfg: {
|
||||
channels: {
|
||||
slack: {
|
||||
botToken: "xoxb-test",
|
||||
appToken: "xapp-test",
|
||||
workspace: {
|
||||
botToken: "workspace-test",
|
||||
appToken: "workspace-app-test",
|
||||
},
|
||||
telegram: {
|
||||
token: "tg-test",
|
||||
forum: {
|
||||
token: "forum-test",
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
@@ -235,35 +235,35 @@ describe("runMessageAction context isolation", () => {
|
||||
actionParams: {
|
||||
message: "hi",
|
||||
},
|
||||
toolContext: { currentChannelId: "C12345678", currentChannelProvider: "slack" },
|
||||
toolContext: { currentChannelId: "C12345678", currentChannelProvider: "workspace" },
|
||||
expectedKind: "send",
|
||||
expectedChannel: "slack",
|
||||
expectedChannel: "workspace",
|
||||
},
|
||||
{
|
||||
name: "falls back to tool-context provider when channel param is an id",
|
||||
cfg: slackConfig,
|
||||
cfg: workspaceConfig,
|
||||
action: "send" as const,
|
||||
actionParams: {
|
||||
channel: "C12345678",
|
||||
target: "#C12345678",
|
||||
message: "hi",
|
||||
},
|
||||
toolContext: { currentChannelId: "C12345678", currentChannelProvider: "slack" },
|
||||
toolContext: { currentChannelId: "C12345678", currentChannelProvider: "workspace" },
|
||||
expectedKind: "send",
|
||||
expectedChannel: "slack",
|
||||
expectedChannel: "workspace",
|
||||
},
|
||||
{
|
||||
name: "falls back to tool-context provider for broadcast channel ids",
|
||||
cfg: slackConfig,
|
||||
cfg: workspaceConfig,
|
||||
action: "broadcast" as const,
|
||||
actionParams: {
|
||||
targets: ["channel:C12345678"],
|
||||
channel: "C12345678",
|
||||
message: "hi",
|
||||
},
|
||||
toolContext: { currentChannelProvider: "slack" },
|
||||
toolContext: { currentChannelProvider: "workspace" },
|
||||
expectedKind: "broadcast",
|
||||
expectedChannel: "slack",
|
||||
expectedChannel: "workspace",
|
||||
},
|
||||
])("$name", async ({ cfg, action, actionParams, toolContext, expectedKind, expectedChannel }) => {
|
||||
const result = await runDryAction({
|
||||
@@ -281,20 +281,20 @@ describe("runMessageAction context isolation", () => {
|
||||
{
|
||||
name: "blocks cross-provider sends by default",
|
||||
action: "send" as const,
|
||||
cfg: slackConfig,
|
||||
cfg: workspaceConfig,
|
||||
actionParams: {
|
||||
channel: "telegram",
|
||||
channel: "forum",
|
||||
target: "@opsbot",
|
||||
message: "hi",
|
||||
},
|
||||
toolContext: { currentChannelId: "C12345678", currentChannelProvider: "slack" },
|
||||
toolContext: { currentChannelId: "C12345678", currentChannelProvider: "workspace" },
|
||||
message: /Cross-context messaging denied/,
|
||||
},
|
||||
{
|
||||
name: "blocks same-provider cross-context when disabled",
|
||||
action: "send" as const,
|
||||
cfg: {
|
||||
...slackConfig,
|
||||
...workspaceConfig,
|
||||
tools: {
|
||||
message: {
|
||||
crossContext: {
|
||||
@@ -304,18 +304,18 @@ describe("runMessageAction context isolation", () => {
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
actionParams: {
|
||||
channel: "slack",
|
||||
channel: "workspace",
|
||||
target: "channel:C99999999",
|
||||
message: "hi",
|
||||
},
|
||||
toolContext: { currentChannelId: "C12345678", currentChannelProvider: "slack" },
|
||||
toolContext: { currentChannelId: "C12345678", currentChannelProvider: "workspace" },
|
||||
message: /Cross-context messaging denied/,
|
||||
},
|
||||
{
|
||||
name: "blocks same-provider cross-context uploads when disabled",
|
||||
action: "upload-file" as const,
|
||||
cfg: {
|
||||
...slackConfig,
|
||||
...workspaceConfig,
|
||||
tools: {
|
||||
message: {
|
||||
crossContext: {
|
||||
@@ -325,19 +325,19 @@ describe("runMessageAction context isolation", () => {
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
actionParams: {
|
||||
channel: "slack",
|
||||
channel: "workspace",
|
||||
target: "channel:C99999999",
|
||||
filePath: "/tmp/report.png",
|
||||
},
|
||||
toolContext: { currentChannelId: "C12345678", currentChannelProvider: "slack" },
|
||||
toolContext: { currentChannelId: "C12345678", currentChannelProvider: "workspace" },
|
||||
message: /Cross-context messaging denied/,
|
||||
},
|
||||
{
|
||||
name: "rejects channel ids that resolve to user targets",
|
||||
action: "channel-info" as const,
|
||||
cfg: slackConfig,
|
||||
cfg: workspaceConfig,
|
||||
actionParams: {
|
||||
channel: "slack",
|
||||
channel: "workspace",
|
||||
channelId: "U12345678",
|
||||
},
|
||||
message: 'Channel id "U12345678" resolved to a user target.',
|
||||
@@ -358,9 +358,9 @@ describe("runMessageAction context isolation", () => {
|
||||
name: "send",
|
||||
run: (abortSignal: AbortSignal) =>
|
||||
runDrySend({
|
||||
cfg: slackConfig,
|
||||
cfg: workspaceConfig,
|
||||
actionParams: {
|
||||
channel: "slack",
|
||||
channel: "workspace",
|
||||
target: "#C12345678",
|
||||
message: "hi",
|
||||
},
|
||||
@@ -371,11 +371,11 @@ describe("runMessageAction context isolation", () => {
|
||||
name: "broadcast",
|
||||
run: (abortSignal: AbortSignal) =>
|
||||
runDryAction({
|
||||
cfg: slackConfig,
|
||||
cfg: workspaceConfig,
|
||||
action: "broadcast",
|
||||
actionParams: {
|
||||
targets: ["channel:C12345678"],
|
||||
channel: "slack",
|
||||
channel: "workspace",
|
||||
message: "hi",
|
||||
},
|
||||
abortSignal,
|
||||
|
||||
@@ -3,10 +3,10 @@ import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { setActivePluginRegistry } from "../../plugins/runtime.js";
|
||||
import { createTestRegistry } from "../../test-utils/channel-plugins.js";
|
||||
import {
|
||||
forumTestPlugin,
|
||||
runDrySend,
|
||||
slackConfig,
|
||||
slackTestPlugin,
|
||||
telegramTestPlugin,
|
||||
workspaceConfig,
|
||||
workspaceTestPlugin,
|
||||
} from "./message-action-runner.test-helpers.js";
|
||||
|
||||
describe("runMessageAction send validation", () => {
|
||||
@@ -14,14 +14,14 @@ describe("runMessageAction send validation", () => {
|
||||
setActivePluginRegistry(
|
||||
createTestRegistry([
|
||||
{
|
||||
pluginId: "slack",
|
||||
pluginId: "workspace",
|
||||
source: "test",
|
||||
plugin: slackTestPlugin,
|
||||
plugin: workspaceTestPlugin,
|
||||
},
|
||||
{
|
||||
pluginId: "telegram",
|
||||
pluginId: "forum",
|
||||
source: "test",
|
||||
plugin: telegramTestPlugin,
|
||||
plugin: forumTestPlugin,
|
||||
},
|
||||
]),
|
||||
);
|
||||
@@ -34,9 +34,9 @@ describe("runMessageAction send validation", () => {
|
||||
it("requires message when no media hint is provided", async () => {
|
||||
await expect(
|
||||
runDrySend({
|
||||
cfg: slackConfig,
|
||||
cfg: workspaceConfig,
|
||||
actionParams: {
|
||||
channel: "slack",
|
||||
channel: "workspace",
|
||||
target: "#C12345678",
|
||||
},
|
||||
toolContext: { currentChannelId: "C12345678" },
|
||||
@@ -48,13 +48,13 @@ describe("runMessageAction send validation", () => {
|
||||
const result = await runDrySend({
|
||||
cfg: {
|
||||
channels: {
|
||||
telegram: {
|
||||
botToken: "telegram-test",
|
||||
forum: {
|
||||
botToken: "forum-test",
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
actionParams: {
|
||||
channel: "telegram",
|
||||
channel: "forum",
|
||||
target: "123456",
|
||||
interactive: {
|
||||
blocks: [
|
||||
@@ -70,11 +70,11 @@ describe("runMessageAction send validation", () => {
|
||||
expect(result.kind).toBe("send");
|
||||
});
|
||||
|
||||
it("allows send when only Slack blocks are provided", async () => {
|
||||
it("allows send when only channel-specific blocks are provided", async () => {
|
||||
const result = await runDrySend({
|
||||
cfg: slackConfig,
|
||||
cfg: workspaceConfig,
|
||||
actionParams: {
|
||||
channel: "slack",
|
||||
channel: "workspace",
|
||||
target: "#C12345678",
|
||||
blocks: [{ type: "divider" }],
|
||||
},
|
||||
@@ -88,7 +88,7 @@ describe("runMessageAction send validation", () => {
|
||||
{
|
||||
name: "structured poll params",
|
||||
actionParams: {
|
||||
channel: "slack",
|
||||
channel: "workspace",
|
||||
target: "#C12345678",
|
||||
message: "hi",
|
||||
pollQuestion: "Ready?",
|
||||
@@ -98,7 +98,7 @@ describe("runMessageAction send validation", () => {
|
||||
{
|
||||
name: "string-encoded poll params",
|
||||
actionParams: {
|
||||
channel: "slack",
|
||||
channel: "workspace",
|
||||
target: "#C12345678",
|
||||
message: "hi",
|
||||
pollDurationSeconds: "60",
|
||||
@@ -108,7 +108,7 @@ describe("runMessageAction send validation", () => {
|
||||
{
|
||||
name: "snake_case poll params",
|
||||
actionParams: {
|
||||
channel: "slack",
|
||||
channel: "workspace",
|
||||
target: "#C12345678",
|
||||
message: "hi",
|
||||
poll_question: "Ready?",
|
||||
@@ -119,7 +119,7 @@ describe("runMessageAction send validation", () => {
|
||||
{
|
||||
name: "negative poll duration params",
|
||||
actionParams: {
|
||||
channel: "slack",
|
||||
channel: "workspace",
|
||||
target: "#C12345678",
|
||||
message: "hi",
|
||||
pollDurationSeconds: -5,
|
||||
@@ -128,7 +128,7 @@ describe("runMessageAction send validation", () => {
|
||||
])("rejects send actions that include $name", async ({ actionParams }) => {
|
||||
await expect(
|
||||
runDrySend({
|
||||
cfg: slackConfig,
|
||||
cfg: workspaceConfig,
|
||||
actionParams,
|
||||
toolContext: { currentChannelId: "C12345678" },
|
||||
}),
|
||||
|
||||
@@ -9,18 +9,18 @@ import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import { createChannelTestPluginBase } from "../../test-utils/channel-plugins.js";
|
||||
import { runMessageAction } from "./message-action-runner.js";
|
||||
|
||||
export const slackConfig = {
|
||||
export const workspaceConfig = {
|
||||
channels: {
|
||||
slack: {
|
||||
botToken: "xoxb-test",
|
||||
appToken: "xapp-test",
|
||||
workspace: {
|
||||
botToken: "workspace-test",
|
||||
appToken: "workspace-app-test",
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
export const whatsappConfig = {
|
||||
export const directChatConfig = {
|
||||
channels: {
|
||||
whatsapp: {
|
||||
directchat: {
|
||||
allowFrom: ["*"],
|
||||
},
|
||||
},
|
||||
@@ -60,7 +60,7 @@ export const runDrySend = (params: {
|
||||
|
||||
type ResolvedTestTarget = { to: string; kind: ChannelDirectoryEntryKind };
|
||||
|
||||
export function normalizeSlackTarget(raw: string): string {
|
||||
export function normalizeWorkspaceTarget(raw: string): string {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) {
|
||||
return trimmed;
|
||||
@@ -82,7 +82,7 @@ export function normalizeSlackTarget(raw: string): string {
|
||||
}
|
||||
|
||||
export function createConfiguredTestPlugin(params: {
|
||||
id: "slack" | "telegram" | "whatsapp";
|
||||
id: string;
|
||||
isConfigured: (cfg: OpenClawConfig) => boolean;
|
||||
normalizeTarget: (raw: string) => string | undefined;
|
||||
resolveTarget: (input: string) => ResolvedTestTarget | null;
|
||||
@@ -114,12 +114,12 @@ export function createConfiguredTestPlugin(params: {
|
||||
};
|
||||
}
|
||||
|
||||
export const slackTestPlugin = createConfiguredTestPlugin({
|
||||
id: "slack",
|
||||
isConfigured: (cfg) => Boolean(cfg.channels?.slack?.botToken?.trim()),
|
||||
normalizeTarget: (raw) => normalizeSlackTarget(raw) || undefined,
|
||||
export const workspaceTestPlugin = createConfiguredTestPlugin({
|
||||
id: "workspace",
|
||||
isConfigured: (cfg) => Boolean(cfg.channels?.workspace?.botToken?.trim()),
|
||||
normalizeTarget: (raw) => normalizeWorkspaceTarget(raw) || undefined,
|
||||
resolveTarget: (input) => {
|
||||
const normalized = normalizeSlackTarget(input);
|
||||
const normalized = normalizeWorkspaceTarget(input);
|
||||
if (!normalized) {
|
||||
return null;
|
||||
}
|
||||
@@ -131,9 +131,9 @@ export const slackTestPlugin = createConfiguredTestPlugin({
|
||||
},
|
||||
});
|
||||
|
||||
export const telegramTestPlugin = createConfiguredTestPlugin({
|
||||
id: "telegram",
|
||||
isConfigured: (cfg) => Boolean(cfg.channels?.telegram?.botToken?.trim()),
|
||||
export const forumTestPlugin = createConfiguredTestPlugin({
|
||||
id: "forum",
|
||||
isConfigured: (cfg) => Boolean(cfg.channels?.forum?.botToken?.trim()),
|
||||
normalizeTarget: (raw) => raw.trim() || undefined,
|
||||
resolveTarget: (input) => {
|
||||
const normalized = input.trim();
|
||||
@@ -141,15 +141,15 @@ export const telegramTestPlugin = createConfiguredTestPlugin({
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
to: normalized.replace(/^telegram:/i, ""),
|
||||
to: normalized.replace(/^forum:/i, ""),
|
||||
kind: normalized.startsWith("@") ? "user" : "group",
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export const whatsappTestPlugin = createConfiguredTestPlugin({
|
||||
id: "whatsapp",
|
||||
isConfigured: (cfg) => Boolean(cfg.channels?.whatsapp),
|
||||
export const directChatTestPlugin = createConfiguredTestPlugin({
|
||||
id: "directchat",
|
||||
isConfigured: (cfg) => Boolean(cfg.channels?.directchat),
|
||||
normalizeTarget: (raw) => raw.trim() || undefined,
|
||||
resolveTarget: (input) => {
|
||||
const normalized = input.trim();
|
||||
|
||||
@@ -4,7 +4,7 @@ import { actionHasTarget, actionRequiresTarget } from "./message-action-spec.js"
|
||||
vi.mock("../../channels/plugins/bootstrap-registry.js", async () => ({
|
||||
getBootstrapChannelPlugin: (
|
||||
await import("./message-action-test-fixtures.js")
|
||||
).createFeishuMessageActionBootstrapRegistryMock(),
|
||||
).createPinboardMessageActionBootstrapRegistryMock(),
|
||||
}));
|
||||
|
||||
describe("actionRequiresTarget", () => {
|
||||
@@ -26,32 +26,32 @@ describe("actionHasTarget", () => {
|
||||
{
|
||||
action: "read",
|
||||
params: { messageId: "msg_123" },
|
||||
ctx: { channel: "feishu" },
|
||||
ctx: { channel: "pinboard" },
|
||||
expected: true,
|
||||
},
|
||||
{ action: "edit", params: { messageId: " msg_123 " }, expected: true },
|
||||
{
|
||||
action: "pin",
|
||||
params: { messageId: "msg_123" },
|
||||
ctx: { channel: "feishu" },
|
||||
ctx: { channel: "pinboard" },
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
action: "unpin",
|
||||
params: { messageId: "msg_123" },
|
||||
ctx: { channel: "feishu" },
|
||||
ctx: { channel: "pinboard" },
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
action: "list-pins",
|
||||
params: { chatId: "oc_123" },
|
||||
ctx: { channel: "feishu" },
|
||||
ctx: { channel: "pinboard" },
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
action: "channel-info",
|
||||
params: { chatId: "oc_123" },
|
||||
ctx: { channel: "feishu" },
|
||||
ctx: { channel: "pinboard" },
|
||||
expected: true,
|
||||
},
|
||||
{ action: "react", params: { chatGuid: "chat-guid" }, expected: true },
|
||||
@@ -61,13 +61,13 @@ describe("actionHasTarget", () => {
|
||||
{
|
||||
action: "pin",
|
||||
params: { messageId: "msg_123" },
|
||||
ctx: { channel: "slack" },
|
||||
ctx: { channel: "workspace" },
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
action: "channel-info",
|
||||
params: { chatId: "oc_123" },
|
||||
ctx: { channel: "discord" },
|
||||
ctx: { channel: "richchat" },
|
||||
expected: false,
|
||||
},
|
||||
{ action: "edit", params: { messageId: " " }, expected: false },
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export function createFeishuMessageActionBootstrapRegistryMock() {
|
||||
export function createPinboardMessageActionBootstrapRegistryMock() {
|
||||
return (channel: string) =>
|
||||
channel === "feishu"
|
||||
channel === "pinboard"
|
||||
? {
|
||||
actions: {
|
||||
messageActionTargetAliases: {
|
||||
|
||||
@@ -16,13 +16,13 @@ class TestSeparator {
|
||||
constructor(readonly options: { divider: boolean; spacing: string }) {}
|
||||
}
|
||||
|
||||
class TestDiscordUiContainer {
|
||||
class TestRichUiContainer {
|
||||
constructor(readonly components: Array<TestTextDisplay | TestSeparator>) {}
|
||||
}
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
getChannelMessageAdapter: vi.fn((channel: string) =>
|
||||
channel === "discord"
|
||||
channel === "richchat"
|
||||
? {
|
||||
supportsComponentsV2: true,
|
||||
buildCrossContextComponents: ({
|
||||
@@ -39,7 +39,7 @@ const mocks = vi.hoisted(() => ({
|
||||
components.push(new TestSeparator({ divider: true, spacing: "small" }));
|
||||
}
|
||||
components.push(new TestTextDisplay(`*From ${originLabel}*`));
|
||||
return [new TestDiscordUiContainer(components)];
|
||||
return [new TestRichUiContainer(components)];
|
||||
},
|
||||
}
|
||||
: { supportsComponentsV2: false },
|
||||
@@ -49,7 +49,7 @@ const mocks = vi.hoisted(() => ({
|
||||
if (!trimmed) {
|
||||
return undefined;
|
||||
}
|
||||
if (channel === "slack") {
|
||||
if (channel === "workspace") {
|
||||
return trimmed.replace(/^#/, "");
|
||||
}
|
||||
return trimmed;
|
||||
@@ -75,18 +75,18 @@ vi.mock("./target-resolver.js", () => ({
|
||||
lookupDirectoryDisplay: mocks.lookupDirectoryDisplay,
|
||||
}));
|
||||
|
||||
const slackConfig = {
|
||||
const workspaceConfig = {
|
||||
channels: {
|
||||
slack: {
|
||||
botToken: "xoxb-test",
|
||||
appToken: "xapp-test",
|
||||
workspace: {
|
||||
botToken: "workspace-test",
|
||||
appToken: "workspace-app-test",
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
const discordConfig = {
|
||||
const richChatConfig = {
|
||||
channels: {
|
||||
discord: {},
|
||||
richchat: {},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
@@ -134,53 +134,53 @@ describe("outbound policy helpers", () => {
|
||||
it.each([
|
||||
{
|
||||
cfg: {
|
||||
...slackConfig,
|
||||
...workspaceConfig,
|
||||
tools: {
|
||||
message: { crossContext: { allowAcrossProviders: true } },
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
channel: "telegram",
|
||||
channel: "forum",
|
||||
action: "send" as const,
|
||||
to: "telegram:@ops",
|
||||
to: "forum:@ops",
|
||||
currentChannelId: "C12345678",
|
||||
currentChannelProvider: "slack",
|
||||
currentChannelProvider: "workspace",
|
||||
expected: "allow" as const,
|
||||
},
|
||||
{
|
||||
cfg: slackConfig,
|
||||
channel: "telegram",
|
||||
cfg: workspaceConfig,
|
||||
channel: "forum",
|
||||
action: "send" as const,
|
||||
to: "telegram:@ops",
|
||||
to: "forum:@ops",
|
||||
currentChannelId: "C12345678",
|
||||
currentChannelProvider: "slack",
|
||||
expected: /target provider "telegram" while bound to "slack"/,
|
||||
currentChannelProvider: "workspace",
|
||||
expected: /target provider "forum" while bound to "workspace"/,
|
||||
},
|
||||
{
|
||||
cfg: {
|
||||
...slackConfig,
|
||||
...workspaceConfig,
|
||||
tools: {
|
||||
message: { crossContext: { allowWithinProvider: false } },
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
channel: "slack",
|
||||
channel: "workspace",
|
||||
action: "send" as const,
|
||||
to: "C999",
|
||||
currentChannelId: "C123",
|
||||
currentChannelProvider: "slack",
|
||||
currentChannelProvider: "workspace",
|
||||
expected: /target="C999" while bound to "C123"/,
|
||||
},
|
||||
{
|
||||
cfg: {
|
||||
...slackConfig,
|
||||
...workspaceConfig,
|
||||
tools: {
|
||||
message: { crossContext: { allowWithinProvider: false } },
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
channel: "slack",
|
||||
channel: "workspace",
|
||||
action: "upload-file" as const,
|
||||
to: "C999",
|
||||
currentChannelId: "C123",
|
||||
currentChannelProvider: "slack",
|
||||
currentChannelProvider: "workspace",
|
||||
expected: /target="C999" while bound to "C123"/,
|
||||
},
|
||||
])("enforces cross-context policy for %j", (params) => {
|
||||
@@ -189,10 +189,10 @@ describe("outbound policy helpers", () => {
|
||||
|
||||
it("uses components when available and preferred", async () => {
|
||||
const decoration = await buildCrossContextDecoration({
|
||||
cfg: discordConfig,
|
||||
channel: "discord",
|
||||
cfg: richChatConfig,
|
||||
channel: "richchat",
|
||||
target: "123",
|
||||
toolContext: { currentChannelId: "C12345678", currentChannelProvider: "discord" },
|
||||
toolContext: { currentChannelId: "C12345678", currentChannelProvider: "richchat" },
|
||||
});
|
||||
|
||||
expect(decoration).not.toBeNull();
|
||||
@@ -211,12 +211,12 @@ describe("outbound policy helpers", () => {
|
||||
it("returns null when decoration is skipped and falls back to text markers", async () => {
|
||||
await expect(
|
||||
buildCrossContextDecoration({
|
||||
cfg: discordConfig,
|
||||
channel: "discord",
|
||||
cfg: richChatConfig,
|
||||
channel: "richchat",
|
||||
target: "123",
|
||||
toolContext: {
|
||||
currentChannelId: "C12345678",
|
||||
currentChannelProvider: "discord",
|
||||
currentChannelProvider: "richchat",
|
||||
skipCrossContextDecoration: true,
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -51,7 +51,7 @@ describe("normalizeChannelTargetInput", () => {
|
||||
|
||||
describe("normalizeTargetForProvider", () => {
|
||||
it.each([undefined, " "])("returns undefined for blank raw input %j", (raw) => {
|
||||
expect(normalizeTargetForProvider("telegram", raw)).toBeUndefined();
|
||||
expect(normalizeTargetForProvider("alpha", raw)).toBeUndefined();
|
||||
});
|
||||
|
||||
it.each([
|
||||
@@ -63,7 +63,7 @@ describe("normalizeTargetForProvider", () => {
|
||||
expected: "raw-id",
|
||||
},
|
||||
{
|
||||
provider: "telegram",
|
||||
provider: "alpha",
|
||||
setup: () => {
|
||||
getActivePluginChannelRegistryVersionMock.mockReturnValueOnce(1);
|
||||
getChannelPluginMock.mockReturnValueOnce(undefined);
|
||||
@@ -93,9 +93,9 @@ describe("normalizeTargetForProvider", () => {
|
||||
messaging: { normalizeTarget: secondNormalizer },
|
||||
});
|
||||
|
||||
expect(normalizeTargetForProvider("telegram", " abc ")).toBe("ABC");
|
||||
expect(normalizeTargetForProvider("telegram", " def ")).toBe("DEF");
|
||||
expect(normalizeTargetForProvider("telegram", " ghi ")).toBe("next:ghi");
|
||||
expect(normalizeTargetForProvider("alpha", " abc ")).toBe("ABC");
|
||||
expect(normalizeTargetForProvider("alpha", " def ")).toBe("DEF");
|
||||
expect(normalizeTargetForProvider("alpha", " ghi ")).toBe("next:ghi");
|
||||
|
||||
expect(getChannelPluginMock).toHaveBeenCalledTimes(2);
|
||||
expect(firstNormalizer).toHaveBeenCalledTimes(2);
|
||||
@@ -110,13 +110,13 @@ describe("normalizeTargetForProvider", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(normalizeTargetForProvider("telegram", " raw-id ")).toBeUndefined();
|
||||
expect(normalizeTargetForProvider("alpha", " raw-id ")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveNormalizedTargetInput", () => {
|
||||
it("returns undefined for blank input", () => {
|
||||
expect(resolveNormalizedTargetInput("telegram", " ")).toBeUndefined();
|
||||
expect(resolveNormalizedTargetInput("alpha", " ")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns raw and normalized values", () => {
|
||||
@@ -127,7 +127,7 @@ describe("resolveNormalizedTargetInput", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(resolveNormalizedTargetInput("telegram", " abc ")).toEqual({
|
||||
expect(resolveNormalizedTargetInput("alpha", " abc ")).toEqual({
|
||||
raw: "abc",
|
||||
normalized: "ABC",
|
||||
});
|
||||
@@ -147,7 +147,7 @@ describe("looksLikeTargetId", () => {
|
||||
|
||||
expect(
|
||||
looksLikeTargetId({
|
||||
channel: "telegram",
|
||||
channel: "alpha",
|
||||
raw: "room-1",
|
||||
normalized: "ROOM-1",
|
||||
}),
|
||||
@@ -159,7 +159,7 @@ describe("looksLikeTargetId", () => {
|
||||
"falls back to built-in id-like heuristics for %s",
|
||||
(raw) => {
|
||||
getChannelPluginMock.mockReturnValueOnce(undefined);
|
||||
expect(looksLikeTargetId({ channel: "slack", raw })).toBe(true);
|
||||
expect(looksLikeTargetId({ channel: "workspace", raw })).toBe(true);
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -180,7 +180,7 @@ describe("maybeResolvePluginMessagingTarget", () => {
|
||||
await expect(
|
||||
maybeResolvePluginMessagingTarget({
|
||||
cfg,
|
||||
channel: "slack",
|
||||
channel: "workspace",
|
||||
input: "general",
|
||||
requireIdLike: true,
|
||||
}),
|
||||
@@ -211,7 +211,7 @@ describe("maybeResolvePluginMessagingTarget", () => {
|
||||
await expect(
|
||||
maybeResolvePluginMessagingTarget({
|
||||
cfg,
|
||||
channel: "slack",
|
||||
channel: "workspace",
|
||||
input: " channel:c123abc ",
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
@@ -243,7 +243,7 @@ describe("buildTargetResolverSignature", () => {
|
||||
},
|
||||
});
|
||||
|
||||
const first = buildTargetResolverSignature("slack");
|
||||
const first = buildTargetResolverSignature("workspace");
|
||||
getChannelPluginMock.mockReturnValueOnce({
|
||||
messaging: {
|
||||
targetResolver: {
|
||||
@@ -252,7 +252,7 @@ describe("buildTargetResolverSignature", () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
const second = buildTargetResolverSignature("slack");
|
||||
const second = buildTargetResolverSignature("workspace");
|
||||
|
||||
expect(first).toBe(second);
|
||||
});
|
||||
@@ -266,7 +266,7 @@ describe("buildTargetResolverSignature", () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
const first = buildTargetResolverSignature("slack");
|
||||
const first = buildTargetResolverSignature("workspace");
|
||||
|
||||
getChannelPluginMock.mockReturnValueOnce({
|
||||
messaging: {
|
||||
@@ -276,7 +276,7 @@ describe("buildTargetResolverSignature", () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
const second = buildTargetResolverSignature("slack");
|
||||
const second = buildTargetResolverSignature("workspace");
|
||||
|
||||
expect(first).not.toBe(second);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user