diff --git a/docs/.generated/plugin-sdk-api-baseline.sha256 b/docs/.generated/plugin-sdk-api-baseline.sha256 index 36aee4f0383..f6d2da78785 100644 --- a/docs/.generated/plugin-sdk-api-baseline.sha256 +++ b/docs/.generated/plugin-sdk-api-baseline.sha256 @@ -1,2 +1,2 @@ -15505f02306eb9920e350babf1b290d8fcc1b2338b4fccf65d3525de83d9bbcb plugin-sdk-api-baseline.json -06fc58082be4c2e1df1030fa9e8d965c6d5947984c4aed360c0474c1639a0a84 plugin-sdk-api-baseline.jsonl +db3c843947298c9af4b5f5fa7ecde6656dba32189ec386c29192fe498d64e5e5 plugin-sdk-api-baseline.json +c14586fd393a1375ee02ba507ffc83a2886d97632e323e5661b618c71624d26b plugin-sdk-api-baseline.jsonl diff --git a/extensions/discord/src/outbound-payload.contract.test.ts b/extensions/discord/src/outbound-payload.contract.test.ts new file mode 100644 index 00000000000..f954036f4f3 --- /dev/null +++ b/extensions/discord/src/outbound-payload.contract.test.ts @@ -0,0 +1,38 @@ +import { + installChannelOutboundPayloadContractSuite, + primeChannelOutboundSendMock, + type OutboundPayloadHarnessParams, +} from "openclaw/plugin-sdk/testing"; +import { describe, vi } from "vitest"; +import { discordOutbound } from "./outbound-adapter.js"; + +function createDiscordHarness(params: OutboundPayloadHarnessParams) { + const sendDiscord = vi.fn(); + primeChannelOutboundSendMock( + sendDiscord, + { messageId: "dc-1", channelId: "123456" }, + params.sendResults, + ); + const ctx = { + cfg: {}, + to: "channel:123456", + text: "", + payload: params.payload, + deps: { + sendDiscord, + }, + }; + return { + run: async () => await discordOutbound.sendPayload!(ctx), + sendMock: sendDiscord, + to: ctx.to, + }; +} + +describe("Discord outbound payload contract", () => { + installChannelOutboundPayloadContractSuite({ + channel: "discord", + chunking: { mode: "passthrough", longTextLength: 3000 }, + createHarness: createDiscordHarness, + }); +}); diff --git a/extensions/slack/src/outbound-payload.contract.test.ts b/extensions/slack/src/outbound-payload.contract.test.ts new file mode 100644 index 00000000000..b555d3ff74d --- /dev/null +++ b/extensions/slack/src/outbound-payload.contract.test.ts @@ -0,0 +1,11 @@ +import { installChannelOutboundPayloadContractSuite } from "openclaw/plugin-sdk/testing"; +import { describe } from "vitest"; +import { createSlackOutboundPayloadHarness } from "./outbound-payload.test-harness.js"; + +describe("Slack outbound payload contract", () => { + installChannelOutboundPayloadContractSuite({ + channel: "slack", + chunking: { mode: "passthrough", longTextLength: 5000 }, + createHarness: createSlackOutboundPayloadHarness, + }); +}); diff --git a/extensions/whatsapp/src/outbound-payload.contract.test.ts b/extensions/whatsapp/src/outbound-payload.contract.test.ts new file mode 100644 index 00000000000..7f961066415 --- /dev/null +++ b/extensions/whatsapp/src/outbound-payload.contract.test.ts @@ -0,0 +1,34 @@ +import { + installChannelOutboundPayloadContractSuite, + primeChannelOutboundSendMock, + type OutboundPayloadHarnessParams, +} from "openclaw/plugin-sdk/testing"; +import { describe, vi } from "vitest"; +import { whatsappOutbound } from "./outbound-adapter.js"; + +function createWhatsAppHarness(params: OutboundPayloadHarnessParams) { + const sendWhatsApp = vi.fn(); + primeChannelOutboundSendMock(sendWhatsApp, { messageId: "wa-1" }, params.sendResults); + const ctx = { + cfg: {}, + to: "5511999999999@c.us", + text: "", + payload: params.payload, + deps: { + whatsapp: sendWhatsApp, + }, + }; + return { + run: async () => await whatsappOutbound.sendPayload!(ctx), + sendMock: sendWhatsApp, + to: ctx.to, + }; +} + +describe("WhatsApp outbound payload contract", () => { + installChannelOutboundPayloadContractSuite({ + channel: "whatsapp", + chunking: { mode: "split", longTextLength: 5000, maxChunkLength: 4000 }, + createHarness: createWhatsAppHarness, + }); +}); diff --git a/extensions/zalo/src/outbound-payload.contract.test.ts b/extensions/zalo/src/outbound-payload.contract.test.ts new file mode 100644 index 00000000000..8367c38d1b7 --- /dev/null +++ b/extensions/zalo/src/outbound-payload.contract.test.ts @@ -0,0 +1,45 @@ +import { + installChannelOutboundPayloadContractSuite, + primeChannelOutboundSendMock, + type OutboundPayloadHarnessParams, +} from "openclaw/plugin-sdk/testing"; +import { describe, vi } from "vitest"; +import { zaloPlugin } from "./channel.js"; + +const { sendZaloTextMock } = vi.hoisted(() => ({ + sendZaloTextMock: vi.fn(), +})); + +vi.mock("./channel.runtime.js", () => ({ + sendZaloText: sendZaloTextMock, +})); + +function createZaloHarness(params: OutboundPayloadHarnessParams) { + const sendZalo = vi.fn(); + primeChannelOutboundSendMock(sendZalo, { ok: true, messageId: "zl-1" }, params.sendResults); + sendZaloTextMock.mockReset().mockImplementation( + async (nextCtx: { to: string; text: string; mediaUrl?: string }) => + await sendZalo(nextCtx.to, nextCtx.text, { + mediaUrl: nextCtx.mediaUrl, + }), + ); + const ctx = { + cfg: {}, + to: "123456789", + text: "", + payload: params.payload, + }; + return { + run: async () => await zaloPlugin.outbound!.sendPayload!(ctx), + sendMock: sendZalo, + to: ctx.to, + }; +} + +describe("Zalo outbound payload contract", () => { + installChannelOutboundPayloadContractSuite({ + channel: "zalo", + chunking: { mode: "split", longTextLength: 3000, maxChunkLength: 2000 }, + createHarness: createZaloHarness, + }); +}); diff --git a/extensions/zalouser/src/channel.sendpayload.test.ts b/extensions/zalouser/src/channel.sendpayload.test.ts index d39bc5b547d..1407ecb5418 100644 --- a/extensions/zalouser/src/channel.sendpayload.test.ts +++ b/extensions/zalouser/src/channel.sendpayload.test.ts @@ -1,4 +1,8 @@ -import { primeChannelOutboundSendMock } from "openclaw/plugin-sdk/testing"; +import { + installChannelOutboundPayloadContractSuite, + primeChannelOutboundSendMock, + type OutboundPayloadHarnessParams, +} from "openclaw/plugin-sdk/testing"; import { beforeEach, describe, expect, it, vi } from "vitest"; import "./accounts.test-mocks.js"; import "./zalo-js.test-mocks.js"; @@ -109,6 +113,38 @@ describe("zalouserPlugin outbound sendPayload", () => { }); }); +describe("zalouserPlugin outbound payload contract", () => { + function createZalouserHarness(params: OutboundPayloadHarnessParams) { + const mockedSend = vi.mocked(sendModule.sendMessageZalouser); + setZalouserRuntime({ + channel: { + text: { + resolveChunkMode: vi.fn(() => "length"), + resolveTextChunkLimit: vi.fn(() => 1200), + }, + }, + } as never); + primeChannelOutboundSendMock(mockedSend, { ok: true, messageId: "zlu-1" }, params.sendResults); + const ctx = { + cfg: {}, + to: "user:987654321", + text: "", + payload: params.payload, + }; + return { + run: async () => await zalouserPlugin.outbound!.sendPayload!(ctx), + sendMock: mockedSend, + to: "987654321", + }; + } + + installChannelOutboundPayloadContractSuite({ + channel: "zalouser", + chunking: { mode: "passthrough", longTextLength: 3000 }, + createHarness: createZalouserHarness, + }); +}); + describe("zalouserPlugin messaging target normalization", () => { it("normalizes user/group aliases to canonical targets", () => { const normalize = zalouserPlugin.messaging?.normalizeTarget; diff --git a/src/channels/plugins/contracts/outbound-payload-testkit.ts b/src/channels/plugins/contracts/outbound-payload-testkit.ts new file mode 100644 index 00000000000..53169bb885a --- /dev/null +++ b/src/channels/plugins/contracts/outbound-payload-testkit.ts @@ -0,0 +1,134 @@ +import { beforeEach, expect, it, type Mock } from "vitest"; +import type { ReplyPayload } from "../../../plugin-sdk/reply-payload.js"; +import { resetGlobalHookRunner } from "../../../plugins/hook-runner-global.js"; + +type PayloadLike = Pick; + +type SendResultLike = { + messageId: string; + [key: string]: unknown; +}; + +type ChunkingMode = + | { + longTextLength: number; + maxChunkLength: number; + mode: "split"; + } + | { + longTextLength: number; + mode: "passthrough"; + }; + +type OutboundPayloadHarness = { + run: () => Promise>; + sendMock: Mock; + to: string; +}; + +export type OutboundPayloadHarnessParams = { + payload: PayloadLike; + sendResults?: SendResultLike[]; +}; + +export function installChannelOutboundPayloadContractSuite(params: { + channel: string; + chunking: ChunkingMode; + createHarness: ( + params: OutboundPayloadHarnessParams, + ) => OutboundPayloadHarness | Promise; +}) { + beforeEach(() => { + resetGlobalHookRunner(); + }); + + it("text-only delegates to sendText", async () => { + const { run, sendMock, to } = await params.createHarness({ + payload: { text: "hello" }, + }); + const result = await run(); + + expect(sendMock).toHaveBeenCalledTimes(1); + expect(sendMock).toHaveBeenCalledWith(to, "hello", expect.any(Object)); + expect(result).toMatchObject({ channel: params.channel }); + }); + + it("single media delegates to sendMedia", async () => { + const { run, sendMock, to } = await params.createHarness({ + payload: { text: "cap", mediaUrl: "https://example.com/a.jpg" }, + }); + const result = await run(); + + expect(sendMock).toHaveBeenCalledTimes(1); + expect(sendMock).toHaveBeenCalledWith( + to, + "cap", + expect.objectContaining({ mediaUrl: "https://example.com/a.jpg" }), + ); + expect(result).toMatchObject({ channel: params.channel }); + }); + + it("multi-media iterates URLs with caption on first", async () => { + const { run, sendMock, to } = await params.createHarness({ + payload: { + text: "caption", + mediaUrls: ["https://example.com/1.jpg", "https://example.com/2.jpg"], + }, + sendResults: [{ messageId: "m-1" }, { messageId: "m-2" }], + }); + const result = await run(); + + expect(sendMock).toHaveBeenCalledTimes(2); + expect(sendMock).toHaveBeenNthCalledWith( + 1, + to, + "caption", + expect.objectContaining({ mediaUrl: "https://example.com/1.jpg" }), + ); + expect(sendMock).toHaveBeenNthCalledWith( + 2, + to, + "", + expect.objectContaining({ mediaUrl: "https://example.com/2.jpg" }), + ); + expect(result).toMatchObject({ channel: params.channel, messageId: "m-2" }); + }); + + it("empty payload returns no-op", async () => { + const { run, sendMock } = await params.createHarness({ payload: {} }); + const result = await run(); + + expect(sendMock).not.toHaveBeenCalled(); + expect(result).toEqual({ channel: params.channel, messageId: "" }); + }); + + 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 } = await params.createHarness({ payload: { text } }); + const result = await run(); + + expect(sendMock).toHaveBeenCalledTimes(1); + expect(sendMock).toHaveBeenCalledWith(to, text, expect.any(Object)); + expect(result).toMatchObject({ channel: params.channel }); + }); + return; + } + + const chunking = params.chunking; + + it("chunking splits long text", async () => { + const text = "a".repeat(chunking.longTextLength); + const { run, sendMock } = await params.createHarness({ + payload: { text }, + sendResults: [{ messageId: "c-1" }, { messageId: "c-2" }], + }); + const result = await run(); + + expect(sendMock.mock.calls.length).toBeGreaterThanOrEqual(2); + for (const call of sendMock.mock.calls) { + expect((call[1] as string).length).toBeLessThanOrEqual(chunking.maxChunkLength); + } + expect(result).toMatchObject({ channel: params.channel }); + }); +} diff --git a/src/channels/plugins/contracts/outbound-payload.contract.test.ts b/src/channels/plugins/contracts/outbound-payload.contract.test.ts index 1a0f1de8e5d..e0025a76ef7 100644 --- a/src/channels/plugins/contracts/outbound-payload.contract.test.ts +++ b/src/channels/plugins/contracts/outbound-payload.contract.test.ts @@ -1,35 +1,40 @@ -import { describe } from "vitest"; +import { describe, vi } from "vitest"; +import { createDirectTextMediaOutbound } from "../outbound/direct-text-media.js"; import { - installDirectTextMediaOutboundPayloadContractSuite, - installDiscordOutboundPayloadContractSuite, - installSlackOutboundPayloadContractSuite, - installWhatsAppOutboundPayloadContractSuite, - installZaloOutboundPayloadContractSuite, - installZalouserOutboundPayloadContractSuite, -} from "../../../../test/helpers/channels/outbound-payload-contract.js"; + installChannelOutboundPayloadContractSuite, + type OutboundPayloadHarnessParams, +} from "./outbound-payload-testkit.js"; +import { primeChannelOutboundSendMock } from "./test-helpers.js"; + +function createDirectTextMediaHarness(params: OutboundPayloadHarnessParams) { + const sendFn = vi.fn(); + primeChannelOutboundSendMock(sendFn, { messageId: "m1" }, params.sendResults); + const outbound = createDirectTextMediaOutbound({ + channel: "direct-text-media", + resolveSender: () => sendFn, + resolveMaxBytes: () => undefined, + buildTextOptions: (opts) => opts as never, + buildMediaOptions: (opts) => opts as never, + }); + const ctx = { + cfg: {}, + to: "user1", + text: "", + payload: params.payload, + }; + return { + run: async () => await outbound.sendPayload!(ctx), + sendMock: sendFn, + to: ctx.to, + }; +} describe("outbound payload contracts", () => { - describe("discord", () => { - installDiscordOutboundPayloadContractSuite(); - }); - - describe("imessage", () => { - installDirectTextMediaOutboundPayloadContractSuite(); - }); - - describe("slack", () => { - installSlackOutboundPayloadContractSuite(); - }); - - describe("whatsapp", () => { - installWhatsAppOutboundPayloadContractSuite(); - }); - - describe("zalo", () => { - installZaloOutboundPayloadContractSuite(); - }); - - describe("zalouser", () => { - installZalouserOutboundPayloadContractSuite(); + describe("direct text/media", () => { + installChannelOutboundPayloadContractSuite({ + channel: "direct-text-media", + chunking: { mode: "split", longTextLength: 5000, maxChunkLength: 4000 }, + createHarness: createDirectTextMediaHarness, + }); }); }); diff --git a/src/plugin-sdk/testing.ts b/src/plugin-sdk/testing.ts index f2fc5290e92..69556c5a8e6 100644 --- a/src/plugin-sdk/testing.ts +++ b/src/plugin-sdk/testing.ts @@ -6,6 +6,10 @@ export { expectChannelInboundContextContract, primeChannelOutboundSendMock, } from "../channels/plugins/contracts/test-helpers.js"; +export { + installChannelOutboundPayloadContractSuite, + type OutboundPayloadHarnessParams, +} from "../channels/plugins/contracts/outbound-payload-testkit.js"; export { buildDispatchInboundCaptureMock } from "../channels/plugins/contracts/inbound-testkit.js"; export { createCliRuntimeCapture, diff --git a/test/helpers/channels/outbound-payload-contract.ts b/test/helpers/channels/outbound-payload-contract.ts deleted file mode 100644 index ffe676709b3..00000000000 --- a/test/helpers/channels/outbound-payload-contract.ts +++ /dev/null @@ -1,409 +0,0 @@ -import { beforeEach, expect, it, type Mock, vi } from "vitest"; -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 { sendPayloadWithChunkedTextAndMedia } from "../../../src/plugin-sdk/reply-payload.js"; -import { chunkTextForOutbound } from "../../../src/plugin-sdk/text-chunking.js"; -import { resetGlobalHookRunner } from "../../../src/plugins/hook-runner-global.js"; -import { resolveRelativeBundledPluginPublicModuleId } from "../../../src/test-utils/bundled-plugin-public-surface.js"; -type CreateSlackOutboundPayloadHarness = (params: PayloadHarnessParams) => { - run: () => Promise>; - sendMock: Mock; - to: 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: "outbound-payload-test-api.js", -}); -const whatsappTestApiModuleId = resolveRelativeBundledPluginPublicModuleId({ - fromModuleUrl: import.meta.url, - pluginId: "whatsapp", - artifactBasename: "outbound-payload-test-api.js", -}); - -let discordOutboundCache: Promise | undefined; -let slackTestApiPromise: - | Promise<{ - createSlackOutboundPayloadHarness: CreateSlackOutboundPayloadHarness; - }> - | undefined; -let whatsappTestApiPromise: - | Promise<{ - whatsappOutbound: ChannelOutboundAdapter; - }> - | undefined; - -async function getDiscordOutbound(): Promise { - discordOutboundCache ??= (async () => { - const module = (await import(discordOutboundAdapterModuleId)) as { - discordOutbound: ChannelOutboundAdapter; - }; - return module.discordOutbound; - })(); - return await discordOutboundCache; -} - -async function getCreateSlackOutboundPayloadHarness(): Promise { - slackTestApiPromise ??= import(slackTestApiModuleId) as Promise<{ - createSlackOutboundPayloadHarness: CreateSlackOutboundPayloadHarness; - }>; - const { createSlackOutboundPayloadHarness } = await slackTestApiPromise; - return createSlackOutboundPayloadHarness; -} - -async function getWhatsAppOutboundAsync(): Promise { - whatsappTestApiPromise ??= import(whatsappTestApiModuleId) as Promise<{ - whatsappOutbound: ChannelOutboundAdapter; - }>; - const { whatsappOutbound } = await whatsappTestApiPromise; - return whatsappOutbound; -} - -type PayloadHarnessParams = { - payload: ReplyPayload; - sendResults?: Array<{ messageId: string }>; -}; - -type PayloadLike = { - mediaUrl?: string; - mediaUrls?: string[]; - text?: string; -}; - -type SendResultLike = { - messageId: string; - [key: string]: unknown; -}; - -type ChunkingMode = - | { - longTextLength: number; - maxChunkLength: number; - mode: "split"; - } - | { - longTextLength: number; - mode: "passthrough"; - }; - -function installChannelOutboundPayloadContractSuite(params: { - channel: string; - chunking: ChunkingMode; - createHarness: (params: { payload: PayloadLike; sendResults?: SendResultLike[] }) => - | { - run: () => Promise>; - sendMock: Mock; - to: string; - } - | Promise<{ - run: () => Promise>; - sendMock: Mock; - to: string; - }>; -}) { - beforeEach(() => { - resetGlobalHookRunner(); - }); - - it("text-only delegates to sendText", async () => { - const { run, sendMock, to } = await params.createHarness({ - payload: { text: "hello" }, - }); - const result = await run(); - - expect(sendMock).toHaveBeenCalledTimes(1); - expect(sendMock).toHaveBeenCalledWith(to, "hello", expect.any(Object)); - expect(result).toMatchObject({ channel: params.channel }); - }); - - it("single media delegates to sendMedia", async () => { - const { run, sendMock, to } = await params.createHarness({ - payload: { text: "cap", mediaUrl: "https://example.com/a.jpg" }, - }); - const result = await run(); - - expect(sendMock).toHaveBeenCalledTimes(1); - expect(sendMock).toHaveBeenCalledWith( - to, - "cap", - expect.objectContaining({ mediaUrl: "https://example.com/a.jpg" }), - ); - expect(result).toMatchObject({ channel: params.channel }); - }); - - it("multi-media iterates URLs with caption on first", async () => { - const { run, sendMock, to } = await params.createHarness({ - payload: { - text: "caption", - mediaUrls: ["https://example.com/1.jpg", "https://example.com/2.jpg"], - }, - sendResults: [{ messageId: "m-1" }, { messageId: "m-2" }], - }); - const result = await run(); - - expect(sendMock).toHaveBeenCalledTimes(2); - expect(sendMock).toHaveBeenNthCalledWith( - 1, - to, - "caption", - expect.objectContaining({ mediaUrl: "https://example.com/1.jpg" }), - ); - expect(sendMock).toHaveBeenNthCalledWith( - 2, - to, - "", - expect.objectContaining({ mediaUrl: "https://example.com/2.jpg" }), - ); - expect(result).toMatchObject({ channel: params.channel, messageId: "m-2" }); - }); - - it("empty payload returns no-op", async () => { - const { run, sendMock } = await params.createHarness({ payload: {} }); - const result = await run(); - - expect(sendMock).not.toHaveBeenCalled(); - expect(result).toEqual({ channel: params.channel, messageId: "" }); - }); - - 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 } = await params.createHarness({ payload: { text } }); - const result = await run(); - - expect(sendMock).toHaveBeenCalledTimes(1); - expect(sendMock).toHaveBeenCalledWith(to, text, expect.any(Object)); - expect(result).toMatchObject({ channel: params.channel }); - }); - return; - } - - const chunking = params.chunking; - - it("chunking splits long text", async () => { - const text = "a".repeat(chunking.longTextLength); - const { run, sendMock } = await params.createHarness({ - payload: { text }, - sendResults: [{ messageId: "c-1" }, { messageId: "c-2" }], - }); - const result = await run(); - - expect(sendMock.mock.calls.length).toBeGreaterThanOrEqual(2); - for (const call of sendMock.mock.calls) { - expect((call[1] as string).length).toBeLessThanOrEqual(chunking.maxChunkLength); - } - expect(result).toMatchObject({ channel: params.channel }); - }); -} - -function buildChannelSendResult(channel: string, result: Record) { - return { - channel, - messageId: typeof result.messageId === "string" ? result.messageId : "", - }; -} - -function createDiscordHarness(params: PayloadHarnessParams) { - const sendDiscord = vi.fn(); - primeChannelOutboundSendMock( - sendDiscord, - { messageId: "dc-1", channelId: "123456" }, - params.sendResults, - ); - const ctx = { - cfg: {}, - to: "channel:123456", - text: "", - payload: params.payload, - deps: { - sendDiscord, - }, - }; - return { - run: async () => await (await getDiscordOutbound()).sendPayload!(ctx), - sendMock: sendDiscord, - to: ctx.to, - }; -} - -function createWhatsAppHarness(params: PayloadHarnessParams) { - const sendWhatsApp = vi.fn(); - primeChannelOutboundSendMock(sendWhatsApp, { messageId: "wa-1" }, params.sendResults); - const ctx = { - cfg: {}, - to: "5511999999999@c.us", - text: "", - payload: params.payload, - deps: { - whatsapp: sendWhatsApp, - }, - }; - return { - run: async () => await (await getWhatsAppOutboundAsync()).sendPayload!(ctx), - sendMock: sendWhatsApp, - to: ctx.to, - }; -} - -function createDirectTextMediaHarness(params: PayloadHarnessParams) { - const sendFn = vi.fn(); - primeChannelOutboundSendMock(sendFn, { messageId: "m1" }, params.sendResults); - const outbound = createDirectTextMediaOutbound({ - channel: "imessage", - resolveSender: () => sendFn, - resolveMaxBytes: () => undefined, - buildTextOptions: (opts) => opts as never, - buildMediaOptions: (opts) => opts as never, - }); - const ctx = { - cfg: {}, - to: "user1", - text: "", - payload: params.payload, - }; - return { - run: async () => await outbound.sendPayload!(ctx), - sendMock: sendFn, - to: ctx.to, - }; -} - -function createZaloHarness(params: PayloadHarnessParams) { - const sendZalo = vi.fn(); - primeChannelOutboundSendMock(sendZalo, { ok: true, messageId: "zl-1" }, params.sendResults); - const ctx = { - cfg: {}, - to: "123456789", - text: "", - payload: params.payload, - }; - return { - run: async () => - await sendPayloadWithChunkedTextAndMedia({ - ctx, - textChunkLimit: 2000, - chunker: chunkTextForOutbound, - sendText: async (nextCtx) => - buildChannelSendResult( - "zalo", - await sendZalo(nextCtx.to, nextCtx.text, { - accountId: undefined, - cfg: nextCtx.cfg, - }), - ), - sendMedia: async (nextCtx) => - buildChannelSendResult( - "zalo", - await sendZalo(nextCtx.to, nextCtx.text, { - accountId: undefined, - cfg: nextCtx.cfg, - mediaUrl: nextCtx.mediaUrl, - }), - ), - emptyResult: { channel: "zalo", messageId: "" }, - }), - sendMock: sendZalo, - to: ctx.to, - }; -} - -function createZalouserHarness(params: PayloadHarnessParams) { - const sendZalouser = vi.fn(); - primeChannelOutboundSendMock(sendZalouser, { ok: true, messageId: "zlu-1" }, params.sendResults); - const ctx = { - cfg: {}, - to: "987654321", - text: "", - payload: params.payload, - }; - return { - run: async () => - await sendPayloadWithChunkedTextAndMedia({ - ctx, - sendText: async (nextCtx) => { - return buildChannelSendResult( - "zalouser", - await sendZalouser(nextCtx.to, nextCtx.text, { - profile: "default", - isGroup: false, - textMode: "markdown", - textChunkMode: "length", - textChunkLimit: 1200, - }), - ); - }, - sendMedia: async (nextCtx) => { - return buildChannelSendResult( - "zalouser", - await sendZalouser(nextCtx.to, nextCtx.text, { - profile: "default", - isGroup: false, - mediaUrl: nextCtx.mediaUrl, - textMode: "markdown", - textChunkMode: "length", - textChunkLimit: 1200, - }), - ); - }, - emptyResult: { channel: "zalouser", messageId: "" }, - }), - sendMock: sendZalouser, - to: ctx.to, - }; -} - -export function installSlackOutboundPayloadContractSuite() { - installChannelOutboundPayloadContractSuite({ - channel: "slack", - chunking: { mode: "passthrough", longTextLength: 5000 }, - createHarness: async (params) => (await getCreateSlackOutboundPayloadHarness())(params), - }); -} - -export function installDiscordOutboundPayloadContractSuite() { - installChannelOutboundPayloadContractSuite({ - channel: "discord", - chunking: { mode: "passthrough", longTextLength: 3000 }, - createHarness: createDiscordHarness, - }); -} - -export function installWhatsAppOutboundPayloadContractSuite() { - installChannelOutboundPayloadContractSuite({ - channel: "whatsapp", - chunking: { mode: "split", longTextLength: 5000, maxChunkLength: 4000 }, - createHarness: createWhatsAppHarness, - }); -} - -export function installZaloOutboundPayloadContractSuite() { - installChannelOutboundPayloadContractSuite({ - channel: "zalo", - chunking: { mode: "split", longTextLength: 3000, maxChunkLength: 2000 }, - createHarness: createZaloHarness, - }); -} - -export function installZalouserOutboundPayloadContractSuite() { - installChannelOutboundPayloadContractSuite({ - channel: "zalouser", - chunking: { mode: "passthrough", longTextLength: 3000 }, - createHarness: createZalouserHarness, - }); -} - -export function installDirectTextMediaOutboundPayloadContractSuite() { - installChannelOutboundPayloadContractSuite({ - channel: "imessage", - chunking: { mode: "split", longTextLength: 5000, maxChunkLength: 4000 }, - createHarness: createDirectTextMediaHarness, - }); -}