test: use synthetic message action fixtures

This commit is contained in:
Peter Steinberger
2026-04-20 23:24:38 +01:00
parent c1be9ac0a7
commit 11eae6b2d8
9 changed files with 204 additions and 198 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
export function createFeishuMessageActionBootstrapRegistryMock() {
export function createPinboardMessageActionBootstrapRegistryMock() {
return (channel: string) =>
channel === "feishu"
channel === "pinboard"
? {
actions: {
messageActionTargetAliases: {

View File

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

View File

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