test: use synthetic outbound dispatch fixtures

This commit is contained in:
Peter Steinberger
2026-04-20 23:47:49 +01:00
parent 59d18a13b7
commit 73f36b0c80
4 changed files with 280 additions and 279 deletions

View File

@@ -41,7 +41,7 @@ vi.mock("./outbound-session.js", () => ({
vi.mock("../../channels/plugins/bootstrap-registry.js", () => ({
getBootstrapChannelPlugin: (id: string) =>
id === "feishu"
id === "actionhub"
? {
actions: {
messageActionTargetAliases: {
@@ -164,14 +164,14 @@ describe("runMessageAction plugin dispatch", () => {
}),
);
const feishuLikePlugin: ChannelPlugin = {
id: "feishu",
const actionHubPlugin: ChannelPlugin = {
id: "actionhub",
meta: {
id: "feishu",
label: "Feishu",
selectionLabel: "Feishu",
docsPath: "/channels/feishu",
blurb: "Feishu action dispatch test plugin.",
id: "actionhub",
label: "Action Hub",
selectionLabel: "Action Hub",
docsPath: "/channels/actionhub",
blurb: "Action Hub action dispatch test plugin.",
},
capabilities: { chatTypes: ["direct", "channel"] },
config: createAlwaysConfiguredPluginConfig(),
@@ -192,9 +192,9 @@ describe("runMessageAction plugin dispatch", () => {
setActivePluginRegistry(
createTestRegistry([
{
pluginId: "feishu",
pluginId: "actionhub",
source: "test",
plugin: feishuLikePlugin,
plugin: actionHubPlugin,
},
]),
);
@@ -207,18 +207,18 @@ describe("runMessageAction plugin dispatch", () => {
vi.unstubAllEnvs();
});
it("dispatches messageId/chatId-based Feishu actions through the shared runner", async () => {
it("dispatches messageId/chatId-based plugin actions through the shared runner", async () => {
await runMessageAction({
cfg: {
channels: {
feishu: {
actionhub: {
enabled: true,
},
},
} as OpenClawConfig,
action: "pin",
params: {
channel: "feishu",
channel: "actionhub",
messageId: "om_123",
},
dryRun: false,
@@ -227,14 +227,14 @@ describe("runMessageAction plugin dispatch", () => {
await runMessageAction({
cfg: {
channels: {
feishu: {
actionhub: {
enabled: true,
},
},
} as OpenClawConfig,
action: "list-pins",
params: {
channel: "feishu",
channel: "actionhub",
chatId: "oc_123",
},
dryRun: false,
@@ -268,14 +268,14 @@ describe("runMessageAction plugin dispatch", () => {
await runMessageAction({
cfg: {
channels: {
feishu: {
actionhub: {
enabled: true,
},
},
} as OpenClawConfig,
action: "pin",
params: {
channel: "feishu",
channel: "actionhub",
messageId: "om_123",
},
defaultAccountId: "ops",
@@ -285,7 +285,7 @@ describe("runMessageAction plugin dispatch", () => {
agentId: "alpha",
toolContext: {
currentChannelId: "oc_123",
currentChannelProvider: "feishu",
currentChannelProvider: "actionhub",
currentThreadTs: "thread-456",
currentMessageId: "msg-789",
},
@@ -303,7 +303,7 @@ describe("runMessageAction plugin dispatch", () => {
mediaLocalRoots: expect.arrayContaining([expectedWorkspaceRoot]),
toolContext: expect.objectContaining({
currentChannelId: "oc_123",
currentChannelProvider: "feishu",
currentChannelProvider: "actionhub",
currentThreadTs: "thread-456",
currentMessageId: "msg-789",
}),
@@ -319,13 +319,13 @@ describe("runMessageAction plugin dispatch", () => {
}),
);
const gatewayPlugin: ChannelPlugin = {
id: "whatsapp",
id: "gatewaychat",
meta: {
id: "whatsapp",
label: "WhatsApp",
selectionLabel: "WhatsApp",
docsPath: "/channels/whatsapp",
blurb: "WhatsApp reaction test plugin.",
id: "gatewaychat",
label: "Gateway Chat",
selectionLabel: "Gateway Chat",
docsPath: "/channels/gatewaychat",
blurb: "Gateway Chat reaction test plugin.",
},
capabilities: { chatTypes: ["direct"], reactions: true },
config: createAlwaysConfiguredPluginConfig(),
@@ -339,7 +339,7 @@ describe("runMessageAction plugin dispatch", () => {
setActivePluginRegistry(
createTestRegistry([
{
pluginId: "whatsapp",
pluginId: "gatewaychat",
source: "test",
plugin: gatewayPlugin,
},
@@ -353,14 +353,14 @@ describe("runMessageAction plugin dispatch", () => {
const result = await runMessageAction({
cfg: {
channels: {
whatsapp: {
gatewaychat: {
enabled: true,
},
},
} as OpenClawConfig,
action: "react",
params: {
channel: "whatsapp",
channel: "gatewaychat",
to: "+15551234567",
chatJid: "+15551234567",
messageId: "wamid.1",
@@ -371,7 +371,7 @@ describe("runMessageAction plugin dispatch", () => {
sessionId: "session-123",
agentId: "alpha",
toolContext: {
currentChannelProvider: "whatsapp",
currentChannelProvider: "gatewaychat",
currentMessageId: "wamid.1",
},
gateway: {
@@ -385,14 +385,14 @@ describe("runMessageAction plugin dispatch", () => {
expect.objectContaining({
method: "message.action",
params: expect.objectContaining({
channel: "whatsapp",
channel: "gatewaychat",
action: "react",
requesterSenderId: "trusted-user",
sessionKey: "agent:alpha:main",
sessionId: "session-123",
agentId: "alpha",
toolContext: expect.objectContaining({
currentChannelProvider: "whatsapp",
currentChannelProvider: "gatewaychat",
currentMessageId: "wamid.1",
}),
idempotencyKey: "idem-gateway-action",
@@ -402,7 +402,7 @@ describe("runMessageAction plugin dispatch", () => {
expect(handleAction).not.toHaveBeenCalled();
expect(result).toMatchObject({
kind: "action",
channel: "whatsapp",
channel: "gatewaychat",
action: "react",
handledBy: "plugin",
payload: {
@@ -420,13 +420,13 @@ describe("runMessageAction plugin dispatch", () => {
}),
);
const policyPlugin: ChannelPlugin = {
id: "feishu",
id: "policydest",
meta: {
id: "feishu",
label: "Feishu",
selectionLabel: "Feishu",
docsPath: "/channels/feishu",
blurb: "Feishu policy test plugin.",
id: "policydest",
label: "Policy Destination",
selectionLabel: "Policy Destination",
docsPath: "/channels/policydest",
blurb: "Policy destination test plugin.",
},
capabilities: { chatTypes: ["direct", "channel"], media: true },
config: createAlwaysConfiguredPluginConfig(),
@@ -445,7 +445,7 @@ describe("runMessageAction plugin dispatch", () => {
setActivePluginRegistry(
createTestRegistry([
{
pluginId: "feishu",
pluginId: "policydest",
source: "test",
plugin: policyPlugin,
},
@@ -456,10 +456,10 @@ describe("runMessageAction plugin dispatch", () => {
cfg: {
tools: { allow: ["read"] },
channels: {
feishu: {
policydest: {
enabled: true,
},
whatsapp: {
requestchat: {
groups: {
ops: {
toolsBySender: {
@@ -474,13 +474,13 @@ describe("runMessageAction plugin dispatch", () => {
} as OpenClawConfig,
action: "send",
params: {
channel: "feishu",
channel: "policydest",
target: "oc_123",
message: "hello",
media: "/tmp/host.png",
},
requesterSenderId: "trusted-user",
sessionKey: "agent:alpha:whatsapp:group:ops",
sessionKey: "agent:alpha:requestchat:group:ops",
dryRun: false,
});
@@ -497,13 +497,13 @@ describe("runMessageAction plugin dispatch", () => {
}),
);
const policyPlugin: ChannelPlugin = {
id: "feishu",
id: "policydest",
meta: {
id: "feishu",
label: "Feishu",
selectionLabel: "Feishu",
docsPath: "/channels/feishu",
blurb: "Feishu username policy test plugin.",
id: "policydest",
label: "Policy Destination",
selectionLabel: "Policy Destination",
docsPath: "/channels/policydest",
blurb: "Policy destination username test plugin.",
},
capabilities: { chatTypes: ["direct", "channel"], media: true },
config: createAlwaysConfiguredPluginConfig(),
@@ -522,7 +522,7 @@ describe("runMessageAction plugin dispatch", () => {
setActivePluginRegistry(
createTestRegistry([
{
pluginId: "feishu",
pluginId: "policydest",
source: "test",
plugin: policyPlugin,
},
@@ -533,10 +533,10 @@ describe("runMessageAction plugin dispatch", () => {
cfg: {
tools: { allow: ["read"] },
channels: {
feishu: {
policydest: {
enabled: true,
},
whatsapp: {
requestchat: {
groups: {
ops: {
toolsBySender: {
@@ -551,13 +551,13 @@ describe("runMessageAction plugin dispatch", () => {
} as OpenClawConfig,
action: "send",
params: {
channel: "feishu",
channel: "policydest",
target: "oc_123",
message: "hello",
media: "/tmp/host.png",
},
requesterSenderUsername: "alice_u",
sessionKey: "agent:alpha:whatsapp:group:ops",
sessionKey: "agent:alpha:requestchat:group:ops",
dryRun: false,
});
@@ -574,13 +574,13 @@ describe("runMessageAction plugin dispatch", () => {
}),
);
const policyPlugin: ChannelPlugin = {
id: "feishu",
id: "policydest",
meta: {
id: "feishu",
label: "Feishu",
selectionLabel: "Feishu",
docsPath: "/channels/feishu",
blurb: "Feishu account policy test plugin.",
id: "policydest",
label: "Policy Destination",
selectionLabel: "Policy Destination",
docsPath: "/channels/policydest",
blurb: "Policy destination account test plugin.",
},
capabilities: { chatTypes: ["direct", "channel"], media: true },
config: createAlwaysConfiguredPluginConfig(),
@@ -599,7 +599,7 @@ describe("runMessageAction plugin dispatch", () => {
setActivePluginRegistry(
createTestRegistry([
{
pluginId: "feishu",
pluginId: "policydest",
source: "test",
plugin: policyPlugin,
},
@@ -610,10 +610,10 @@ describe("runMessageAction plugin dispatch", () => {
cfg: {
tools: { allow: ["read"] },
channels: {
feishu: {
policydest: {
enabled: true,
},
whatsapp: {
requestchat: {
accounts: {
source: {
groups: {
@@ -643,7 +643,7 @@ describe("runMessageAction plugin dispatch", () => {
} as OpenClawConfig,
action: "send",
params: {
channel: "feishu",
channel: "policydest",
accountId: "destination",
target: "oc_123",
message: "hello",
@@ -651,7 +651,7 @@ describe("runMessageAction plugin dispatch", () => {
},
requesterAccountId: "source",
requesterSenderId: "trusted-user",
sessionKey: "agent:alpha:whatsapp:group:ops",
sessionKey: "agent:alpha:requestchat:group:ops",
dryRun: false,
});
@@ -669,13 +669,13 @@ describe("runMessageAction plugin dispatch", () => {
}),
);
const policyPlugin: ChannelPlugin = {
id: "whatsapp",
id: "policychat",
meta: {
id: "whatsapp",
label: "WhatsApp",
selectionLabel: "WhatsApp",
docsPath: "/channels/whatsapp",
blurb: "WhatsApp account policy fallback test plugin.",
id: "policychat",
label: "Policy Chat",
selectionLabel: "Policy Chat",
docsPath: "/channels/policychat",
blurb: "Policy chat account fallback test plugin.",
},
capabilities: { chatTypes: ["direct", "channel"], media: true },
config: createAlwaysConfiguredPluginConfig(),
@@ -694,7 +694,7 @@ describe("runMessageAction plugin dispatch", () => {
setActivePluginRegistry(
createTestRegistry([
{
pluginId: "whatsapp",
pluginId: "policychat",
source: "test",
plugin: policyPlugin,
},
@@ -705,7 +705,7 @@ describe("runMessageAction plugin dispatch", () => {
cfg: {
tools: { allow: ["read"] },
channels: {
whatsapp: {
policychat: {
enabled: true,
accounts: {
source: {
@@ -725,14 +725,14 @@ describe("runMessageAction plugin dispatch", () => {
} as OpenClawConfig,
action: "send",
params: {
channel: "whatsapp",
channel: "policychat",
accountId: "source",
target: "group:ops",
message: "hello",
media: "/tmp/host.png",
},
requesterSenderId: "trusted-user",
sessionKey: "agent:alpha:whatsapp:group:ops",
sessionKey: "agent:alpha:policychat:group:ops",
dryRun: false,
});
@@ -824,7 +824,7 @@ describe("runMessageAction plugin dispatch", () => {
});
});
describe("telegram plugin poll forwarding", () => {
describe("poll plugin forwarding", () => {
const handleAction = vi.fn(async ({ params }: { params: Record<string, unknown> }) =>
jsonResult({
ok: true,
@@ -839,10 +839,10 @@ describe("runMessageAction plugin dispatch", () => {
}),
);
const telegramPollPlugin = createPollForwardingPlugin({
pluginId: "telegram",
label: "Telegram",
blurb: "Telegram poll forwarding test plugin.",
const pollChatPlugin = createPollForwardingPlugin({
pluginId: "pollchat",
label: "Poll Chat",
blurb: "Poll chat forwarding test plugin.",
handleAction,
});
@@ -850,9 +850,9 @@ describe("runMessageAction plugin dispatch", () => {
setActivePluginRegistry(
createTestRegistry([
{
pluginId: "telegram",
pluginId: "pollchat",
source: "test",
plugin: telegramPollPlugin,
plugin: pollChatPlugin,
},
]),
);
@@ -864,19 +864,19 @@ describe("runMessageAction plugin dispatch", () => {
vi.clearAllMocks();
});
it("forwards telegram poll params through plugin dispatch", async () => {
it("forwards poll params through plugin dispatch", async () => {
const result = await runMessageAction({
cfg: {
channels: {
telegram: {
pollchat: {
botToken: "tok",
},
},
} as OpenClawConfig,
action: "poll",
params: {
channel: "telegram",
target: "telegram:123",
channel: "pollchat",
target: "pollchat:123",
pollQuestion: "Lunch?",
pollOption: ["Pizza", "Sushi"],
pollDurationSeconds: 120,
@@ -891,9 +891,9 @@ describe("runMessageAction plugin dispatch", () => {
expect(handleAction).toHaveBeenCalledWith(
expect.objectContaining({
action: "poll",
channel: "telegram",
channel: "pollchat",
params: expect.objectContaining({
to: "telegram:123",
to: "pollchat:123",
pollQuestion: "Lunch?",
pollOption: ["Pizza", "Sushi"],
pollDurationSeconds: 120,
@@ -905,7 +905,7 @@ describe("runMessageAction plugin dispatch", () => {
expect(result.payload).toMatchObject({
ok: true,
forwarded: {
to: "telegram:123",
to: "pollchat:123",
pollQuestion: "Lunch?",
pollOption: ["Pizza", "Sushi"],
pollDurationSeconds: 120,
@@ -930,10 +930,10 @@ describe("runMessageAction plugin dispatch", () => {
}),
);
const discordPollPlugin = createPollForwardingPlugin({
pluginId: "discord",
label: "Discord",
blurb: "Discord plugin-owned poll test plugin.",
const guildPollPlugin = createPollForwardingPlugin({
pluginId: "guildchat",
label: "Guild Chat",
blurb: "Guild chat plugin-owned poll test plugin.",
handleAction,
});
@@ -941,9 +941,9 @@ describe("runMessageAction plugin dispatch", () => {
setActivePluginRegistry(
createTestRegistry([
{
pluginId: "discord",
pluginId: "guildchat",
source: "test",
plugin: discordPollPlugin,
plugin: guildPollPlugin,
},
]),
);
@@ -955,18 +955,18 @@ describe("runMessageAction plugin dispatch", () => {
vi.clearAllMocks();
});
it("lets non-telegram plugins own extra poll fields", async () => {
it("lets other plugins own extra poll fields", async () => {
const result = await runMessageAction({
cfg: {
channels: {
discord: {
guildchat: {
token: "tok",
},
},
} as OpenClawConfig,
action: "poll",
params: {
channel: "discord",
channel: "guildchat",
target: "channel:123",
pollQuestion: "Lunch?",
pollOption: ["Pizza", "Sushi"],
@@ -981,7 +981,7 @@ describe("runMessageAction plugin dispatch", () => {
expect(handleAction).toHaveBeenCalledWith(
expect.objectContaining({
action: "poll",
channel: "discord",
channel: "guildchat",
params: expect.objectContaining({
to: "channel:123",
pollQuestion: "Lunch?",
@@ -1003,13 +1003,13 @@ describe("runMessageAction plugin dispatch", () => {
);
const componentsPlugin: ChannelPlugin = {
id: "discord",
id: "componentchat",
meta: {
id: "discord",
label: "Discord",
selectionLabel: "Discord",
docsPath: "/channels/discord",
blurb: "Discord components send test plugin.",
id: "componentchat",
label: "Component Chat",
selectionLabel: "Component Chat",
docsPath: "/channels/componentchat",
blurb: "Component chat send test plugin.",
},
capabilities: { chatTypes: ["direct"] },
config: createAlwaysConfiguredPluginConfig({}),
@@ -1024,7 +1024,7 @@ describe("runMessageAction plugin dispatch", () => {
setActivePluginRegistry(
createTestRegistry([
{
pluginId: "discord",
pluginId: "componentchat",
source: "test",
plugin: componentsPlugin,
},
@@ -1047,7 +1047,7 @@ describe("runMessageAction plugin dispatch", () => {
cfg: {} as OpenClawConfig,
action: "send",
params: {
channel: "discord",
channel: "componentchat",
target: "channel:123",
message: "hi",
components: JSON.stringify(components),
@@ -1066,7 +1066,7 @@ describe("runMessageAction plugin dispatch", () => {
cfg: {} as OpenClawConfig,
action: "send",
params: {
channel: "discord",
channel: "componentchat",
target: "channel:123",
message: "hi",
components: "{not-json}",
@@ -1082,13 +1082,13 @@ describe("runMessageAction plugin dispatch", () => {
describe("accountId defaults", () => {
const handleAction = vi.fn(async () => jsonResult({ ok: true }));
const accountPlugin: ChannelPlugin = {
id: "discord",
id: "accountchat",
meta: {
id: "discord",
label: "Discord",
selectionLabel: "Discord",
docsPath: "/channels/discord",
blurb: "Discord test plugin.",
id: "accountchat",
label: "Account Chat",
selectionLabel: "Account Chat",
docsPath: "/channels/accountchat",
blurb: "Account chat test plugin.",
},
capabilities: { chatTypes: ["direct"] },
config: {
@@ -1105,7 +1105,7 @@ describe("runMessageAction plugin dispatch", () => {
setActivePluginRegistry(
createTestRegistry([
{
pluginId: "discord",
pluginId: "accountchat",
source: "test",
plugin: accountPlugin,
},
@@ -1133,7 +1133,7 @@ describe("runMessageAction plugin dispatch", () => {
args: {
cfg: {
bindings: [
{ agentId: "agent-b", match: { channel: "discord", accountId: "account-b" } },
{ agentId: "agent-b", match: { channel: "accountchat", accountId: "account-b" } },
],
} as OpenClawConfig,
agentId: "agent-b",
@@ -1145,7 +1145,7 @@ describe("runMessageAction plugin dispatch", () => {
...args,
action: "send",
params: {
channel: "discord",
channel: "accountchat",
target: "channel:123",
message: "hi",
},

View File

@@ -79,15 +79,14 @@ function buildThreadedChannelRoute(params: {
};
}
function parseTelegramTargetForTest(raw: string): {
function parseForumTargetForTest(raw: string): {
chatId: string;
messageThreadId?: number;
chatType: "direct" | "group" | "unknown";
} {
const trimmed = raw
.trim()
.replace(/^telegram:/i, "")
.replace(/^tg:/i, "")
.replace(/^forum:/i, "")
.replace(/^group:/i, "");
const prefixedTopic = /^([^:]+):topic:(\d+)$/i.exec(trimmed);
if (prefixedTopic) {
@@ -104,7 +103,7 @@ function parseTelegramTargetForTest(raw: string): {
};
}
function parseTelegramThreadIdForTest(threadId?: string | number | null): number | undefined {
function parseForumThreadIdForTest(threadId?: string | number | null): number | undefined {
const normalized = normalizeOutboundThreadId(threadId);
if (!normalized) {
return undefined;
@@ -116,26 +115,24 @@ function parseTelegramThreadIdForTest(threadId?: string | number | null): number
return Number.parseInt(topicMatch[1], 10);
}
function buildTelegramGroupPeerIdForTest(chatId: string, messageThreadId?: number): string {
function buildForumGroupPeerIdForTest(chatId: string, messageThreadId?: number): string {
return messageThreadId ? `${chatId}:topic:${messageThreadId}` : chatId;
}
function resolveTelegramOutboundSessionRouteForTest(params: ChannelOutboundSessionRouteParams) {
const parsed = parseTelegramTargetForTest(params.target);
function resolveForumOutboundSessionRouteForTest(params: ChannelOutboundSessionRouteParams) {
const parsed = parseForumTargetForTest(params.target);
const chatId = parsed.chatId.trim();
if (!chatId) {
return null;
}
const resolvedThreadId = parsed.messageThreadId ?? parseTelegramThreadIdForTest(params.threadId);
const resolvedThreadId = parsed.messageThreadId ?? parseForumThreadIdForTest(params.threadId);
const isGroup =
parsed.chatType === "group" ||
(parsed.chatType === "unknown" &&
params.resolvedTarget?.kind !== undefined &&
params.resolvedTarget.kind !== "user");
const peerId =
isGroup && resolvedThreadId
? buildTelegramGroupPeerIdForTest(chatId, resolvedThreadId)
: chatId;
isGroup && resolvedThreadId ? buildForumGroupPeerIdForTest(chatId, resolvedThreadId) : chatId;
const peer: RoutePeer = {
kind: isGroup ? "group" : "direct",
id: peerId,
@@ -144,67 +141,71 @@ function resolveTelegramOutboundSessionRouteForTest(params: ChannelOutboundSessi
return buildChannelOutboundSessionRoute({
cfg: params.cfg,
agentId: params.agentId,
channel: "telegram",
channel: "forum",
accountId: params.accountId,
peer,
chatType: "group",
from: `telegram:group:${peerId}`,
to: `telegram:${chatId}`,
from: `forum:group:${peerId}`,
to: `forum:${chatId}`,
...(resolvedThreadId !== undefined ? { threadId: resolvedThreadId } : {}),
});
}
return buildThreadedChannelRoute({
cfg: params.cfg,
agentId: params.agentId,
channel: "telegram",
channel: "forum",
accountId: params.accountId,
peer,
chatType: "direct",
from:
resolvedThreadId !== undefined
? `telegram:${chatId}:topic:${resolvedThreadId}`
: `telegram:${chatId}`,
to: `telegram:${chatId}`,
? `forum:${chatId}:topic:${resolvedThreadId}`
: `forum:${chatId}`,
to: `forum:${chatId}`,
threadId: resolvedThreadId,
});
}
function resolveSlackOutboundSessionRouteForTest(params: ChannelOutboundSessionRouteParams) {
function resolveWorkspaceOutboundSessionRouteForTest(params: ChannelOutboundSessionRouteParams) {
const trimmed = params.target.trim();
if (!trimmed) {
return null;
}
const lower = normalizeLowercaseStringOrEmpty(trimmed);
const rawId = stripTargetKindPrefix(stripChannelTargetPrefix(trimmed, "slack"));
const rawId = stripTargetKindPrefix(stripChannelTargetPrefix(trimmed, "workspace"));
if (!rawId) {
return null;
}
const normalizedId = normalizeLowercaseStringOrEmpty(rawId);
const isDm = lower.startsWith("user:") || lower.startsWith("slack:") || /^u/i.test(rawId);
const isDm = lower.startsWith("user:") || lower.startsWith("workspace:") || /^u/i.test(rawId);
const workspaceConfig = params.cfg.channels?.workspace as
| { dm?: { groupChannels?: unknown[] } }
| undefined;
const isGroupChannel =
/^g/i.test(rawId) &&
params.cfg.channels?.slack?.dm?.groupChannels?.some(
(candidate) => normalizeLowercaseStringOrEmpty(String(candidate)) === normalizedId,
) === true;
Array.isArray(workspaceConfig?.dm?.groupChannels) &&
workspaceConfig.dm.groupChannels.some(
(candidate: unknown) => normalizeLowercaseStringOrEmpty(String(candidate)) === normalizedId,
);
const peerKind: RoutePeer["kind"] = isDm ? "direct" : isGroupChannel ? "group" : "channel";
return buildThreadedChannelRoute({
cfg: params.cfg,
agentId: params.agentId,
channel: "slack",
channel: "workspace",
accountId: params.accountId,
peer: { kind: peerKind, id: normalizedId },
chatType: peerKind === "direct" ? "direct" : peerKind === "group" ? "group" : "channel",
from: isDm
? `slack:${rawId}`
? `workspace:${rawId}`
: isGroupChannel
? `slack:group:${rawId}`
: `slack:channel:${rawId}`,
? `workspace:group:${rawId}`
: `workspace:channel:${rawId}`,
to: isDm ? `user:${rawId}` : `channel:${rawId}`,
threadId: params.replyToId ?? params.threadId ?? undefined,
});
}
function resolveDiscordOutboundSessionRouteForTest(params: ChannelOutboundSessionRouteParams) {
function resolveGuildChatOutboundSessionRouteForTest(params: ChannelOutboundSessionRouteParams) {
const trimmed = params.target.trim();
if (!trimmed) {
return null;
@@ -215,16 +216,16 @@ function resolveDiscordOutboundSessionRouteForTest(params: ChannelOutboundSessio
kind = "user";
} else if (resolvedKind === "channel" || resolvedKind === "group") {
kind = "channel";
} else if (/^user:/i.test(trimmed) || /^discord:/i.test(trimmed) || /^<@!?/.test(trimmed)) {
} else if (/^user:/i.test(trimmed) || /^guildchat:/i.test(trimmed) || /^<@!?/.test(trimmed)) {
kind = "user";
} else if (/^channel:/i.test(trimmed)) {
kind = "channel";
} else if (/^\d+$/u.test(trimmed)) {
throw new Error("Ambiguous Discord recipient");
throw new Error("Ambiguous Guild Chat recipient");
} else {
kind = "channel";
}
const rawId = stripTargetKindPrefix(stripChannelTargetPrefix(trimmed, "discord"));
const rawId = stripTargetKindPrefix(stripChannelTargetPrefix(trimmed, "guildchat"));
if (!rawId) {
return null;
}
@@ -235,43 +236,43 @@ function resolveDiscordOutboundSessionRouteForTest(params: ChannelOutboundSessio
return buildThreadedChannelRoute({
cfg: params.cfg,
agentId: params.agentId,
channel: "discord",
channel: "guildchat",
accountId: params.accountId,
peer,
chatType: kind === "user" ? "direct" : "channel",
from: kind === "user" ? `discord:${rawId}` : `discord:channel:${rawId}`,
from: kind === "user" ? `guildchat:${rawId}` : `guildchat:channel:${rawId}`,
to: kind === "user" ? `user:${rawId}` : `channel:${rawId}`,
threadId: params.threadId ?? undefined,
useSuffix: false,
});
}
function resolveMattermostOutboundSessionRouteForTest(params: ChannelOutboundSessionRouteParams) {
function resolveBoardChatOutboundSessionRouteForTest(params: ChannelOutboundSessionRouteParams) {
const trimmed = params.target.trim();
if (!trimmed) {
return null;
}
const isUser = params.resolvedTarget?.kind === "user" || /^user:/i.test(trimmed);
const rawId = stripTargetKindPrefix(stripChannelTargetPrefix(trimmed, "mattermost"));
const rawId = stripTargetKindPrefix(stripChannelTargetPrefix(trimmed, "boardchat"));
if (!rawId) {
return null;
}
return buildThreadedChannelRoute({
cfg: params.cfg,
agentId: params.agentId,
channel: "mattermost",
channel: "boardchat",
accountId: params.accountId,
peer: { kind: isUser ? "direct" : "channel", id: rawId },
chatType: isUser ? "direct" : "channel",
from: isUser ? `mattermost:${rawId}` : `mattermost:channel:${rawId}`,
from: isUser ? `boardchat:${rawId}` : `boardchat:channel:${rawId}`,
to: isUser ? `user:${rawId}` : `channel:${rawId}`,
threadId: params.replyToId ?? params.threadId ?? undefined,
});
}
function resolveWhatsAppOutboundSessionRouteForTest(params: ChannelOutboundSessionRouteParams) {
function resolveMobileChatOutboundSessionRouteForTest(params: ChannelOutboundSessionRouteParams) {
const normalized = normalizeOptionalLowercaseString(
stripChannelTargetPrefix(params.target, "whatsapp"),
stripChannelTargetPrefix(params.target, "mobilechat"),
);
if (!normalized) {
return null;
@@ -280,7 +281,7 @@ function resolveWhatsAppOutboundSessionRouteForTest(params: ChannelOutboundSessi
return buildChannelOutboundSessionRoute({
cfg: params.cfg,
agentId: params.agentId,
channel: "whatsapp",
channel: "mobilechat",
accountId: params.accountId,
peer: { kind: isGroup ? "group" : "direct", id: normalized },
chatType: isGroup ? "group" : "direct",
@@ -309,8 +310,8 @@ function resolveMatrixOutboundSessionRouteForTest(params: ChannelOutboundSession
});
}
function resolveMSTeamsOutboundSessionRouteForTest(params: ChannelOutboundSessionRouteParams) {
const trimmed = stripChannelTargetPrefix(params.target, "msteams", "teams");
function resolveMeetingChatOutboundSessionRouteForTest(params: ChannelOutboundSessionRouteParams) {
const trimmed = stripChannelTargetPrefix(params.target, "meetingchat", "meet");
if (!trimmed) {
return null;
}
@@ -326,21 +327,21 @@ function resolveMSTeamsOutboundSessionRouteForTest(params: ChannelOutboundSessio
return buildChannelOutboundSessionRoute({
cfg: params.cfg,
agentId: params.agentId,
channel: "msteams",
channel: "meetingchat",
accountId: params.accountId,
peer: { kind: peerKind, id: conversationId },
chatType: peerKind,
from: isUser
? `msteams:${conversationId}`
? `meetingchat:${conversationId}`
: isChannel
? `msteams:channel:${conversationId}`
: `msteams:group:${conversationId}`,
? `meetingchat:channel:${conversationId}`
: `meetingchat:group:${conversationId}`,
to: isUser ? `user:${conversationId}` : `conversation:${conversationId}`,
});
}
function resolveFeishuOutboundSessionRouteForTest(params: ChannelOutboundSessionRouteParams) {
let trimmed = stripChannelTargetPrefix(params.target, "feishu", "lark");
function resolveCollabChatOutboundSessionRouteForTest(params: ChannelOutboundSessionRouteParams) {
let trimmed = stripChannelTargetPrefix(params.target, "collabchat", "collab");
if (!trimmed) {
return null;
}
@@ -360,11 +361,11 @@ function resolveFeishuOutboundSessionRouteForTest(params: ChannelOutboundSession
return buildChannelOutboundSessionRoute({
cfg: params.cfg,
agentId: params.agentId,
channel: "feishu",
channel: "collabchat",
accountId: params.accountId,
peer: { kind: isGroup ? "group" : "direct", id: trimmed },
chatType: isGroup ? "group" : "direct",
from: isGroup ? `feishu:group:${trimmed}` : `feishu:${trimmed}`,
from: isGroup ? `collabchat:group:${trimmed}` : `collabchat:${trimmed}`,
to: trimmed,
});
}
@@ -390,8 +391,8 @@ function resolveNextcloudTalkOutboundSessionRouteForTest(
});
}
function resolveBlueBubblesOutboundSessionRouteForTest(params: ChannelOutboundSessionRouteParams) {
const stripped = stripChannelTargetPrefix(params.target, "bluebubbles");
function resolveLocalChatOutboundSessionRouteForTest(params: ChannelOutboundSessionRouteParams) {
const stripped = stripChannelTargetPrefix(params.target, "localchat");
if (!stripped) {
return null;
}
@@ -405,12 +406,12 @@ function resolveBlueBubblesOutboundSessionRouteForTest(params: ChannelOutboundSe
return buildChannelOutboundSessionRoute({
cfg: params.cfg,
agentId: params.agentId,
channel: "bluebubbles",
channel: "localchat",
accountId: params.accountId,
peer: { kind: isGroup ? "group" : "direct", id: normalizedId },
chatType: isGroup ? "group" : "direct",
from: isGroup ? `group:${rawId}` : `bluebubbles:${rawId}`,
to: `bluebubbles:${stripped}`,
from: isGroup ? `group:${rawId}` : `localchat:${rawId}`,
to: `localchat:${stripped}`,
});
}
@@ -510,9 +511,9 @@ function resolveTlonOutboundSessionRouteForTest(params: ChannelOutboundSessionRo
export function setMinimalOutboundSessionPluginRegistryForTests(): void {
const plugins: ChannelPlugin[] = [
createSessionRouteTestPlugin({
id: "whatsapp",
label: "WhatsApp",
resolveOutboundSessionRoute: resolveWhatsAppOutboundSessionRouteForTest,
id: "mobilechat",
label: "Mobile Chat",
resolveOutboundSessionRoute: resolveMobileChatOutboundSessionRouteForTest,
}),
createSessionRouteTestPlugin({
id: "matrix",
@@ -520,24 +521,24 @@ export function setMinimalOutboundSessionPluginRegistryForTests(): void {
resolveOutboundSessionRoute: resolveMatrixOutboundSessionRouteForTest,
}),
createSessionRouteTestPlugin({
id: "msteams",
label: "Microsoft Teams",
resolveOutboundSessionRoute: resolveMSTeamsOutboundSessionRouteForTest,
id: "meetingchat",
label: "Meeting Chat",
resolveOutboundSessionRoute: resolveMeetingChatOutboundSessionRouteForTest,
}),
createSessionRouteTestPlugin({
id: "slack",
label: "Slack",
resolveOutboundSessionRoute: resolveSlackOutboundSessionRouteForTest,
id: "workspace",
label: "Workspace",
resolveOutboundSessionRoute: resolveWorkspaceOutboundSessionRouteForTest,
}),
createSessionRouteTestPlugin({
id: "telegram",
label: "Telegram",
resolveOutboundSessionRoute: resolveTelegramOutboundSessionRouteForTest,
id: "forum",
label: "Forum",
resolveOutboundSessionRoute: resolveForumOutboundSessionRouteForTest,
}),
createSessionRouteTestPlugin({
id: "discord",
label: "Discord",
resolveOutboundSessionRoute: resolveDiscordOutboundSessionRouteForTest,
id: "guildchat",
label: "Guild Chat",
resolveOutboundSessionRoute: resolveGuildChatOutboundSessionRouteForTest,
}),
createSessionRouteTestPlugin({
id: "nextcloud-talk",
@@ -545,9 +546,9 @@ export function setMinimalOutboundSessionPluginRegistryForTests(): void {
resolveOutboundSessionRoute: resolveNextcloudTalkOutboundSessionRouteForTest,
}),
createSessionRouteTestPlugin({
id: "bluebubbles",
label: "BlueBubbles",
resolveOutboundSessionRoute: resolveBlueBubblesOutboundSessionRouteForTest,
id: "localchat",
label: "Local Chat",
resolveOutboundSessionRoute: resolveLocalChatOutboundSessionRouteForTest,
}),
createSessionRouteTestPlugin({
id: "zalo",
@@ -570,14 +571,14 @@ export function setMinimalOutboundSessionPluginRegistryForTests(): void {
resolveOutboundSessionRoute: resolveTlonOutboundSessionRouteForTest,
}),
createSessionRouteTestPlugin({
id: "feishu",
label: "Feishu",
resolveOutboundSessionRoute: resolveFeishuOutboundSessionRouteForTest,
id: "collabchat",
label: "Collab Chat",
resolveOutboundSessionRoute: resolveCollabChatOutboundSessionRouteForTest,
}),
createSessionRouteTestPlugin({
id: "mattermost",
label: "Mattermost",
resolveOutboundSessionRoute: resolveMattermostOutboundSessionRouteForTest,
id: "boardchat",
label: "Board Chat",
resolveOutboundSessionRoute: resolveBoardChatOutboundSessionRouteForTest,
}),
];
setActivePluginRegistry(

View File

@@ -28,13 +28,13 @@ describe("resolveOutboundSessionRoute", () => {
session: {
dmScope: "per-peer",
identityLinks: {
alice: ["discord:123"],
alice: ["guildchat:123"],
},
},
} as OpenClawConfig;
const slackMpimCfg = {
const workspaceMpimCfg = {
channels: {
slack: {
workspace: {
dm: {
groupChannels: ["G123"],
},
@@ -86,12 +86,12 @@ describe("resolveOutboundSessionRoute", () => {
it.each([
{
name: "WhatsApp group jid",
name: "MobileChat group jid",
cfg: baseConfig,
channel: "whatsapp",
channel: "mobilechat",
target: "120363040000000000@g.us",
expected: {
sessionKey: "agent:main:whatsapp:group:120363040000000000@g.us",
sessionKey: "agent:main:mobilechat:group:120363040000000000@g.us",
from: "120363040000000000@g.us",
to: "120363040000000000@g.us",
chatType: "group",
@@ -110,75 +110,75 @@ describe("resolveOutboundSessionRoute", () => {
},
},
{
name: "MSTeams conversation target",
name: "MeetingChat conversation target",
cfg: baseConfig,
channel: "msteams",
channel: "meetingchat",
target: "conversation:19:meeting_abc@thread.tacv2",
expected: {
sessionKey: "agent:main:msteams:channel:19:meeting_abc@thread.tacv2",
from: "msteams:channel:19:meeting_abc@thread.tacv2",
sessionKey: "agent:main:meetingchat:channel:19:meeting_abc@thread.tacv2",
from: "meetingchat:channel:19:meeting_abc@thread.tacv2",
to: "conversation:19:meeting_abc@thread.tacv2",
chatType: "channel",
},
},
{
name: "Slack thread",
name: "Workspace thread",
cfg: baseConfig,
channel: "slack",
channel: "workspace",
target: "channel:C123",
replyToId: "456",
expected: {
sessionKey: "agent:main:slack:channel:c123:thread:456",
from: "slack:channel:C123",
sessionKey: "agent:main:workspace:channel:c123:thread:456",
from: "workspace:channel:C123",
to: "channel:C123",
threadId: "456",
},
},
{
name: "Telegram topic group",
name: "Forum topic group",
cfg: baseConfig,
channel: "telegram",
channel: "forum",
target: "-100123456:topic:42",
expected: {
sessionKey: "agent:main:telegram:group:-100123456:topic:42",
from: "telegram:group:-100123456:topic:42",
to: "telegram:-100123456",
sessionKey: "agent:main:forum:group:-100123456:topic:42",
from: "forum:group:-100123456:topic:42",
to: "forum:-100123456",
threadId: 42,
},
},
{
name: "Telegram DM with topic",
name: "Forum DM with topic",
cfg: perChannelPeerCfg,
channel: "telegram",
channel: "forum",
target: "123456789:topic:99",
expected: {
sessionKey: "agent:main:telegram:direct:123456789:thread:99",
from: "telegram:123456789:topic:99",
to: "telegram:123456789",
sessionKey: "agent:main:forum:direct:123456789:thread:99",
from: "forum:123456789:topic:99",
to: "forum:123456789",
threadId: 99,
chatType: "direct",
},
},
{
name: "Telegram unresolved username DM",
name: "Forum unresolved username DM",
cfg: perChannelPeerCfg,
channel: "telegram",
channel: "forum",
target: "@alice",
expected: {
sessionKey: "agent:main:telegram:direct:@alice",
sessionKey: "agent:main:forum:direct:@alice",
chatType: "direct",
},
},
{
name: "Telegram DM scoped threadId fallback",
name: "Forum DM scoped threadId fallback",
cfg: perChannelPeerCfg,
channel: "telegram",
channel: "forum",
target: "12345",
threadId: "12345:99",
expected: {
sessionKey: "agent:main:telegram:direct:12345:thread:99",
from: "telegram:12345:topic:99",
to: "telegram:12345",
sessionKey: "agent:main:forum:direct:12345:thread:99",
from: "forum:12345:topic:99",
to: "forum:12345",
threadId: 99,
chatType: "direct",
},
@@ -186,7 +186,7 @@ describe("resolveOutboundSessionRoute", () => {
{
name: "identity-links per-peer",
cfg: identityLinksCfg,
channel: "discord",
channel: "guildchat",
target: "user:123",
expected: {
sessionKey: "agent:main:direct:alice",
@@ -205,12 +205,12 @@ describe("resolveOutboundSessionRoute", () => {
},
},
{
name: "BlueBubbles chat_* prefix stripping",
name: "LocalChat chat_* prefix stripping",
cfg: baseConfig,
channel: "bluebubbles",
channel: "localchat",
target: "chat_guid:ABC123",
expected: {
sessionKey: "agent:main:bluebubbles:group:abc123",
sessionKey: "agent:main:localchat:group:abc123",
from: "group:ABC123",
},
},
@@ -261,71 +261,71 @@ describe("resolveOutboundSessionRoute", () => {
},
},
{
name: "Slack mpim allowlist -> group key",
cfg: slackMpimCfg,
channel: "slack",
name: "Workspace group allowlist -> group key",
cfg: workspaceMpimCfg,
channel: "workspace",
target: "channel:G123",
expected: {
sessionKey: "agent:main:slack:group:g123",
from: "slack:group:G123",
sessionKey: "agent:main:workspace:group:g123",
from: "workspace:group:G123",
},
},
{
name: "Feishu explicit group prefix keeps group routing",
name: "CollabChat explicit group prefix keeps group routing",
cfg: baseConfig,
channel: "feishu",
channel: "collabchat",
target: "group:oc_group_chat",
expected: {
sessionKey: "agent:main:feishu:group:oc_group_chat",
from: "feishu:group:oc_group_chat",
sessionKey: "agent:main:collabchat:group:oc_group_chat",
from: "collabchat:group:oc_group_chat",
to: "oc_group_chat",
chatType: "group",
},
},
{
name: "Feishu explicit dm prefix keeps direct routing",
name: "CollabChat explicit dm prefix keeps direct routing",
cfg: perChannelPeerCfg,
channel: "feishu",
channel: "collabchat",
target: "dm:oc_dm_chat",
expected: {
sessionKey: "agent:main:feishu:direct:oc_dm_chat",
from: "feishu:oc_dm_chat",
sessionKey: "agent:main:collabchat:direct:oc_dm_chat",
from: "collabchat:oc_dm_chat",
to: "oc_dm_chat",
chatType: "direct",
},
},
{
name: "Feishu bare oc_ target defaults to direct routing",
name: "CollabChat bare oc_ target defaults to direct routing",
cfg: perChannelPeerCfg,
channel: "feishu",
channel: "collabchat",
target: "oc_ambiguous_chat",
expected: {
sessionKey: "agent:main:feishu:direct:oc_ambiguous_chat",
from: "feishu:oc_ambiguous_chat",
sessionKey: "agent:main:collabchat:direct:oc_ambiguous_chat",
from: "collabchat:oc_ambiguous_chat",
to: "oc_ambiguous_chat",
chatType: "direct",
},
},
{
name: "Slack user DM target",
name: "Workspace user DM target",
cfg: perChannelPeerCfg,
channel: "slack",
channel: "workspace",
target: "user:U12345ABC",
expected: {
sessionKey: "agent:main:slack:direct:u12345abc",
from: "slack:U12345ABC",
sessionKey: "agent:main:workspace:direct:u12345abc",
from: "workspace:U12345ABC",
to: "user:U12345ABC",
chatType: "direct",
},
},
{
name: "Slack channel target without thread",
name: "Workspace channel target without thread",
cfg: baseConfig,
channel: "slack",
channel: "workspace",
target: "channel:C999XYZ",
expected: {
sessionKey: "agent:main:slack:channel:c999xyz",
from: "slack:channel:C999XYZ",
sessionKey: "agent:main:workspace:channel:c999xyz",
from: "workspace:channel:C999XYZ",
to: "channel:C999XYZ",
chatType: "channel",
},
@@ -336,7 +336,7 @@ describe("resolveOutboundSessionRoute", () => {
it.each([
{
name: "uses resolved Discord user targets to route bare numeric ids as DMs",
name: "uses resolved GuildChat user targets to route bare numeric ids as DMs",
target: "123",
resolvedTarget: {
to: "user:123",
@@ -344,14 +344,14 @@ describe("resolveOutboundSessionRoute", () => {
source: "directory" as const,
},
expected: {
sessionKey: "agent:main:discord:direct:123",
from: "discord:123",
sessionKey: "agent:main:guildchat:direct:123",
from: "guildchat:123",
to: "user:123",
chatType: "direct",
},
},
{
name: "uses resolved Discord channel targets to route bare numeric ids as channels without thread suffixes",
name: "uses resolved GuildChat channel targets to route bare numeric ids as channels without thread suffixes",
target: "456",
threadId: "789",
resolvedTarget: {
@@ -360,31 +360,31 @@ describe("resolveOutboundSessionRoute", () => {
source: "directory" as const,
},
expected: {
sessionKey: "agent:main:discord:channel:456",
baseSessionKey: "agent:main:discord:channel:456",
from: "discord:channel:456",
sessionKey: "agent:main:guildchat:channel:456",
baseSessionKey: "agent:main:guildchat:channel:456",
from: "guildchat:channel:456",
to: "channel:456",
chatType: "channel",
threadId: "789",
},
},
{
name: "uses resolved Mattermost user targets to route bare ids as DMs",
name: "uses resolved BoardChat user targets to route bare ids as DMs",
target: "dthcxgoxhifn3pwh65cut3ud3w",
channel: "mattermost",
channel: "boardchat",
resolvedTarget: {
to: "user:dthcxgoxhifn3pwh65cut3ud3w",
kind: "user" as const,
source: "directory" as const,
},
expected: {
sessionKey: "agent:main:mattermost:direct:dthcxgoxhifn3pwh65cut3ud3w",
from: "mattermost:dthcxgoxhifn3pwh65cut3ud3w",
sessionKey: "agent:main:boardchat:direct:dthcxgoxhifn3pwh65cut3ud3w",
from: "boardchat:dthcxgoxhifn3pwh65cut3ud3w",
to: "user:dthcxgoxhifn3pwh65cut3ud3w",
chatType: "direct",
},
},
])("$name", async ({ channel = "discord", target, threadId, resolvedTarget, expected }) => {
])("$name", async ({ channel = "guildchat", target, threadId, resolvedTarget, expected }) => {
const route = await resolveOutboundSessionRoute({
cfg: perChannelPeerSessionCfg,
channel,
@@ -397,15 +397,15 @@ describe("resolveOutboundSessionRoute", () => {
expect(route).toMatchObject(expected);
});
it("rejects bare numeric Discord targets when the caller has no kind hint", async () => {
it("rejects bare numeric GuildChat targets when the caller has no kind hint", async () => {
await expect(
resolveOutboundSessionRoute({
cfg: perChannelPeerSessionCfg,
channel: "discord",
channel: "guildchat",
agentId: "main",
target: "123",
}),
).rejects.toThrow(/Ambiguous Discord recipient/);
).rejects.toThrow(/Ambiguous Guild Chat recipient/);
});
});
@@ -422,13 +422,13 @@ describe("ensureOutboundSessionEntry", () => {
store: "/stores/{agentId}.json",
},
} as OpenClawConfig,
channel: "slack",
channel: "workspace",
route: {
sessionKey: "agent:main:slack:channel:c1",
baseSessionKey: "agent:work:slack:channel:resolved",
sessionKey: "agent:main:workspace:channel:c1",
baseSessionKey: "agent:work:workspace:channel:resolved",
peer: { kind: "channel", id: "c1" },
chatType: "channel",
from: "slack:channel:C1",
from: "workspace:channel:C1",
to: "channel:C1",
},
});
@@ -439,7 +439,7 @@ describe("ensureOutboundSessionEntry", () => {
expect(mocks.recordSessionMetaFromInbound).toHaveBeenCalledWith(
expect.objectContaining({
storePath: "/stores/main.json",
sessionKey: "agent:main:slack:channel:c1",
sessionKey: "agent:main:workspace:channel:c1",
}),
);
});

View File

@@ -135,7 +135,7 @@ describe("resolveMessagingTarget (directory fallback)", () => {
const result = await expectOkResolution({
cfg,
channel: "mattermost",
channel: "workspace",
input: "dthcxgoxhifn3pwh65cut3ud3w",
});
expect(result.target).toEqual({