From 698c200eba2c88a76f349644e49e65f4424eb849 Mon Sep 17 00:00:00 2001 From: Shakker Date: Wed, 4 Mar 2026 18:39:38 +0000 Subject: [PATCH] fix(outbound): fail media-only text-only adapter fallback --- src/infra/outbound/deliver.test.ts | 47 ++++++++++++++++++++++++++++++ src/infra/outbound/deliver.ts | 4 ++- 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/src/infra/outbound/deliver.test.ts b/src/infra/outbound/deliver.test.ts index 236d66c783c..7bc6d69f98a 100644 --- a/src/infra/outbound/deliver.test.ts +++ b/src/infra/outbound/deliver.test.ts @@ -971,6 +971,53 @@ describe("deliverOutboundPayloads", () => { expect(results).toEqual([{ channel: "matrix", messageId: "mx-2" }]); }); + it("fails media-only payloads when plugin outbound omits sendMedia", async () => { + hookMocks.runner.hasHooks.mockReturnValue(true); + const sendText = vi.fn().mockResolvedValue({ channel: "matrix", messageId: "mx-3" }); + setActivePluginRegistry( + createTestRegistry([ + { + pluginId: "matrix", + source: "test", + plugin: createOutboundTestPlugin({ + id: "matrix", + outbound: { deliveryMode: "direct", sendText }, + }), + }, + ]), + ); + + await expect( + deliverOutboundPayloads({ + cfg: {}, + channel: "matrix", + to: "!room:1", + payloads: [{ text: " ", mediaUrl: "https://example.com/file.png" }], + }), + ).rejects.toThrow( + "Plugin outbound adapter does not implement sendMedia and no text fallback is available for media payload", + ); + + expect(sendText).not.toHaveBeenCalled(); + expect(logMocks.warn).toHaveBeenCalledWith( + "Plugin outbound adapter does not implement sendMedia; media URLs will be dropped and text fallback will be used", + expect.objectContaining({ + channel: "matrix", + mediaCount: 1, + }), + ); + expect(hookMocks.runner.runMessageSent).toHaveBeenCalledWith( + expect.objectContaining({ + to: "!room:1", + content: "", + success: false, + error: + "Plugin outbound adapter does not implement sendMedia and no text fallback is available for media payload", + }), + expect.objectContaining({ channelId: "matrix" }), + ); + }); + it("emits message_sent failure when delivery errors", async () => { hookMocks.runner.hasHooks.mockReturnValue(true); const sendWhatsApp = vi.fn().mockRejectedValue(new Error("downstream failed")); diff --git a/src/infra/outbound/deliver.ts b/src/infra/outbound/deliver.ts index f110a17501f..0b1f0bc72fc 100644 --- a/src/infra/outbound/deliver.ts +++ b/src/infra/outbound/deliver.ts @@ -750,7 +750,9 @@ async function deliverOutboundPayloadsCore( ); const fallbackText = payloadSummary.text.trim(); if (!fallbackText) { - continue; + throw new Error( + "Plugin outbound adapter does not implement sendMedia and no text fallback is available for media payload", + ); } const beforeCount = results.length; await sendTextChunks(fallbackText, sendOverrides);