diff --git a/src/infra/outbound/message-action-runner.media.test.ts b/src/infra/outbound/message-action-runner.media.test.ts index 698c6270a91..8ecfd863846 100644 --- a/src/infra/outbound/message-action-runner.media.test.ts +++ b/src/infra/outbound/message-action-runner.media.test.ts @@ -6,7 +6,7 @@ import { jsonResult } from "../../agents/tools/common.js"; import type { ChannelPlugin } from "../../channels/plugins/types.js"; import type { OpenClawConfig } from "../../config/config.js"; import { loadWebMedia } from "../../media/web-media.js"; -import { setActivePluginRegistry } from "../../plugins/runtime.js"; +import { getActivePluginRegistry, setActivePluginRegistry } from "../../plugins/runtime.js"; import { createChannelTestPluginBase, createTestRegistry, @@ -19,6 +19,113 @@ const onePixelPng = Buffer.from( "base64", ); +const channelResolutionMocks = vi.hoisted(() => ({ + resolveOutboundChannelPlugin: vi.fn(), + executeSendAction: vi.fn(), + executePollAction: vi.fn(), +})); + +vi.mock("./channel-resolution.js", () => ({ + resolveOutboundChannelPlugin: channelResolutionMocks.resolveOutboundChannelPlugin, + resetOutboundChannelResolutionStateForTest: vi.fn(), +})); + +vi.mock("./outbound-send-service.js", () => ({ + executeSendAction: channelResolutionMocks.executeSendAction, + executePollAction: channelResolutionMocks.executePollAction, +})); + +vi.mock("./outbound-session.js", () => ({ + ensureOutboundSessionEntry: vi.fn(async () => undefined), + resolveOutboundSessionRoute: vi.fn(async () => null), +})); + +vi.mock("./message-action-threading.js", () => ({ + resolveAndApplyOutboundThreadId: vi.fn( + ( + actionParams: Record, + context: { + cfg: OpenClawConfig; + to: string; + accountId?: string | null; + toolContext?: Record; + resolveAutoThreadId?: (params: { + cfg: OpenClawConfig; + accountId?: string | null; + to: string; + toolContext?: Record; + replyToId?: string; + }) => string | undefined; + }, + ) => { + const explicit = + typeof actionParams.threadId === "string" ? actionParams.threadId : undefined; + const replyToId = typeof actionParams.replyTo === "string" ? actionParams.replyTo : undefined; + const resolved = + explicit ?? + context.resolveAutoThreadId?.({ + cfg: context.cfg, + accountId: context.accountId, + to: context.to, + toolContext: context.toolContext, + replyToId, + }); + if (resolved && !actionParams.threadId) { + actionParams.threadId = resolved; + } + return resolved ?? undefined; + }, + ), + prepareOutboundMirrorRoute: vi.fn( + async ({ + actionParams, + cfg, + to, + accountId, + toolContext, + agentId, + resolveAutoThreadId, + }: { + actionParams: Record; + cfg: OpenClawConfig; + to: string; + accountId?: string | null; + toolContext?: Record; + agentId?: string; + resolveAutoThreadId?: (params: { + cfg: OpenClawConfig; + accountId?: string | null; + to: string; + toolContext?: Record; + replyToId?: string; + }) => string | undefined; + }) => { + const explicit = + typeof actionParams.threadId === "string" ? actionParams.threadId : undefined; + const replyToId = typeof actionParams.replyTo === "string" ? actionParams.replyTo : undefined; + const resolvedThreadId = + explicit ?? + resolveAutoThreadId?.({ + cfg, + accountId, + to, + toolContext, + replyToId, + }); + if (resolvedThreadId && !actionParams.threadId) { + actionParams.threadId = resolvedThreadId; + } + if (agentId) { + actionParams.__agentId = agentId; + } + return { + resolvedThreadId, + outboundRoute: null, + }; + }, + ), +})); + vi.mock("../../media/web-media.js", async () => { const actual = await vi.importActual( "../../media/web-media.js", @@ -131,6 +238,47 @@ describe("runMessageAction media behavior", () => { ).loadWebMedia; vi.restoreAllMocks(); vi.clearAllMocks(); + channelResolutionMocks.resolveOutboundChannelPlugin.mockReset(); + channelResolutionMocks.resolveOutboundChannelPlugin.mockImplementation( + ({ channel }: { channel: string }) => + getActivePluginRegistry()?.channels.find((entry) => entry?.plugin?.id === channel)?.plugin, + ); + channelResolutionMocks.executeSendAction.mockReset(); + channelResolutionMocks.executeSendAction.mockImplementation( + async ({ + ctx, + to, + message, + mediaUrl, + mediaUrls, + }: { + ctx: { channel: string; dryRun: boolean }; + to: string; + message: string; + mediaUrl?: string; + mediaUrls?: string[]; + }) => ({ + handledBy: "core" as const, + payload: { + channel: ctx.channel, + to, + message, + mediaUrl, + mediaUrls, + dryRun: ctx.dryRun, + }, + sendResult: { + channel: ctx.channel, + messageId: "msg-test", + ...(mediaUrl ? { mediaUrl } : {}), + ...(mediaUrls ? { mediaUrls } : {}), + }, + }), + ); + channelResolutionMocks.executePollAction.mockReset(); + channelResolutionMocks.executePollAction.mockImplementation(async () => { + throw new Error("executePollAction should not run in media tests"); + }); vi.mocked(loadWebMedia).mockReset(); vi.mocked(loadWebMedia).mockImplementation(actualLoadWebMedia); }); diff --git a/src/infra/outbound/message-action-runner.poll.test.ts b/src/infra/outbound/message-action-runner.poll.test.ts index c806c46f855..e726c0b98fa 100644 --- a/src/infra/outbound/message-action-runner.poll.test.ts +++ b/src/infra/outbound/message-action-runner.poll.test.ts @@ -1,23 +1,117 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { ChannelPlugin } from "../../channels/plugins/types.js"; import type { OpenClawConfig } from "../../config/config.js"; -import { setActivePluginRegistry } from "../../plugins/runtime.js"; +import { getActivePluginRegistry, setActivePluginRegistry } from "../../plugins/runtime.js"; import { createTestRegistry } from "../../test-utils/channel-plugins.js"; import { runMessageAction } from "./message-action-runner.js"; const mocks = vi.hoisted(() => ({ executePollAction: vi.fn(), + resolveOutboundChannelPlugin: vi.fn(), })); -vi.mock("./outbound-send-service.js", async () => { - const actual = await vi.importActual( - "./outbound-send-service.js", - ); - return { - ...actual, - executePollAction: mocks.executePollAction, - }; -}); +vi.mock("./channel-resolution.js", () => ({ + resolveOutboundChannelPlugin: mocks.resolveOutboundChannelPlugin, + resetOutboundChannelResolutionStateForTest: vi.fn(), +})); + +vi.mock("./outbound-send-service.js", () => ({ + executeSendAction: vi.fn(async () => { + throw new Error("executeSendAction should not run in poll tests"); + }), + executePollAction: mocks.executePollAction, +})); + +vi.mock("./outbound-session.js", () => ({ + ensureOutboundSessionEntry: vi.fn(async () => undefined), + resolveOutboundSessionRoute: vi.fn(async () => null), +})); + +vi.mock("./message-action-threading.js", () => ({ + resolveAndApplyOutboundThreadId: vi.fn( + ( + actionParams: Record, + context: { + cfg: OpenClawConfig; + to: string; + accountId?: string | null; + toolContext?: Record; + resolveAutoThreadId?: (params: { + cfg: OpenClawConfig; + accountId?: string | null; + to: string; + toolContext?: Record; + replyToId?: string; + }) => string | undefined; + }, + ) => { + const explicit = + typeof actionParams.threadId === "string" ? actionParams.threadId : undefined; + const replyToId = typeof actionParams.replyTo === "string" ? actionParams.replyTo : undefined; + const resolved = + explicit ?? + context.resolveAutoThreadId?.({ + cfg: context.cfg, + accountId: context.accountId, + to: context.to, + toolContext: context.toolContext, + replyToId, + }); + if (resolved && !actionParams.threadId) { + actionParams.threadId = resolved; + } + return resolved ?? undefined; + }, + ), + prepareOutboundMirrorRoute: vi.fn( + async ({ + actionParams, + cfg, + to, + accountId, + toolContext, + agentId, + resolveAutoThreadId, + }: { + actionParams: Record; + cfg: OpenClawConfig; + to: string; + accountId?: string | null; + toolContext?: Record; + agentId?: string; + resolveAutoThreadId?: (params: { + cfg: OpenClawConfig; + accountId?: string | null; + to: string; + toolContext?: Record; + replyToId?: string; + }) => string | undefined; + }) => { + const explicit = + typeof actionParams.threadId === "string" ? actionParams.threadId : undefined; + const replyToId = typeof actionParams.replyTo === "string" ? actionParams.replyTo : undefined; + const resolvedThreadId = + explicit ?? + resolveAutoThreadId?.({ + cfg, + accountId, + to, + toolContext, + replyToId, + }); + if (resolvedThreadId && !actionParams.threadId) { + actionParams.threadId = resolvedThreadId; + } + if (agentId) { + actionParams.__agentId = agentId; + } + return { + resolvedThreadId, + outboundRoute: null, + }; + }, + ), +})); const telegramConfig = { channels: { telegram: { @@ -111,6 +205,11 @@ describe("runMessageAction poll handling", () => { }, ]), ); + mocks.resolveOutboundChannelPlugin.mockReset(); + mocks.resolveOutboundChannelPlugin.mockImplementation( + ({ channel }: { channel: string }) => + getActivePluginRegistry()?.channels.find((entry) => entry?.plugin?.id === channel)?.plugin, + ); mocks.executePollAction.mockReset(); mocks.executePollAction.mockImplementation(async (input) => ({ handledBy: "core",