test: trim plugin-heavy unit test imports
This commit is contained in:
parent
740b345a2e
commit
b26edfe1ff
@ -1,11 +1,11 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { discordPlugin } from "../../extensions/discord/src/channel.js";
|
||||
import { feishuPlugin } from "../../extensions/feishu/src/channel.js";
|
||||
import { telegramPlugin } from "../../extensions/telegram/src/channel.js";
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { parseFeishuConversationId } from "../../extensions/feishu/src/conversation-id.js";
|
||||
import { parseTelegramTopicConversation } from "../../extensions/telegram/runtime-api.js";
|
||||
import { resolveAgentWorkspaceDir } from "../agents/agent-scope.js";
|
||||
import type { ChannelConfiguredBindingProvider, ChannelPlugin } from "../channels/plugins/types.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { setActivePluginRegistry } from "../plugins/runtime.js";
|
||||
import { createTestRegistry } from "../test-utils/channel-plugins.js";
|
||||
import { createChannelTestPluginBase, createTestRegistry } from "../test-utils/channel-plugins.js";
|
||||
import * as persistentBindingsResolveModule from "./persistent-bindings.resolve.js";
|
||||
import { buildConfiguredAcpSessionKey } from "./persistent-bindings.types.js";
|
||||
const managerMocks = vi.hoisted(() => ({
|
||||
@ -39,6 +39,10 @@ type PersistentBindingsModule = Pick<
|
||||
"ensureConfiguredAcpBindingSession" | "resetAcpSessionInPlace"
|
||||
>;
|
||||
let persistentBindings: PersistentBindingsModule;
|
||||
let lifecycleBindingsModule: Pick<
|
||||
typeof import("./persistent-bindings.lifecycle.js"),
|
||||
"ensureConfiguredAcpBindingSession" | "resetAcpSessionInPlace"
|
||||
>;
|
||||
|
||||
type ConfiguredBinding = NonNullable<OpenClawConfig["bindings"]>[number];
|
||||
type BindingRecordInput = Parameters<
|
||||
@ -58,6 +62,131 @@ const baseCfg = {
|
||||
const defaultDiscordConversationId = "1478836151241412759";
|
||||
const defaultDiscordAccountId = "default";
|
||||
|
||||
const discordBindings: ChannelConfiguredBindingProvider = {
|
||||
compileConfiguredBinding: ({ conversationId }) => {
|
||||
const normalized = conversationId.trim();
|
||||
return normalized ? { conversationId: normalized } : null;
|
||||
},
|
||||
matchInboundConversation: ({ compiledBinding, conversationId, parentConversationId }) => {
|
||||
if (compiledBinding.conversationId === conversationId) {
|
||||
return { conversationId, matchPriority: 2 };
|
||||
}
|
||||
if (
|
||||
parentConversationId &&
|
||||
parentConversationId !== conversationId &&
|
||||
compiledBinding.conversationId === parentConversationId
|
||||
) {
|
||||
return { conversationId: parentConversationId, matchPriority: 1 };
|
||||
}
|
||||
return null;
|
||||
},
|
||||
};
|
||||
|
||||
const telegramBindings: ChannelConfiguredBindingProvider = {
|
||||
compileConfiguredBinding: ({ conversationId }) => {
|
||||
const parsed = parseTelegramTopicConversation({ conversationId });
|
||||
if (!parsed || !parsed.chatId.startsWith("-")) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
conversationId: parsed.canonicalConversationId,
|
||||
parentConversationId: parsed.chatId,
|
||||
};
|
||||
},
|
||||
matchInboundConversation: ({ compiledBinding, conversationId, parentConversationId }) => {
|
||||
const incoming = parseTelegramTopicConversation({
|
||||
conversationId,
|
||||
parentConversationId,
|
||||
});
|
||||
if (!incoming || !incoming.chatId.startsWith("-")) {
|
||||
return null;
|
||||
}
|
||||
if (compiledBinding.conversationId !== incoming.canonicalConversationId) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
conversationId: incoming.canonicalConversationId,
|
||||
parentConversationId: incoming.chatId,
|
||||
matchPriority: 2,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
function isSupportedFeishuDirectConversationId(conversationId: string): boolean {
|
||||
const trimmed = conversationId.trim();
|
||||
if (!trimmed || trimmed.includes(":")) {
|
||||
return false;
|
||||
}
|
||||
if (trimmed.startsWith("oc_") || trimmed.startsWith("on_")) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const feishuBindings: ChannelConfiguredBindingProvider = {
|
||||
compileConfiguredBinding: ({ conversationId }) => {
|
||||
const parsed = parseFeishuConversationId({ conversationId });
|
||||
if (
|
||||
!parsed ||
|
||||
(parsed.scope !== "group_topic" &&
|
||||
parsed.scope !== "group_topic_sender" &&
|
||||
!isSupportedFeishuDirectConversationId(parsed.canonicalConversationId))
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
conversationId: parsed.canonicalConversationId,
|
||||
parentConversationId:
|
||||
parsed.scope === "group_topic" || parsed.scope === "group_topic_sender"
|
||||
? parsed.chatId
|
||||
: undefined,
|
||||
};
|
||||
},
|
||||
matchInboundConversation: ({ compiledBinding, conversationId, parentConversationId }) => {
|
||||
const incoming = parseFeishuConversationId({
|
||||
conversationId,
|
||||
parentConversationId,
|
||||
});
|
||||
if (
|
||||
!incoming ||
|
||||
(incoming.scope !== "group_topic" &&
|
||||
incoming.scope !== "group_topic_sender" &&
|
||||
!isSupportedFeishuDirectConversationId(incoming.canonicalConversationId))
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const matchesCanonicalConversation =
|
||||
compiledBinding.conversationId === incoming.canonicalConversationId;
|
||||
const matchesParentTopicForSenderScopedConversation =
|
||||
incoming.scope === "group_topic_sender" &&
|
||||
compiledBinding.parentConversationId === incoming.chatId &&
|
||||
compiledBinding.conversationId === `${incoming.chatId}:topic:${incoming.topicId}`;
|
||||
if (!matchesCanonicalConversation && !matchesParentTopicForSenderScopedConversation) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
conversationId: matchesParentTopicForSenderScopedConversation
|
||||
? compiledBinding.conversationId
|
||||
: incoming.canonicalConversationId,
|
||||
parentConversationId:
|
||||
incoming.scope === "group_topic" || incoming.scope === "group_topic_sender"
|
||||
? incoming.chatId
|
||||
: undefined,
|
||||
matchPriority: matchesCanonicalConversation ? 2 : 1,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
function createConfiguredBindingTestPlugin(
|
||||
id: ChannelPlugin["id"],
|
||||
bindings: ChannelConfiguredBindingProvider,
|
||||
): Pick<ChannelPlugin, "id" | "meta" | "capabilities" | "config" | "bindings"> {
|
||||
return {
|
||||
...createChannelTestPluginBase({ id }),
|
||||
bindings,
|
||||
};
|
||||
}
|
||||
|
||||
function createCfgWithBindings(
|
||||
bindings: ConfiguredBinding[],
|
||||
overrides?: Partial<OpenClawConfig>,
|
||||
@ -185,20 +314,26 @@ beforeEach(() => {
|
||||
persistentBindingsResolveModule.resolveConfiguredAcpBindingRecord,
|
||||
resolveConfiguredAcpBindingSpecBySessionKey:
|
||||
persistentBindingsResolveModule.resolveConfiguredAcpBindingSpecBySessionKey,
|
||||
ensureConfiguredAcpBindingSession: async (...args) => {
|
||||
const lifecycleModule = await import("./persistent-bindings.lifecycle.js");
|
||||
return await lifecycleModule.ensureConfiguredAcpBindingSession(...args);
|
||||
},
|
||||
resetAcpSessionInPlace: async (...args) => {
|
||||
const lifecycleModule = await import("./persistent-bindings.lifecycle.js");
|
||||
return await lifecycleModule.resetAcpSessionInPlace(...args);
|
||||
},
|
||||
ensureConfiguredAcpBindingSession: lifecycleBindingsModule.ensureConfiguredAcpBindingSession,
|
||||
resetAcpSessionInPlace: lifecycleBindingsModule.resetAcpSessionInPlace,
|
||||
};
|
||||
setActivePluginRegistry(
|
||||
createTestRegistry([
|
||||
{ pluginId: "discord", plugin: discordPlugin, source: "test" },
|
||||
{ pluginId: "telegram", plugin: telegramPlugin, source: "test" },
|
||||
{ pluginId: "feishu", plugin: feishuPlugin, source: "test" },
|
||||
{
|
||||
pluginId: "discord",
|
||||
plugin: createConfiguredBindingTestPlugin("discord", discordBindings),
|
||||
source: "test",
|
||||
},
|
||||
{
|
||||
pluginId: "telegram",
|
||||
plugin: createConfiguredBindingTestPlugin("telegram", telegramBindings),
|
||||
source: "test",
|
||||
},
|
||||
{
|
||||
pluginId: "feishu",
|
||||
plugin: createConfiguredBindingTestPlugin("feishu", feishuBindings),
|
||||
source: "test",
|
||||
},
|
||||
]),
|
||||
);
|
||||
managerMocks.resolveSession.mockReset();
|
||||
@ -211,6 +346,10 @@ beforeEach(() => {
|
||||
sessionMetaMocks.readAcpSessionEntry.mockReset().mockReturnValue(undefined);
|
||||
});
|
||||
|
||||
beforeAll(async () => {
|
||||
lifecycleBindingsModule = await import("./persistent-bindings.lifecycle.js");
|
||||
});
|
||||
|
||||
describe("resolveConfiguredAcpBindingRecord", () => {
|
||||
it("resolves discord channel ACP binding from top-level typed bindings", () => {
|
||||
const cfg = createCfgWithBindings([
|
||||
|
||||
@ -1,6 +1,4 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { mattermostPlugin } from "../../../extensions/mattermost/src/channel.js";
|
||||
import { slackPlugin } from "../../../extensions/slack/src/channel.js";
|
||||
import {
|
||||
discordOutbound,
|
||||
imessageOutbound,
|
||||
@ -9,7 +7,12 @@ import {
|
||||
telegramOutbound,
|
||||
whatsappOutbound,
|
||||
} from "../../../test/channel-outbounds.js";
|
||||
import type { ChannelOutboundAdapter, ChannelPlugin } from "../../channels/plugins/types.js";
|
||||
import type {
|
||||
ChannelMessagingAdapter,
|
||||
ChannelOutboundAdapter,
|
||||
ChannelPlugin,
|
||||
ChannelThreadingAdapter,
|
||||
} from "../../channels/plugins/types.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import type { PluginRegistry } from "../../plugins/registry.js";
|
||||
import { setActivePluginRegistry } from "../../plugins/runtime.js";
|
||||
@ -28,13 +31,22 @@ const mocks = vi.hoisted(() => ({
|
||||
sendMessageSlack: vi.fn(async () => ({ messageId: "m1", channelId: "c1" })),
|
||||
sendMessageTelegram: vi.fn(async () => ({ messageId: "m1", chatId: "c1" })),
|
||||
sendMessageWhatsApp: vi.fn(async () => ({ messageId: "m1", toJid: "jid" })),
|
||||
sendMessageMattermost: vi.fn(async () => ({ messageId: "m1", channelId: "c1" })),
|
||||
sendMessageMattermost: vi.fn(async (..._args: unknown[]) => ({
|
||||
messageId: "m1",
|
||||
channelId: "c1",
|
||||
})),
|
||||
deliverOutboundPayloads: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../../../extensions/discord/src/send.js", () => ({
|
||||
sendMessageDiscord: mocks.sendMessageDiscord,
|
||||
}));
|
||||
vi.mock("../../../extensions/discord/src/send.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../../../extensions/discord/src/send.js")>();
|
||||
return {
|
||||
...actual,
|
||||
sendMessageDiscord: mocks.sendMessageDiscord,
|
||||
sendPollDiscord: mocks.sendMessageDiscord,
|
||||
sendWebhookMessageDiscord: vi.fn(),
|
||||
};
|
||||
});
|
||||
vi.mock("../../../extensions/imessage/src/send.js", () => ({
|
||||
sendMessageIMessage: mocks.sendMessageIMessage,
|
||||
}));
|
||||
@ -44,21 +56,17 @@ vi.mock("../../../extensions/signal/src/send.js", () => ({
|
||||
vi.mock("../../../extensions/slack/src/send.js", () => ({
|
||||
sendMessageSlack: mocks.sendMessageSlack,
|
||||
}));
|
||||
vi.mock("../../../extensions/telegram/src/send.js", () => ({
|
||||
sendMessageTelegram: mocks.sendMessageTelegram,
|
||||
}));
|
||||
vi.mock("../../../extensions/telegram/src/send.js", () => ({
|
||||
sendMessageTelegram: mocks.sendMessageTelegram,
|
||||
}));
|
||||
vi.mock("../../../extensions/telegram/src/send.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../../../extensions/telegram/src/send.js")>();
|
||||
return {
|
||||
...actual,
|
||||
sendMessageTelegram: mocks.sendMessageTelegram,
|
||||
};
|
||||
});
|
||||
vi.mock("../../../extensions/whatsapp/src/send.js", () => ({
|
||||
sendMessageWhatsApp: mocks.sendMessageWhatsApp,
|
||||
sendPollWhatsApp: mocks.sendMessageWhatsApp,
|
||||
}));
|
||||
vi.mock("../../../extensions/discord/src/send.js", () => ({
|
||||
sendMessageDiscord: mocks.sendMessageDiscord,
|
||||
sendPollDiscord: mocks.sendMessageDiscord,
|
||||
sendWebhookMessageDiscord: vi.fn(),
|
||||
}));
|
||||
vi.mock("../../../extensions/mattermost/src/mattermost/send.js", () => ({
|
||||
sendMessageMattermost: mocks.sendMessageMattermost,
|
||||
}));
|
||||
@ -132,6 +140,47 @@ const createMSTeamsPlugin = (params: { outbound: ChannelOutboundAdapter }): Chan
|
||||
outbound: params.outbound,
|
||||
});
|
||||
|
||||
const slackMessaging: ChannelMessagingAdapter = {
|
||||
enableInteractiveReplies: ({ cfg }) =>
|
||||
(cfg.channels?.slack as { capabilities?: { interactiveReplies?: boolean } } | undefined)
|
||||
?.capabilities?.interactiveReplies === true,
|
||||
hasStructuredReplyPayload: ({ payload }) => {
|
||||
const blocks = (payload.channelData?.slack as { blocks?: unknown } | undefined)?.blocks;
|
||||
if (typeof blocks === "string") {
|
||||
return blocks.trim().length > 0;
|
||||
}
|
||||
return Array.isArray(blocks) && blocks.length > 0;
|
||||
},
|
||||
};
|
||||
|
||||
const slackThreading: ChannelThreadingAdapter = {
|
||||
resolveReplyTransport: ({ threadId, replyToId }) => ({
|
||||
replyToId: replyToId ?? (threadId != null && threadId !== "" ? String(threadId) : undefined),
|
||||
threadId: null,
|
||||
}),
|
||||
};
|
||||
|
||||
const mattermostOutbound: ChannelOutboundAdapter = {
|
||||
deliveryMode: "direct",
|
||||
sendText: async ({ to, text, cfg, accountId, replyToId, threadId }) => {
|
||||
const result = await mocks.sendMessageMattermost(to, text, {
|
||||
cfg,
|
||||
accountId: accountId ?? undefined,
|
||||
replyToId: replyToId ?? (threadId != null ? String(threadId) : undefined),
|
||||
});
|
||||
return { channel: "mattermost", ...result };
|
||||
},
|
||||
sendMedia: async ({ to, text, cfg, accountId, replyToId, threadId, mediaUrl }) => {
|
||||
const result = await mocks.sendMessageMattermost(to, text, {
|
||||
cfg,
|
||||
accountId: accountId ?? undefined,
|
||||
replyToId: replyToId ?? (threadId != null ? String(threadId) : undefined),
|
||||
mediaUrl,
|
||||
});
|
||||
return { channel: "mattermost", ...result };
|
||||
},
|
||||
};
|
||||
|
||||
async function expectSlackNoSend(
|
||||
payload: Parameters<typeof routeReply>[0]["payload"],
|
||||
overrides: Partial<Parameters<typeof routeReply>[0]> = {},
|
||||
@ -553,8 +602,8 @@ const defaultRegistry = createTestRegistry([
|
||||
pluginId: "slack",
|
||||
plugin: {
|
||||
...createOutboundTestPlugin({ id: "slack", outbound: slackOutbound, label: "Slack" }),
|
||||
messaging: slackPlugin.messaging,
|
||||
threading: slackPlugin.threading,
|
||||
messaging: slackMessaging,
|
||||
threading: slackThreading,
|
||||
},
|
||||
source: "test",
|
||||
},
|
||||
@ -595,7 +644,11 @@ const defaultRegistry = createTestRegistry([
|
||||
},
|
||||
{
|
||||
pluginId: "mattermost",
|
||||
plugin: mattermostPlugin,
|
||||
plugin: createOutboundTestPlugin({
|
||||
id: "mattermost",
|
||||
outbound: mattermostOutbound,
|
||||
label: "Mattermost",
|
||||
}),
|
||||
source: "test",
|
||||
},
|
||||
]);
|
||||
|
||||
@ -1,15 +1,6 @@
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { telegramPlugin } from "../../../extensions/telegram/src/channel.js";
|
||||
import { setActivePluginRegistry } from "../../plugins/runtime.js";
|
||||
import { createTestRegistry } from "../../test-utils/channel-plugins.js";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveTelegramConversationId } from "./telegram-context.js";
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePluginRegistry(
|
||||
createTestRegistry([{ pluginId: "telegram", source: "test", plugin: telegramPlugin }]),
|
||||
);
|
||||
});
|
||||
|
||||
describe("resolveTelegramConversationId", () => {
|
||||
it("builds canonical topic ids from chat target and message thread id", () => {
|
||||
const conversationId = resolveTelegramConversationId({
|
||||
|
||||
@ -29,13 +29,104 @@ vi.mock("../../../extensions/discord/src/runtime.js", () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
const { slackPlugin } = await import("../../../extensions/slack/src/channel.js");
|
||||
const { telegramPlugin } = await import("../../../extensions/telegram/src/channel.js");
|
||||
const { discordPlugin } = await import("../../../extensions/discord/src/channel.js");
|
||||
const { mattermostPlugin } = await import("../../../extensions/mattermost/src/channel.js");
|
||||
const { feishuPlugin } = await import("../../../extensions/feishu/src/channel.js");
|
||||
const { msteamsPlugin } = await import("../../../extensions/msteams/src/channel.js");
|
||||
const { zaloPlugin } = await import("../../../extensions/zalo/src/channel.js");
|
||||
|
||||
// Keep this matrix focused on capability wiring. The extension packages already
|
||||
// cover their own full channel/plugin boot paths, so local stubs are enough here.
|
||||
const slackPlugin: Pick<ChannelPlugin, "actions"> = {
|
||||
actions: {
|
||||
describeMessageTool: ({ cfg }) => {
|
||||
const account = cfg.channels?.slack;
|
||||
const enabled =
|
||||
typeof account?.botToken === "string" &&
|
||||
account.botToken.trim() !== "" &&
|
||||
typeof account?.appToken === "string" &&
|
||||
account.appToken.trim() !== "";
|
||||
const capabilities = new Set<string>();
|
||||
if (enabled) {
|
||||
capabilities.add("blocks");
|
||||
}
|
||||
if (
|
||||
account?.capabilities &&
|
||||
(account.capabilities as { interactiveReplies?: unknown }).interactiveReplies === true
|
||||
) {
|
||||
capabilities.add("interactive");
|
||||
}
|
||||
return {
|
||||
actions: enabled ? ["send"] : [],
|
||||
capabilities: Array.from(capabilities) as Array<"blocks" | "interactive">,
|
||||
};
|
||||
},
|
||||
supportsAction: () => true,
|
||||
},
|
||||
};
|
||||
|
||||
const mattermostPlugin: Pick<ChannelPlugin, "actions"> = {
|
||||
actions: {
|
||||
describeMessageTool: ({ cfg }) => {
|
||||
const account = cfg.channels?.mattermost;
|
||||
const enabled =
|
||||
account?.enabled !== false &&
|
||||
typeof account?.botToken === "string" &&
|
||||
account.botToken.trim() !== "" &&
|
||||
typeof account?.baseUrl === "string" &&
|
||||
account.baseUrl.trim() !== "";
|
||||
return {
|
||||
actions: enabled ? ["send"] : [],
|
||||
capabilities: enabled ? (["buttons"] as const) : [],
|
||||
};
|
||||
},
|
||||
supportsAction: () => true,
|
||||
},
|
||||
};
|
||||
|
||||
const feishuPlugin: Pick<ChannelPlugin, "actions"> = {
|
||||
actions: {
|
||||
describeMessageTool: ({ cfg }) => {
|
||||
const account = cfg.channels?.feishu;
|
||||
const enabled =
|
||||
account?.enabled !== false &&
|
||||
typeof account?.appId === "string" &&
|
||||
account.appId.trim() !== "" &&
|
||||
typeof account?.appSecret === "string" &&
|
||||
account.appSecret.trim() !== "";
|
||||
return {
|
||||
actions: enabled ? ["send"] : [],
|
||||
capabilities: enabled ? (["cards"] as const) : [],
|
||||
};
|
||||
},
|
||||
supportsAction: () => true,
|
||||
},
|
||||
};
|
||||
|
||||
const msteamsPlugin: Pick<ChannelPlugin, "actions"> = {
|
||||
actions: {
|
||||
describeMessageTool: ({ cfg }) => {
|
||||
const account = cfg.channels?.msteams;
|
||||
const enabled =
|
||||
account?.enabled !== false &&
|
||||
typeof account?.tenantId === "string" &&
|
||||
account.tenantId.trim() !== "" &&
|
||||
typeof account?.appId === "string" &&
|
||||
account.appId.trim() !== "" &&
|
||||
typeof account?.appPassword === "string" &&
|
||||
account.appPassword.trim() !== "";
|
||||
return {
|
||||
actions: enabled ? ["poll"] : [],
|
||||
capabilities: enabled ? (["cards"] as const) : [],
|
||||
};
|
||||
},
|
||||
supportsAction: () => true,
|
||||
},
|
||||
};
|
||||
|
||||
const zaloPlugin: Pick<ChannelPlugin, "actions"> = {
|
||||
actions: {
|
||||
describeMessageTool: () => ({ actions: [], capabilities: [] }),
|
||||
supportsAction: () => true,
|
||||
},
|
||||
};
|
||||
|
||||
describe("channel action capability matrix", () => {
|
||||
afterEach(() => {
|
||||
|
||||
@ -1,10 +1,18 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { signalPlugin } from "../../extensions/signal/src/channel.js";
|
||||
import { collectStatusIssuesFromLastError } from "../plugin-sdk/status-helpers.js";
|
||||
import { setActivePluginRegistry } from "../plugins/runtime.js";
|
||||
import { createTestRegistry } from "../test-utils/channel-plugins.js";
|
||||
import { createChannelTestPluginBase, createTestRegistry } from "../test-utils/channel-plugins.js";
|
||||
import { createIMessageTestPlugin } from "../test-utils/imessage-test-plugin.js";
|
||||
import { formatGatewayChannelsStatusLines } from "./channels/status.js";
|
||||
|
||||
const signalPlugin = {
|
||||
...createChannelTestPluginBase({ id: "signal" }),
|
||||
status: {
|
||||
collectStatusIssues: (accounts: Parameters<typeof collectStatusIssuesFromLastError>[1]) =>
|
||||
collectStatusIssuesFromLastError("signal", accounts),
|
||||
},
|
||||
};
|
||||
|
||||
describe("channels command", () => {
|
||||
beforeEach(() => {
|
||||
setActivePluginRegistry(
|
||||
|
||||
@ -5,6 +5,7 @@ import type {
|
||||
ChannelPlugin,
|
||||
} from "../channels/plugins/types.js";
|
||||
import type { CliDeps } from "../cli/deps.js";
|
||||
import { setActivePluginRegistry } from "../plugins/runtime.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { createTestRegistry } from "../test-utils/channel-plugins.js";
|
||||
import { captureEnv } from "../test-utils/env.js";
|
||||
@ -69,21 +70,17 @@ vi.mock("../../extensions/whatsapp/runtime-api.js", () => ({
|
||||
handleWhatsAppAction,
|
||||
}));
|
||||
|
||||
import { messageCommand } from "./message.js";
|
||||
|
||||
let envSnapshot: ReturnType<typeof captureEnv>;
|
||||
const EMPTY_TEST_REGISTRY = createTestRegistry([]);
|
||||
|
||||
const setRegistry = async (registry: ReturnType<typeof createTestRegistry>) => {
|
||||
const { setActivePluginRegistry } = await import("../plugins/runtime.js");
|
||||
setActivePluginRegistry(registry);
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
beforeEach(() => {
|
||||
envSnapshot = captureEnv(["TELEGRAM_BOT_TOKEN", "DISCORD_BOT_TOKEN"]);
|
||||
process.env.TELEGRAM_BOT_TOKEN = "";
|
||||
process.env.DISCORD_BOT_TOKEN = "";
|
||||
testConfig = {};
|
||||
({ messageCommand } = await import("./message.js"));
|
||||
await setRegistry(createTestRegistry([]));
|
||||
setActivePluginRegistry(EMPTY_TEST_REGISTRY);
|
||||
callGatewayMock.mockClear();
|
||||
webAuthExists.mockClear().mockResolvedValue(false);
|
||||
handleDiscordAction.mockClear();
|
||||
@ -197,8 +194,6 @@ const createTelegramPollPluginRegistration = () => ({
|
||||
}),
|
||||
});
|
||||
|
||||
let messageCommand: typeof import("./message.js").messageCommand;
|
||||
|
||||
function createTelegramSecretRawConfig() {
|
||||
return {
|
||||
channels: {
|
||||
@ -247,7 +242,7 @@ async function runTelegramDirectOutboundSend(params: {
|
||||
messageId: "msg-2",
|
||||
chatId: "123456",
|
||||
}));
|
||||
await setRegistry(
|
||||
setActivePluginRegistry(
|
||||
createTestRegistry([
|
||||
{
|
||||
pluginId: "telegram",
|
||||
@ -288,7 +283,7 @@ describe("messageCommand", () => {
|
||||
rawConfig: rawConfig as unknown as Record<string, unknown>,
|
||||
resolvedConfig: resolvedConfig as unknown as Record<string, unknown>,
|
||||
});
|
||||
await setRegistry(
|
||||
setActivePluginRegistry(
|
||||
createTestRegistry([
|
||||
{
|
||||
...createTelegramSendPluginRegistration(),
|
||||
@ -379,7 +374,7 @@ describe("messageCommand", () => {
|
||||
|
||||
it("defaults channel when only one configured", async () => {
|
||||
process.env.TELEGRAM_BOT_TOKEN = "token-abc";
|
||||
await setRegistry(
|
||||
setActivePluginRegistry(
|
||||
createTestRegistry([
|
||||
{
|
||||
...createTelegramSendPluginRegistration(),
|
||||
@ -401,7 +396,7 @@ describe("messageCommand", () => {
|
||||
it("requires channel when multiple configured", async () => {
|
||||
process.env.TELEGRAM_BOT_TOKEN = "token-abc";
|
||||
process.env.DISCORD_BOT_TOKEN = "token-discord";
|
||||
await setRegistry(
|
||||
setActivePluginRegistry(
|
||||
createTestRegistry([
|
||||
{
|
||||
...createTelegramSendPluginRegistration(),
|
||||
@ -426,7 +421,7 @@ describe("messageCommand", () => {
|
||||
|
||||
it("sends via gateway for WhatsApp", async () => {
|
||||
callGatewayMock.mockResolvedValueOnce({ messageId: "g1" });
|
||||
await setRegistry(
|
||||
setActivePluginRegistry(
|
||||
createTestRegistry([
|
||||
{
|
||||
pluginId: "whatsapp",
|
||||
@ -456,7 +451,7 @@ describe("messageCommand", () => {
|
||||
});
|
||||
|
||||
it("routes discord polls through message action", async () => {
|
||||
await setRegistry(
|
||||
setActivePluginRegistry(
|
||||
createTestRegistry([
|
||||
{
|
||||
...createDiscordPollPluginRegistration(),
|
||||
@ -485,7 +480,7 @@ describe("messageCommand", () => {
|
||||
});
|
||||
|
||||
it("routes telegram polls through message action", async () => {
|
||||
await setRegistry(
|
||||
setActivePluginRegistry(
|
||||
createTestRegistry([
|
||||
{
|
||||
...createTelegramPollPluginRegistration(),
|
||||
|
||||
@ -1,9 +1,12 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { discordPlugin } from "../../extensions/discord/src/channel.js";
|
||||
import { telegramPlugin } from "../../extensions/telegram/src/channel.js";
|
||||
import { isDiscordExecApprovalClientEnabled } from "../../extensions/discord/src/exec-approvals.js";
|
||||
import { buildTelegramExecApprovalButtons } from "../../extensions/telegram/src/approval-buttons.js";
|
||||
import { isTelegramExecApprovalClientEnabled } from "../../extensions/telegram/src/exec-approvals.js";
|
||||
import type { ChannelPlugin } from "../channels/plugins/types.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { buildExecApprovalPendingReplyPayload } from "../infra/exec-approval-reply.js";
|
||||
import { setActivePluginRegistry } from "../plugins/runtime.js";
|
||||
import { createTestRegistry } from "../test-utils/channel-plugins.js";
|
||||
import { createChannelTestPluginBase, createTestRegistry } from "../test-utils/channel-plugins.js";
|
||||
import { createExecApprovalForwarder } from "./exec-approval-forwarder.js";
|
||||
|
||||
const baseRequest = {
|
||||
@ -23,15 +26,65 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
const emptyRegistry = createTestRegistry([]);
|
||||
const telegramApprovalPlugin: Pick<
|
||||
ChannelPlugin,
|
||||
"id" | "meta" | "capabilities" | "config" | "execApprovals"
|
||||
> = {
|
||||
...createChannelTestPluginBase({ id: "telegram" }),
|
||||
execApprovals: {
|
||||
shouldSuppressForwardingFallback: ({ cfg, target, request }) => {
|
||||
if (target.channel !== "telegram" || request.request.turnSourceChannel !== "telegram") {
|
||||
return false;
|
||||
}
|
||||
const accountId = target.accountId?.trim() || request.request.turnSourceAccountId?.trim();
|
||||
return isTelegramExecApprovalClientEnabled({ cfg, accountId });
|
||||
},
|
||||
buildPendingPayload: ({ request, nowMs }) => {
|
||||
const payload = buildExecApprovalPendingReplyPayload({
|
||||
approvalId: request.id,
|
||||
approvalSlug: request.id.slice(0, 8),
|
||||
approvalCommandId: request.id,
|
||||
command: request.request.command,
|
||||
cwd: request.request.cwd ?? undefined,
|
||||
host: request.request.host === "node" ? "node" : "gateway",
|
||||
nodeId: request.request.nodeId ?? undefined,
|
||||
expiresAtMs: request.expiresAtMs,
|
||||
nowMs,
|
||||
});
|
||||
const buttons = buildTelegramExecApprovalButtons(request.id);
|
||||
if (!buttons) {
|
||||
return payload;
|
||||
}
|
||||
return {
|
||||
...payload,
|
||||
channelData: {
|
||||
...payload.channelData,
|
||||
telegram: { buttons },
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
const discordApprovalPlugin: Pick<
|
||||
ChannelPlugin,
|
||||
"id" | "meta" | "capabilities" | "config" | "execApprovals"
|
||||
> = {
|
||||
...createChannelTestPluginBase({ id: "discord" }),
|
||||
execApprovals: {
|
||||
shouldSuppressForwardingFallback: ({ cfg, target }) =>
|
||||
target.channel === "discord" &&
|
||||
isDiscordExecApprovalClientEnabled({ cfg, accountId: target.accountId }),
|
||||
},
|
||||
};
|
||||
const defaultRegistry = createTestRegistry([
|
||||
{
|
||||
pluginId: "telegram",
|
||||
plugin: telegramPlugin,
|
||||
plugin: telegramApprovalPlugin,
|
||||
source: "test",
|
||||
},
|
||||
{
|
||||
pluginId: "discord",
|
||||
plugin: discordPlugin,
|
||||
plugin: discordApprovalPlugin,
|
||||
source: "test",
|
||||
},
|
||||
]);
|
||||
|
||||
@ -1,15 +1,42 @@
|
||||
import { Separator, TextDisplay } from "@buape/carbon";
|
||||
import { Container, Separator, TextDisplay } from "@buape/carbon";
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { discordPlugin } from "../../../extensions/discord/src/channel.js";
|
||||
import { DiscordUiContainer } from "../../../extensions/discord/src/ui.js";
|
||||
import type { ChannelPlugin } from "../../channels/plugins/types.js";
|
||||
import { setActivePluginRegistry } from "../../plugins/runtime.js";
|
||||
import { createTestRegistry } from "../../test-utils/channel-plugins.js";
|
||||
import {
|
||||
createChannelTestPluginBase,
|
||||
createTestRegistry,
|
||||
} from "../../test-utils/channel-plugins.js";
|
||||
import { getChannelMessageAdapter } from "./channel-adapters.js";
|
||||
|
||||
class TestDiscordUiContainer extends Container {}
|
||||
|
||||
const discordCrossContextPlugin: Pick<
|
||||
ChannelPlugin,
|
||||
"id" | "meta" | "capabilities" | "config" | "messaging"
|
||||
> = {
|
||||
...createChannelTestPluginBase({ id: "discord" }),
|
||||
messaging: {
|
||||
buildCrossContextComponents: ({ originLabel, message, cfg, accountId }) => {
|
||||
const trimmed = message.trim();
|
||||
const components: Array<TextDisplay | Separator> = [];
|
||||
if (trimmed) {
|
||||
components.push(new TextDisplay(message));
|
||||
components.push(new Separator({ divider: true, spacing: "small" }));
|
||||
}
|
||||
components.push(new TextDisplay(`*From ${originLabel}*`));
|
||||
void cfg;
|
||||
void accountId;
|
||||
return [new TestDiscordUiContainer(components)];
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe("getChannelMessageAdapter", () => {
|
||||
beforeEach(() => {
|
||||
setActivePluginRegistry(
|
||||
createTestRegistry([{ pluginId: "discord", plugin: discordPlugin, source: "test" }]),
|
||||
createTestRegistry([
|
||||
{ pluginId: "discord", plugin: discordCrossContextPlugin, source: "test" },
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
@ -31,10 +58,10 @@ describe("getChannelMessageAdapter", () => {
|
||||
cfg: {} as never,
|
||||
accountId: "primary",
|
||||
});
|
||||
const container = components?.[0] as DiscordUiContainer | undefined;
|
||||
const container = components?.[0] as TestDiscordUiContainer | undefined;
|
||||
|
||||
expect(components).toHaveLength(1);
|
||||
expect(container).toBeInstanceOf(DiscordUiContainer);
|
||||
expect(container).toBeInstanceOf(TestDiscordUiContainer);
|
||||
expect(container?.components).toEqual([
|
||||
expect.any(TextDisplay),
|
||||
expect.any(Separator),
|
||||
@ -49,7 +76,7 @@ describe("getChannelMessageAdapter", () => {
|
||||
message: " ",
|
||||
cfg: {} as never,
|
||||
});
|
||||
const container = components?.[0] as DiscordUiContainer | undefined;
|
||||
const container = components?.[0] as TestDiscordUiContainer | undefined;
|
||||
|
||||
expect(components).toHaveLength(1);
|
||||
expect(container?.components).toEqual([expect.any(TextDisplay)]);
|
||||
|
||||
@ -2,8 +2,8 @@ import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { slackPlugin } from "../../../extensions/slack/src/channel.js";
|
||||
import { telegramPlugin } from "../../../extensions/telegram/src/channel.js";
|
||||
import { parseSlackTarget } from "../../../extensions/slack/src/targets.js";
|
||||
import { parseTelegramTarget } from "../../../extensions/telegram/src/targets.js";
|
||||
import type { ChannelThreadingToolContext } from "../../channels/plugins/types.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import {
|
||||
@ -27,28 +27,67 @@ function createToolContext(
|
||||
};
|
||||
}
|
||||
|
||||
function resolveSlackAutoThreadId(params: {
|
||||
to: string;
|
||||
toolContext?: {
|
||||
currentChannelId?: string;
|
||||
currentThreadTs?: string;
|
||||
replyToMode?: "off" | "first" | "all";
|
||||
hasRepliedRef?: { value: boolean };
|
||||
};
|
||||
}): string | undefined {
|
||||
const context = params.toolContext;
|
||||
if (!context?.currentThreadTs || !context.currentChannelId) {
|
||||
return undefined;
|
||||
}
|
||||
if (context.replyToMode !== "all" && context.replyToMode !== "first") {
|
||||
return undefined;
|
||||
}
|
||||
const parsedTarget = parseSlackTarget(params.to, { defaultKind: "channel" });
|
||||
if (!parsedTarget || parsedTarget.kind !== "channel") {
|
||||
return undefined;
|
||||
}
|
||||
if (parsedTarget.id.toLowerCase() !== context.currentChannelId.toLowerCase()) {
|
||||
return undefined;
|
||||
}
|
||||
if (context.replyToMode === "first" && context.hasRepliedRef?.value) {
|
||||
return undefined;
|
||||
}
|
||||
return context.currentThreadTs;
|
||||
}
|
||||
|
||||
function resolveTelegramAutoThreadId(params: {
|
||||
to: string;
|
||||
toolContext?: { currentThreadTs?: string; currentChannelId?: string };
|
||||
}): string | undefined {
|
||||
const context = params.toolContext;
|
||||
if (!context?.currentThreadTs || !context.currentChannelId) {
|
||||
return undefined;
|
||||
}
|
||||
const parsedTo = parseTelegramTarget(params.to);
|
||||
const parsedChannel = parseTelegramTarget(context.currentChannelId);
|
||||
if (parsedTo.chatId.toLowerCase() !== parsedChannel.chatId.toLowerCase()) {
|
||||
return undefined;
|
||||
}
|
||||
return context.currentThreadTs;
|
||||
}
|
||||
|
||||
describe("message action threading helpers", () => {
|
||||
it("resolves Slack auto-thread ids only for matching active channels", () => {
|
||||
expect(
|
||||
slackPlugin?.threading?.resolveAutoThreadId?.({
|
||||
cfg,
|
||||
accountId: undefined,
|
||||
resolveSlackAutoThreadId({
|
||||
to: "#c123",
|
||||
toolContext: createToolContext(),
|
||||
}),
|
||||
).toBe("thread-1");
|
||||
expect(
|
||||
slackPlugin?.threading?.resolveAutoThreadId?.({
|
||||
cfg,
|
||||
accountId: undefined,
|
||||
resolveSlackAutoThreadId({
|
||||
to: "channel:C999",
|
||||
toolContext: createToolContext(),
|
||||
}),
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
slackPlugin?.threading?.resolveAutoThreadId?.({
|
||||
cfg,
|
||||
accountId: undefined,
|
||||
resolveSlackAutoThreadId({
|
||||
to: "user:U123",
|
||||
toolContext: createToolContext(),
|
||||
}),
|
||||
@ -57,9 +96,7 @@ describe("message action threading helpers", () => {
|
||||
|
||||
it("skips Slack auto-thread ids when reply mode or context blocks them", () => {
|
||||
expect(
|
||||
slackPlugin?.threading?.resolveAutoThreadId?.({
|
||||
cfg,
|
||||
accountId: undefined,
|
||||
resolveSlackAutoThreadId({
|
||||
to: "C123",
|
||||
toolContext: createToolContext({
|
||||
replyToMode: "first",
|
||||
@ -68,17 +105,13 @@ describe("message action threading helpers", () => {
|
||||
}),
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
slackPlugin?.threading?.resolveAutoThreadId?.({
|
||||
cfg,
|
||||
accountId: undefined,
|
||||
resolveSlackAutoThreadId({
|
||||
to: "C123",
|
||||
toolContext: createToolContext({ replyToMode: "off" }),
|
||||
}),
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
slackPlugin?.threading?.resolveAutoThreadId?.({
|
||||
cfg,
|
||||
accountId: undefined,
|
||||
resolveSlackAutoThreadId({
|
||||
to: "C123",
|
||||
toolContext: createToolContext({ currentThreadTs: undefined }),
|
||||
}),
|
||||
@ -87,9 +120,7 @@ describe("message action threading helpers", () => {
|
||||
|
||||
it("resolves Telegram auto-thread ids for matching chats across target formats", () => {
|
||||
expect(
|
||||
telegramPlugin?.threading?.resolveAutoThreadId?.({
|
||||
cfg,
|
||||
accountId: undefined,
|
||||
resolveTelegramAutoThreadId({
|
||||
to: "telegram:group:-100123:topic:77",
|
||||
toolContext: createToolContext({
|
||||
currentChannelId: "tg:group:-100123",
|
||||
@ -97,9 +128,7 @@ describe("message action threading helpers", () => {
|
||||
}),
|
||||
).toBe("thread-1");
|
||||
expect(
|
||||
telegramPlugin?.threading?.resolveAutoThreadId?.({
|
||||
cfg,
|
||||
accountId: undefined,
|
||||
resolveTelegramAutoThreadId({
|
||||
to: "-100999:77",
|
||||
toolContext: createToolContext({
|
||||
currentChannelId: "-100123",
|
||||
@ -107,9 +136,7 @@ describe("message action threading helpers", () => {
|
||||
}),
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
telegramPlugin?.threading?.resolveAutoThreadId?.({
|
||||
cfg,
|
||||
accountId: undefined,
|
||||
resolveTelegramAutoThreadId({
|
||||
to: "-100123",
|
||||
toolContext: createToolContext({ currentChannelId: undefined }),
|
||||
}),
|
||||
|
||||
@ -1,8 +1,12 @@
|
||||
import { Container, Separator, TextDisplay } from "@buape/carbon";
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { discordPlugin } from "../../../extensions/discord/src/channel.js";
|
||||
import type { ChannelPlugin } from "../../channels/plugins/types.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { setActivePluginRegistry } from "../../plugins/runtime.js";
|
||||
import { createTestRegistry } from "../../test-utils/channel-plugins.js";
|
||||
import {
|
||||
createChannelTestPluginBase,
|
||||
createTestRegistry,
|
||||
} from "../../test-utils/channel-plugins.js";
|
||||
import {
|
||||
applyCrossContextDecoration,
|
||||
buildCrossContextDecoration,
|
||||
@ -10,6 +14,29 @@ import {
|
||||
shouldApplyCrossContextMarker,
|
||||
} from "./outbound-policy.js";
|
||||
|
||||
class TestDiscordUiContainer extends Container {}
|
||||
|
||||
const discordCrossContextPlugin: Pick<
|
||||
ChannelPlugin,
|
||||
"id" | "meta" | "capabilities" | "config" | "messaging"
|
||||
> = {
|
||||
...createChannelTestPluginBase({ id: "discord" }),
|
||||
messaging: {
|
||||
buildCrossContextComponents: ({ originLabel, message, cfg, accountId }) => {
|
||||
const trimmed = message.trim();
|
||||
const components: Array<TextDisplay | Separator> = [];
|
||||
if (trimmed) {
|
||||
components.push(new TextDisplay(message));
|
||||
components.push(new Separator({ divider: true, spacing: "small" }));
|
||||
}
|
||||
components.push(new TextDisplay(`*From ${originLabel}*`));
|
||||
void cfg;
|
||||
void accountId;
|
||||
return [new TestDiscordUiContainer(components)];
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const slackConfig = {
|
||||
channels: {
|
||||
slack: {
|
||||
@ -28,7 +55,9 @@ const discordConfig = {
|
||||
describe("outbound policy helpers", () => {
|
||||
beforeEach(() => {
|
||||
setActivePluginRegistry(
|
||||
createTestRegistry([{ pluginId: "discord", plugin: discordPlugin, source: "test" }]),
|
||||
createTestRegistry([
|
||||
{ pluginId: "discord", plugin: discordCrossContextPlugin, source: "test" },
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@ -2,7 +2,6 @@ import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { discordPlugin } from "../../../extensions/discord/src/channel.js";
|
||||
import type { ReplyPayload } from "../../auto-reply/types.js";
|
||||
import { setDefaultChannelPluginRegistryForTests } from "../../commands/channel-test-helpers.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
@ -44,9 +43,7 @@ import {
|
||||
import { runResolveOutboundTargetCoreTests } from "./targets.shared-test.js";
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePluginRegistry(
|
||||
createTestRegistry([{ pluginId: "discord", plugin: discordPlugin, source: "test" }]),
|
||||
);
|
||||
setActivePluginRegistry(createTestRegistry([]));
|
||||
});
|
||||
|
||||
describe("delivery-queue", () => {
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { discordPlugin } from "../../extensions/discord/src/channel.js";
|
||||
import { createTestRegistry } from "../test-utils/channel-plugins.js";
|
||||
import {
|
||||
__testing,
|
||||
@ -21,9 +20,7 @@ async function importCommandsModule(cacheBust: string): Promise<CommandsModule>
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePluginRegistry(
|
||||
createTestRegistry([{ pluginId: "discord", source: "test", plugin: discordPlugin }]),
|
||||
);
|
||||
setActivePluginRegistry(createTestRegistry([]));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user