mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-12 09:41:11 +00:00
fix(plugins): keep test helpers out of contract barrels (#63311)
Merged via squash.
Prepared head SHA: 769e90c6af
Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com>
Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com>
Reviewed-by: @altaywtf
This commit is contained in:
@@ -1 +1,19 @@
|
||||
export { createIMessageTestPlugin } from "../../../extensions/imessage/contract-api.js";
|
||||
import type { ChannelOutboundAdapter } from "openclaw/plugin-sdk/channel-contract";
|
||||
import type { ChannelPlugin } from "openclaw/plugin-sdk/core";
|
||||
import { loadBundledPluginTestApiSync } from "../../../src/test-utils/bundled-plugin-public-surface.js";
|
||||
|
||||
type CreateIMessageTestPlugin = (params?: { outbound?: ChannelOutboundAdapter }) => ChannelPlugin;
|
||||
|
||||
let createIMessageTestPluginCache: CreateIMessageTestPlugin | undefined;
|
||||
|
||||
function getCreateIMessageTestPlugin(): CreateIMessageTestPlugin {
|
||||
if (!createIMessageTestPluginCache) {
|
||||
({ createIMessageTestPlugin: createIMessageTestPluginCache } = loadBundledPluginTestApiSync<{
|
||||
createIMessageTestPlugin: CreateIMessageTestPlugin;
|
||||
}>("imessage"));
|
||||
}
|
||||
return createIMessageTestPluginCache;
|
||||
}
|
||||
|
||||
export const createIMessageTestPlugin: CreateIMessageTestPlugin = (...args) =>
|
||||
getCreateIMessageTestPlugin()(...args);
|
||||
|
||||
@@ -1,30 +1,77 @@
|
||||
import { beforeEach, expect, it, type Mock, vi } from "vitest";
|
||||
import { createSlackOutboundPayloadHarness } from "../../../extensions/slack/contract-api.js";
|
||||
import { whatsappOutbound } from "../../../extensions/whatsapp/test-api.js";
|
||||
import {
|
||||
chunkTextForOutbound as chunkZaloTextForOutbound,
|
||||
sendPayloadWithChunkedTextAndMedia as sendZaloPayloadWithChunkedTextAndMedia,
|
||||
} from "../../../extensions/zalo/runtime-api.js";
|
||||
import { sendPayloadWithChunkedTextAndMedia as sendZalouserPayloadWithChunkedTextAndMedia } from "../../../extensions/zalouser/runtime-api.js";
|
||||
import type { ReplyPayload } from "../../../src/auto-reply/types.js";
|
||||
import { primeChannelOutboundSendMock } from "../../../src/channels/plugins/contracts/test-helpers.js";
|
||||
import { createDirectTextMediaOutbound } from "../../../src/channels/plugins/outbound/direct-text-media.js";
|
||||
import type { ChannelOutboundAdapter } from "../../../src/channels/plugins/types.js";
|
||||
import { resetGlobalHookRunner } from "../../../src/plugins/hook-runner-global.js";
|
||||
import {
|
||||
loadBundledPluginPublicSurfaceSync,
|
||||
loadBundledPluginTestApiSync,
|
||||
resolveRelativeBundledPluginPublicModuleId,
|
||||
} from "../../../src/test-utils/bundled-plugin-public-surface.js";
|
||||
type ParseZalouserOutboundTarget = (raw: string) => { threadId: string; isGroup: boolean };
|
||||
type CreateSlackOutboundPayloadHarness = (params: PayloadHarnessParams) => {
|
||||
run: () => Promise<Record<string, unknown>>;
|
||||
sendMock: Mock;
|
||||
to: string;
|
||||
};
|
||||
type ChunkZaloTextForOutbound = (text: string, maxLength?: number) => string[];
|
||||
type SendPayloadWithChunkedTextAndMedia = (params: {
|
||||
ctx: {
|
||||
cfg: unknown;
|
||||
to: string;
|
||||
text: string;
|
||||
payload: ReplyPayload;
|
||||
};
|
||||
sendText: (ctx: {
|
||||
cfg: unknown;
|
||||
to: string;
|
||||
text: string;
|
||||
payload: ReplyPayload;
|
||||
}) => Promise<{ channel: string; messageId: string }>;
|
||||
sendMedia: (ctx: {
|
||||
cfg: unknown;
|
||||
to: string;
|
||||
text: string;
|
||||
payload: ReplyPayload;
|
||||
mediaUrl?: string;
|
||||
}) => Promise<{ channel: string; messageId: string }>;
|
||||
emptyResult: { channel: string; messageId: string };
|
||||
textChunkLimit?: number;
|
||||
chunker?: ChunkZaloTextForOutbound | null;
|
||||
}) => Promise<{ channel: string; messageId: string }>;
|
||||
|
||||
const discordOutboundAdapterModuleId = resolveRelativeBundledPluginPublicModuleId({
|
||||
fromModuleUrl: import.meta.url,
|
||||
pluginId: "discord",
|
||||
artifactBasename: "src/outbound-adapter.js",
|
||||
});
|
||||
const slackTestApiModuleId = resolveRelativeBundledPluginPublicModuleId({
|
||||
fromModuleUrl: import.meta.url,
|
||||
pluginId: "slack",
|
||||
artifactBasename: "test-api.js",
|
||||
});
|
||||
const whatsappTestApiModuleId = resolveRelativeBundledPluginPublicModuleId({
|
||||
fromModuleUrl: import.meta.url,
|
||||
pluginId: "whatsapp",
|
||||
artifactBasename: "test-api.js",
|
||||
});
|
||||
|
||||
let discordOutboundCache: Promise<ChannelOutboundAdapter> | undefined;
|
||||
let parseZalouserOutboundTargetCache: ParseZalouserOutboundTarget | undefined;
|
||||
let slackTestApiPromise:
|
||||
| Promise<{
|
||||
createSlackOutboundPayloadHarness: CreateSlackOutboundPayloadHarness;
|
||||
}>
|
||||
| undefined;
|
||||
let whatsappTestApiPromise:
|
||||
| Promise<{
|
||||
whatsappOutbound: ChannelOutboundAdapter;
|
||||
}>
|
||||
| undefined;
|
||||
let chunkZaloTextForOutboundCache: ChunkZaloTextForOutbound | undefined;
|
||||
let sendZaloPayloadWithChunkedTextAndMediaCache: SendPayloadWithChunkedTextAndMedia | undefined;
|
||||
let sendZalouserPayloadWithChunkedTextAndMediaCache: SendPayloadWithChunkedTextAndMedia | undefined;
|
||||
|
||||
async function getDiscordOutbound(): Promise<ChannelOutboundAdapter> {
|
||||
discordOutboundCache ??= (async () => {
|
||||
@@ -36,6 +83,47 @@ async function getDiscordOutbound(): Promise<ChannelOutboundAdapter> {
|
||||
return await discordOutboundCache;
|
||||
}
|
||||
|
||||
async function getCreateSlackOutboundPayloadHarness(): Promise<CreateSlackOutboundPayloadHarness> {
|
||||
slackTestApiPromise ??= import(slackTestApiModuleId) as Promise<{
|
||||
createSlackOutboundPayloadHarness: CreateSlackOutboundPayloadHarness;
|
||||
}>;
|
||||
const { createSlackOutboundPayloadHarness } = await slackTestApiPromise;
|
||||
return createSlackOutboundPayloadHarness;
|
||||
}
|
||||
|
||||
async function getWhatsAppOutboundAsync(): Promise<ChannelOutboundAdapter> {
|
||||
whatsappTestApiPromise ??= import(whatsappTestApiModuleId) as Promise<{
|
||||
whatsappOutbound: ChannelOutboundAdapter;
|
||||
}>;
|
||||
const { whatsappOutbound } = await whatsappTestApiPromise;
|
||||
return whatsappOutbound;
|
||||
}
|
||||
|
||||
function getChunkZaloTextForOutbound(): ChunkZaloTextForOutbound {
|
||||
if (!chunkZaloTextForOutboundCache) {
|
||||
({ chunkTextForOutbound: chunkZaloTextForOutboundCache } = loadBundledPluginPublicSurfaceSync<{
|
||||
chunkTextForOutbound: ChunkZaloTextForOutbound;
|
||||
}>({
|
||||
pluginId: "zalo",
|
||||
artifactBasename: "runtime-api.js",
|
||||
}));
|
||||
}
|
||||
return chunkZaloTextForOutboundCache;
|
||||
}
|
||||
|
||||
function getSendZaloPayloadWithChunkedTextAndMedia(): SendPayloadWithChunkedTextAndMedia {
|
||||
if (!sendZaloPayloadWithChunkedTextAndMediaCache) {
|
||||
({ sendPayloadWithChunkedTextAndMedia: sendZaloPayloadWithChunkedTextAndMediaCache } =
|
||||
loadBundledPluginPublicSurfaceSync<{
|
||||
sendPayloadWithChunkedTextAndMedia: SendPayloadWithChunkedTextAndMedia;
|
||||
}>({
|
||||
pluginId: "zalo",
|
||||
artifactBasename: "runtime-api.js",
|
||||
}));
|
||||
}
|
||||
return sendZaloPayloadWithChunkedTextAndMediaCache;
|
||||
}
|
||||
|
||||
function getParseZalouserOutboundTarget(): ParseZalouserOutboundTarget {
|
||||
if (!parseZalouserOutboundTargetCache) {
|
||||
({ parseZalouserOutboundTarget: parseZalouserOutboundTargetCache } =
|
||||
@@ -46,6 +134,19 @@ function getParseZalouserOutboundTarget(): ParseZalouserOutboundTarget {
|
||||
return parseZalouserOutboundTargetCache;
|
||||
}
|
||||
|
||||
function getSendZalouserPayloadWithChunkedTextAndMedia(): SendPayloadWithChunkedTextAndMedia {
|
||||
if (!sendZalouserPayloadWithChunkedTextAndMediaCache) {
|
||||
({ sendPayloadWithChunkedTextAndMedia: sendZalouserPayloadWithChunkedTextAndMediaCache } =
|
||||
loadBundledPluginPublicSurfaceSync<{
|
||||
sendPayloadWithChunkedTextAndMedia: SendPayloadWithChunkedTextAndMedia;
|
||||
}>({
|
||||
pluginId: "zalouser",
|
||||
artifactBasename: "runtime-api.js",
|
||||
}));
|
||||
}
|
||||
return sendZalouserPayloadWithChunkedTextAndMediaCache;
|
||||
}
|
||||
|
||||
type PayloadHarnessParams = {
|
||||
payload: ReplyPayload;
|
||||
sendResults?: Array<{ messageId: string }>;
|
||||
@@ -76,18 +177,24 @@ type ChunkingMode =
|
||||
function installChannelOutboundPayloadContractSuite(params: {
|
||||
channel: string;
|
||||
chunking: ChunkingMode;
|
||||
createHarness: (params: { payload: PayloadLike; sendResults?: SendResultLike[] }) => {
|
||||
run: () => Promise<Record<string, unknown>>;
|
||||
sendMock: Mock;
|
||||
to: string;
|
||||
};
|
||||
createHarness: (params: { payload: PayloadLike; sendResults?: SendResultLike[] }) =>
|
||||
| {
|
||||
run: () => Promise<Record<string, unknown>>;
|
||||
sendMock: Mock;
|
||||
to: string;
|
||||
}
|
||||
| Promise<{
|
||||
run: () => Promise<Record<string, unknown>>;
|
||||
sendMock: Mock;
|
||||
to: string;
|
||||
}>;
|
||||
}) {
|
||||
beforeEach(() => {
|
||||
resetGlobalHookRunner();
|
||||
});
|
||||
|
||||
it("text-only delegates to sendText", async () => {
|
||||
const { run, sendMock, to } = params.createHarness({
|
||||
const { run, sendMock, to } = await params.createHarness({
|
||||
payload: { text: "hello" },
|
||||
});
|
||||
const result = await run();
|
||||
@@ -98,7 +205,7 @@ function installChannelOutboundPayloadContractSuite(params: {
|
||||
});
|
||||
|
||||
it("single media delegates to sendMedia", async () => {
|
||||
const { run, sendMock, to } = params.createHarness({
|
||||
const { run, sendMock, to } = await params.createHarness({
|
||||
payload: { text: "cap", mediaUrl: "https://example.com/a.jpg" },
|
||||
});
|
||||
const result = await run();
|
||||
@@ -113,7 +220,7 @@ function installChannelOutboundPayloadContractSuite(params: {
|
||||
});
|
||||
|
||||
it("multi-media iterates URLs with caption on first", async () => {
|
||||
const { run, sendMock, to } = params.createHarness({
|
||||
const { run, sendMock, to } = await params.createHarness({
|
||||
payload: {
|
||||
text: "caption",
|
||||
mediaUrls: ["https://example.com/1.jpg", "https://example.com/2.jpg"],
|
||||
@@ -139,7 +246,7 @@ function installChannelOutboundPayloadContractSuite(params: {
|
||||
});
|
||||
|
||||
it("empty payload returns no-op", async () => {
|
||||
const { run, sendMock } = params.createHarness({ payload: {} });
|
||||
const { run, sendMock } = await params.createHarness({ payload: {} });
|
||||
const result = await run();
|
||||
|
||||
expect(sendMock).not.toHaveBeenCalled();
|
||||
@@ -149,7 +256,7 @@ function installChannelOutboundPayloadContractSuite(params: {
|
||||
if (params.chunking.mode === "passthrough") {
|
||||
it("text exceeding chunk limit is sent as-is when chunker is null", async () => {
|
||||
const text = "a".repeat(params.chunking.longTextLength);
|
||||
const { run, sendMock, to } = params.createHarness({ payload: { text } });
|
||||
const { run, sendMock, to } = await params.createHarness({ payload: { text } });
|
||||
const result = await run();
|
||||
|
||||
expect(sendMock).toHaveBeenCalledTimes(1);
|
||||
@@ -163,7 +270,7 @@ function installChannelOutboundPayloadContractSuite(params: {
|
||||
|
||||
it("chunking splits long text", async () => {
|
||||
const text = "a".repeat(chunking.longTextLength);
|
||||
const { run, sendMock } = params.createHarness({
|
||||
const { run, sendMock } = await params.createHarness({
|
||||
payload: { text },
|
||||
sendResults: [{ messageId: "c-1" }, { messageId: "c-2" }],
|
||||
});
|
||||
@@ -220,7 +327,7 @@ function createWhatsAppHarness(params: PayloadHarnessParams) {
|
||||
},
|
||||
};
|
||||
return {
|
||||
run: async () => await whatsappOutbound.sendPayload!(ctx),
|
||||
run: async () => await (await getWhatsAppOutboundAsync()).sendPayload!(ctx),
|
||||
sendMock: sendWhatsApp,
|
||||
to: ctx.to,
|
||||
};
|
||||
@@ -260,10 +367,10 @@ function createZaloHarness(params: PayloadHarnessParams) {
|
||||
};
|
||||
return {
|
||||
run: async () =>
|
||||
await sendZaloPayloadWithChunkedTextAndMedia({
|
||||
await getSendZaloPayloadWithChunkedTextAndMedia()({
|
||||
ctx,
|
||||
textChunkLimit: 2000,
|
||||
chunker: chunkZaloTextForOutbound,
|
||||
chunker: getChunkZaloTextForOutbound(),
|
||||
sendText: async (nextCtx) =>
|
||||
buildChannelSendResult(
|
||||
"zalo",
|
||||
@@ -299,7 +406,7 @@ function createZalouserHarness(params: PayloadHarnessParams) {
|
||||
};
|
||||
return {
|
||||
run: async () =>
|
||||
await sendZalouserPayloadWithChunkedTextAndMedia({
|
||||
await getSendZalouserPayloadWithChunkedTextAndMedia()({
|
||||
ctx,
|
||||
sendText: async (nextCtx) => {
|
||||
const target = getParseZalouserOutboundTarget()(nextCtx.to);
|
||||
@@ -339,7 +446,7 @@ export function installSlackOutboundPayloadContractSuite() {
|
||||
installChannelOutboundPayloadContractSuite({
|
||||
channel: "slack",
|
||||
chunking: { mode: "passthrough", longTextLength: 5000 },
|
||||
createHarness: createSlackOutboundPayloadHarness,
|
||||
createHarness: async (params) => (await getCreateSlackOutboundPayloadHarness())(params),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user