diff --git a/src/infra/outbound/outbound-send-service.test.ts b/src/infra/outbound/outbound-send-service.test.ts index b8b44410c4d..4132da4f877 100644 --- a/src/infra/outbound/outbound-send-service.test.ts +++ b/src/infra/outbound/outbound-send-service.test.ts @@ -3,6 +3,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; const mocks = vi.hoisted(() => ({ dispatchChannelMessageAction: vi.fn(), sendMessage: vi.fn(), + sendPoll: vi.fn(), })); vi.mock("../../channels/plugins/message-actions.js", () => ({ @@ -11,15 +12,16 @@ vi.mock("../../channels/plugins/message-actions.js", () => ({ vi.mock("./message.js", () => ({ sendMessage: (...args: unknown[]) => mocks.sendMessage(...args), - sendPoll: vi.fn(), + sendPoll: (...args: unknown[]) => mocks.sendPoll(...args), })); -import { executeSendAction } from "./outbound-send-service.js"; +import { executePollAction, executeSendAction } from "./outbound-send-service.js"; describe("executeSendAction", () => { beforeEach(() => { mocks.dispatchChannelMessageAction.mockReset(); mocks.sendMessage.mockReset(); + mocks.sendPoll.mockReset(); }); it("forwards ctx.agentId to sendMessage on core outbound path", async () => { @@ -52,4 +54,77 @@ describe("executeSendAction", () => { }), ); }); + + it("uses plugin poll action when available", async () => { + mocks.dispatchChannelMessageAction.mockResolvedValue({ + ok: true, + value: { messageId: "poll-plugin" }, + continuePrompt: "", + output: "", + sessionId: "s1", + model: "gpt-5.2", + usage: {}, + }); + + const result = await executePollAction({ + ctx: { + cfg: {}, + channel: "discord", + params: {}, + dryRun: false, + }, + to: "channel:123", + question: "Lunch?", + options: ["Pizza", "Sushi"], + maxSelections: 1, + }); + + expect(result.handledBy).toBe("plugin"); + expect(mocks.sendPoll).not.toHaveBeenCalled(); + }); + + it("forwards poll args to sendPoll on core outbound path", async () => { + mocks.dispatchChannelMessageAction.mockResolvedValue(null); + mocks.sendPoll.mockResolvedValue({ + channel: "discord", + to: "channel:123", + question: "Lunch?", + options: ["Pizza", "Sushi"], + maxSelections: 1, + durationSeconds: null, + durationHours: null, + via: "gateway", + }); + + await executePollAction({ + ctx: { + cfg: {}, + channel: "discord", + params: {}, + accountId: "acc-1", + dryRun: false, + }, + to: "channel:123", + question: "Lunch?", + options: ["Pizza", "Sushi"], + maxSelections: 1, + durationSeconds: 300, + threadId: "thread-1", + isAnonymous: true, + }); + + expect(mocks.sendPoll).toHaveBeenCalledWith( + expect.objectContaining({ + channel: "discord", + accountId: "acc-1", + to: "channel:123", + question: "Lunch?", + options: ["Pizza", "Sushi"], + maxSelections: 1, + durationSeconds: 300, + threadId: "thread-1", + isAnonymous: true, + }), + ); + }); }); diff --git a/src/infra/outbound/outbound-send-service.ts b/src/infra/outbound/outbound-send-service.ts index 85fa5800195..2c5c5bafdc3 100644 --- a/src/infra/outbound/outbound-send-service.ts +++ b/src/infra/outbound/outbound-send-service.ts @@ -40,6 +40,41 @@ export type OutboundSendContext = { silent?: boolean; }; +type PluginHandledResult = { + handledBy: "plugin"; + payload: unknown; + toolResult: AgentToolResult; +}; + +async function tryHandleWithPluginAction(params: { + ctx: OutboundSendContext; + action: "send" | "poll"; + onHandled?: () => Promise | void; +}): Promise { + if (params.ctx.dryRun) { + return null; + } + const handled = await dispatchChannelMessageAction({ + channel: params.ctx.channel, + action: params.action, + cfg: params.ctx.cfg, + params: params.ctx.params, + accountId: params.ctx.accountId ?? undefined, + gateway: params.ctx.gateway, + toolContext: params.ctx.toolContext, + dryRun: params.ctx.dryRun, + }); + if (!handled) { + return null; + } + await params.onHandled?.(); + return { + handledBy: "plugin", + payload: extractToolPayload(handled), + toolResult: handled, + }; +} + export async function executeSendAction(params: { ctx: OutboundSendContext; to: string; @@ -57,37 +92,28 @@ export async function executeSendAction(params: { sendResult?: MessageSendResult; }> { throwIfAborted(params.ctx.abortSignal); - if (!params.ctx.dryRun) { - const handled = await dispatchChannelMessageAction({ - channel: params.ctx.channel, - action: "send", - cfg: params.ctx.cfg, - params: params.ctx.params, - accountId: params.ctx.accountId ?? undefined, - gateway: params.ctx.gateway, - toolContext: params.ctx.toolContext, - dryRun: params.ctx.dryRun, - }); - if (handled) { - if (params.ctx.mirror) { - const mirrorText = params.ctx.mirror.text ?? params.message; - const mirrorMediaUrls = - params.ctx.mirror.mediaUrls ?? - params.mediaUrls ?? - (params.mediaUrl ? [params.mediaUrl] : undefined); - await appendAssistantMessageToSessionTranscript({ - agentId: params.ctx.mirror.agentId, - sessionKey: params.ctx.mirror.sessionKey, - text: mirrorText, - mediaUrls: mirrorMediaUrls, - }); + const pluginHandled = await tryHandleWithPluginAction({ + ctx: params.ctx, + action: "send", + onHandled: async () => { + if (!params.ctx.mirror) { + return; } - return { - handledBy: "plugin", - payload: extractToolPayload(handled), - toolResult: handled, - }; - } + const mirrorText = params.ctx.mirror.text ?? params.message; + const mirrorMediaUrls = + params.ctx.mirror.mediaUrls ?? + params.mediaUrls ?? + (params.mediaUrl ? [params.mediaUrl] : undefined); + await appendAssistantMessageToSessionTranscript({ + agentId: params.ctx.mirror.agentId, + sessionKey: params.ctx.mirror.sessionKey, + text: mirrorText, + mediaUrls: mirrorMediaUrls, + }); + }, + }); + if (pluginHandled) { + return pluginHandled; } throwIfAborted(params.ctx.abortSignal); @@ -135,24 +161,12 @@ export async function executePollAction(params: { toolResult?: AgentToolResult; pollResult?: MessagePollResult; }> { - if (!params.ctx.dryRun) { - const handled = await dispatchChannelMessageAction({ - channel: params.ctx.channel, - action: "poll", - cfg: params.ctx.cfg, - params: params.ctx.params, - accountId: params.ctx.accountId ?? undefined, - gateway: params.ctx.gateway, - toolContext: params.ctx.toolContext, - dryRun: params.ctx.dryRun, - }); - if (handled) { - return { - handledBy: "plugin", - payload: extractToolPayload(handled), - toolResult: handled, - }; - } + const pluginHandled = await tryHandleWithPluginAction({ + ctx: params.ctx, + action: "poll", + }); + if (pluginHandled) { + return pluginHandled; } const result: MessagePollResult = await sendPoll({