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:
Altay
2026-04-08 22:59:05 +01:00
committed by GitHub
parent 9ffe216a52
commit 554bc0a9fd
21 changed files with 529 additions and 62 deletions

View File

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

View File

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