// Discord tests cover channel.message adapter plugin behavior. import { verifyChannelMessageAdapterCapabilityProofs, verifyChannelMessageLiveCapabilityAdapterProofs, verifyChannelMessageLiveFinalizerProofs, } from "openclaw/plugin-sdk/channel-outbound"; import { beforeAll, beforeEach, describe, expect, it } from "vitest"; import { createDiscordOutboundHoisted, installDiscordOutboundModuleSpies, resetDiscordOutboundMocks, } from "./outbound-adapter.test-harness.js"; const hoisted = createDiscordOutboundHoisted(); await installDiscordOutboundModuleSpies(hoisted); let discordPlugin: typeof import("./channel.js").discordPlugin; beforeAll(async () => { ({ discordPlugin } = await import("./channel.js")); }); type DiscordMessageAdapter = NonNullable; type DiscordMessageSender = NonNullable; function requireDiscordMessageAdapter(): DiscordMessageAdapter { const adapter = discordPlugin.message; if (!adapter) { throw new Error("Expected discord plugin to expose a channel message adapter"); } return adapter; } function requireTextSender( adapter: DiscordMessageAdapter, ): NonNullable { const text = adapter.send?.text; if (!text) { throw new Error("Expected discord message adapter text sender"); } return text; } function requireMediaSender( adapter: DiscordMessageAdapter, ): NonNullable { const media = adapter.send?.media; if (!media) { throw new Error("Expected discord message adapter media sender"); } return media; } function requirePayloadSender( adapter: DiscordMessageAdapter, ): NonNullable { const payload = adapter.send?.payload; if (!payload) { throw new Error("Expected discord message adapter payload sender"); } return payload; } function requirePollSender( adapter: DiscordMessageAdapter, ): NonNullable { const poll = adapter.send?.poll; if (!poll) { throw new Error("Expected discord message adapter poll sender"); } return poll; } describe("discord channel message adapter", () => { beforeEach(() => { resetDiscordOutboundMocks(hoisted); }); it("backs declared durable-final capabilities with outbound send proofs", async () => { const adapter = requireDiscordMessageAdapter(); const sendText = requireTextSender(adapter); const sendMedia = requireMediaSender(adapter); const sendPayload = requirePayloadSender(adapter); const sendPoll = requirePollSender(adapter); const proveText = async () => { resetDiscordOutboundMocks(hoisted); const result = await sendText({ cfg: {}, to: "channel:123456", text: "hello", accountId: "default", }); expect(hoisted.sendMessageDiscordMock).toHaveBeenLastCalledWith("channel:123456", "hello", { verbose: false, replyTo: undefined, accountId: "default", silent: undefined, cfg: {}, textLimit: undefined, maxLinesPerMessage: undefined, tableMode: undefined, chunkMode: undefined, }); expect(result.receipt.platformMessageIds).toEqual(["msg-1"]); expect(result.receipt.parts[0]?.kind).toBe("text"); }; const proveMedia = async () => { resetDiscordOutboundMocks(hoisted); const result = await sendMedia({ cfg: {}, to: "channel:123456", text: "caption", mediaUrl: "https://example.com/a.png", accountId: "default", }); expect(hoisted.sendMessageDiscordMock).toHaveBeenLastCalledWith("channel:123456", "caption", { verbose: false, mediaUrl: "https://example.com/a.png", mediaAccess: undefined, mediaLocalRoots: undefined, mediaReadFile: undefined, replyTo: undefined, accountId: "default", silent: undefined, cfg: {}, textLimit: undefined, maxLinesPerMessage: undefined, tableMode: undefined, chunkMode: undefined, }); expect(result.receipt.parts[0]?.kind).toBe("media"); }; const provePayload = async () => { resetDiscordOutboundMocks(hoisted); const result = await sendPayload({ cfg: {}, to: "channel:123456", text: "payload", payload: { text: "payload" }, accountId: "default", }); expect(hoisted.sendMessageDiscordMock).toHaveBeenLastCalledWith( "channel:123456", "payload", expect.objectContaining({ verbose: false, replyTo: undefined, accountId: "default", silent: undefined, cfg: {}, textLimit: undefined, maxLinesPerMessage: undefined, tableMode: undefined, chunkMode: undefined, onDeliveryResult: expect.any(Function), }), ); expect(result.receipt.platformMessageIds).toEqual(["msg-1"]); }; const provePoll = async () => { resetDiscordOutboundMocks(hoisted); const result = await sendPoll({ cfg: {}, to: "channel:123456", poll: { question: "Ship?", options: ["Yes", "No"] }, accountId: "default", silent: true, }); expect(hoisted.sendPollDiscordMock).toHaveBeenLastCalledWith( "channel:123456", { question: "Ship?", options: ["Yes", "No"] }, { accountId: "default", silent: true, cfg: {}, }, ); expect(result.receipt.parts[0]?.kind).toBe("poll"); }; const proveReplyThreadSilent = async () => { resetDiscordOutboundMocks(hoisted); const result = await sendText({ cfg: {}, to: "channel:parent-1", text: "threaded", accountId: "default", replyToId: "reply-1", threadId: "thread-1", silent: true, }); expect(hoisted.sendMessageDiscordMock).toHaveBeenLastCalledWith( "channel:thread-1", "threaded", { verbose: false, accountId: "default", replyTo: "reply-1", silent: true, cfg: {}, textLimit: undefined, maxLinesPerMessage: undefined, tableMode: undefined, chunkMode: undefined, }, ); expect(result.receipt.threadId).toBe("thread-1"); expect(result.receipt.replyToId).toBe("reply-1"); }; await verifyChannelMessageAdapterCapabilityProofs({ adapterName: "discordMessageAdapter", adapter, proofs: { text: proveText, media: proveMedia, poll: provePoll, payload: provePayload, silent: proveReplyThreadSilent, replyTo: proveReplyThreadSilent, thread: proveReplyThreadSilent, messageSendingHooks: () => { expect(sendText).toBeTypeOf("function"); }, }, }); }); it("backs declared live preview finalizer capabilities with adapter proofs", async () => { const adapter = requireDiscordMessageAdapter(); const sendText = requireTextSender(adapter); await verifyChannelMessageLiveCapabilityAdapterProofs({ adapterName: "discordMessageAdapter", adapter, proofs: { draftPreview: () => { expect(adapter.live?.finalizer?.capabilities?.discardPending).toBe(true); }, previewFinalization: () => { expect(adapter.live?.finalizer?.capabilities?.finalEdit).toBe(true); }, progressUpdates: () => { expect(adapter.live?.capabilities?.draftPreview).toBe(true); }, }, }); await verifyChannelMessageLiveFinalizerProofs({ adapterName: "discordMessageAdapter", adapter, proofs: { finalEdit: () => { expect(adapter.live?.capabilities?.previewFinalization).toBe(true); }, normalFallback: () => { expect(sendText).toBeTypeOf("function"); }, discardPending: () => { expect(adapter.live?.capabilities?.draftPreview).toBe(true); }, }, }); }); });