diff --git a/extensions/discord/src/actions/runtime.messaging.ts b/extensions/discord/src/actions/runtime.messaging.ts index adbad1bd2d54..15a074e00730 100644 --- a/extensions/discord/src/actions/runtime.messaging.ts +++ b/extensions/discord/src/actions/runtime.messaging.ts @@ -64,6 +64,15 @@ export const discordMessagingActionRuntime = { unpinMessageDiscord, }; +function hasDiscordComponentObjectKeys(value: unknown): value is Record { + return Boolean( + value && + typeof value === "object" && + !Array.isArray(value) && + Object.keys(value as Record).length > 0, + ); +} + function parseDiscordMessageLink(link: string) { const normalized = link.trim(); const match = normalized.match( @@ -299,10 +308,9 @@ export async function handleDiscordMessagingAction( const asVoice = params.asVoice === true; const silent = params.silent === true; const rawComponents = params.components; - const componentSpec = - rawComponents && typeof rawComponents === "object" && !Array.isArray(rawComponents) - ? discordMessagingActionRuntime.readDiscordComponentSpec(rawComponents) - : null; + const componentSpec = hasDiscordComponentObjectKeys(rawComponents) + ? discordMessagingActionRuntime.readDiscordComponentSpec(rawComponents) + : null; const components: DiscordSendComponents | undefined = Array.isArray(rawComponents) || typeof rawComponents === "function" ? (rawComponents as DiscordSendComponents) diff --git a/extensions/discord/src/actions/runtime.test.ts b/extensions/discord/src/actions/runtime.test.ts index 8f11162f8f3f..2eb76c9d426a 100644 --- a/extensions/discord/src/actions/runtime.test.ts +++ b/extensions/discord/src/actions/runtime.test.ts @@ -46,6 +46,7 @@ const discordSendMocks = { removeOwnReactionsDiscord: vi.fn(async () => ({ removed: ["👍"] })), removeReactionDiscord: vi.fn(async () => ({})), searchMessagesDiscord: vi.fn(async () => ({})), + sendDiscordComponentMessage: vi.fn(async () => ({})), sendMessageDiscord: vi.fn(async () => ({})), sendPollDiscord: vi.fn(async () => ({})), sendStickerDiscord: vi.fn(async () => ({})), @@ -71,6 +72,7 @@ const { removeOwnReactionsDiscord, removeReactionDiscord, searchMessagesDiscord, + sendDiscordComponentMessage, sendMessageDiscord, sendPollDiscord, sendVoiceMessageDiscord, @@ -366,6 +368,33 @@ describe("handleDiscordMessagingAction", () => { ); }); + it("ignores empty components objects for regular media sends", async () => { + sendMessageDiscord.mockClear(); + sendDiscordComponentMessage.mockClear(); + + await handleDiscordMessagingAction( + "sendMessage", + { + to: "channel:123", + content: "hello", + mediaUrl: "/tmp/image.png", + components: {}, + }, + enableAllActions, + { mediaLocalRoots: ["/tmp/agent-root"] }, + ); + + expect(sendDiscordComponentMessage).not.toHaveBeenCalled(); + expect(sendMessageDiscord).toHaveBeenCalledWith( + "channel:123", + "hello", + expect.objectContaining({ + mediaUrl: "/tmp/image.png", + mediaLocalRoots: ["/tmp/agent-root"], + }), + ); + }); + it("rejects voice messages that include content", async () => { await expect( handleDiscordMessagingAction(