Files
openclaw/src/cron/isolated-agent.direct-delivery-core-channels.test.ts
2026-04-07 08:18:44 +08:00

212 lines
6.1 KiB
TypeScript

import "./isolated-agent.mocks.js";
import { beforeEach, describe, expect, it } from "vitest";
import { runSubagentAnnounceFlow } from "../agents/subagent-announce.js";
import type { ChannelOutboundAdapter, ChannelOutboundContext } from "../channels/plugins/types.js";
import type { CliDeps } from "../cli/deps.js";
import { resolveOutboundSendDep } from "../infra/outbound/send-deps.js";
import { createWhatsAppTestPlugin } from "../infra/outbound/targets.test-helpers.js";
import { setActivePluginRegistry } from "../plugins/runtime.js";
import { createOutboundTestPlugin, createTestRegistry } from "../test-utils/channel-plugins.js";
import { createCliDeps, mockAgentPayloads } from "./isolated-agent.delivery.test-helpers.js";
import { runCronIsolatedAgentTurn } from "./isolated-agent.js";
import {
makeCfg,
makeJob,
withTempCronHome,
writeSessionStore,
} from "./isolated-agent.test-harness.js";
import { setupIsolatedAgentTurnMocks } from "./isolated-agent.test-setup.js";
type ChannelCase = {
name: string;
channel: "slack" | "discord" | "whatsapp" | "imessage";
to: string;
sendKey: keyof Pick<
CliDeps,
"sendMessageSlack" | "sendMessageDiscord" | "sendMessageWhatsApp" | "sendMessageIMessage"
>;
expectedTo: string;
};
const CASES: ChannelCase[] = [
{
name: "Slack",
channel: "slack",
to: "channel:C12345",
sendKey: "sendMessageSlack",
expectedTo: "channel:C12345",
},
{
name: "Discord",
channel: "discord",
to: "channel:789",
sendKey: "sendMessageDiscord",
expectedTo: "channel:789",
},
{
name: "WhatsApp",
channel: "whatsapp",
to: "+15551234567",
sendKey: "sendMessageWhatsApp",
expectedTo: "+15551234567",
},
{
name: "iMessage",
channel: "imessage",
to: "friend@example.com",
sendKey: "sendMessageIMessage",
expectedTo: "friend@example.com",
},
];
async function runExplicitAnnounceTurn(params: {
home: string;
storePath: string;
deps: CliDeps;
channel: ChannelCase["channel"];
to: string;
}) {
return await runCronIsolatedAgentTurn({
cfg: makeCfg(params.home, params.storePath),
deps: params.deps,
job: {
...makeJob({ kind: "agentTurn", message: "do it" }),
delivery: {
mode: "announce",
channel: params.channel,
to: params.to,
},
},
message: "do it",
sessionKey: "cron:job-1",
lane: "cron",
});
}
type CoreChannel = ChannelCase["channel"];
type TestSendFn = (
to: string,
text: string,
options?: Record<string, unknown>,
) => Promise<{ messageId?: string } & Record<string, unknown>>;
function withRequiredMessageId(channel: CoreChannel, result: Awaited<ReturnType<TestSendFn>>) {
return {
channel,
...result,
messageId:
typeof result.messageId === "string" && result.messageId.trim()
? result.messageId
: `${channel}-test-message`,
};
}
function resolveCoreChannelSender(
channel: CoreChannel,
deps: ChannelOutboundContext["deps"],
): TestSendFn {
const sender = resolveOutboundSendDep<TestSendFn>(deps, channel);
if (!sender) {
throw new Error(`missing ${channel} sender`);
}
return sender;
}
function createCliDelegatingOutbound(params: {
channel: CoreChannel;
deliveryMode?: ChannelOutboundAdapter["deliveryMode"];
resolveTarget?: ChannelOutboundAdapter["resolveTarget"];
}): ChannelOutboundAdapter {
return {
deliveryMode: params.deliveryMode ?? "direct",
...(params.resolveTarget ? { resolveTarget: params.resolveTarget } : {}),
sendText: async ({ cfg, to, text, accountId, deps }) =>
withRequiredMessageId(
params.channel,
await resolveCoreChannelSender(params.channel, deps)(to, text, {
cfg,
accountId: accountId ?? undefined,
}),
),
};
}
const whatsappResolveTarget = createWhatsAppTestPlugin().outbound?.resolveTarget;
describe("runCronIsolatedAgentTurn core-channel direct delivery", () => {
beforeEach(() => {
setupIsolatedAgentTurnMocks({ fast: true });
setActivePluginRegistry(
createTestRegistry([
{
pluginId: "slack",
plugin: createOutboundTestPlugin({
id: "slack",
outbound: createCliDelegatingOutbound({ channel: "slack" }),
}),
source: "test",
},
{
pluginId: "discord",
plugin: createOutboundTestPlugin({
id: "discord",
outbound: createCliDelegatingOutbound({ channel: "discord" }),
}),
source: "test",
},
{
pluginId: "whatsapp",
plugin: createOutboundTestPlugin({
id: "whatsapp",
outbound: createCliDelegatingOutbound({
channel: "whatsapp",
deliveryMode: "gateway",
resolveTarget: whatsappResolveTarget,
}),
}),
source: "test",
},
{
pluginId: "imessage",
plugin: createOutboundTestPlugin({
id: "imessage",
outbound: createCliDelegatingOutbound({ channel: "imessage" }),
}),
source: "test",
},
]),
);
});
for (const testCase of CASES) {
it(`routes ${testCase.name} text-only announce delivery through the outbound adapter`, async () => {
await withTempCronHome(async (home) => {
const storePath = await writeSessionStore(home, { lastProvider: "webchat", lastTo: "" });
const deps = createCliDeps();
mockAgentPayloads([{ text: "hello from cron" }]);
const res = await runExplicitAnnounceTurn({
home,
storePath,
deps,
channel: testCase.channel,
to: testCase.to,
});
expect(res.status).toBe("ok");
expect(res.delivered).toBe(true);
expect(res.deliveryAttempted).toBe(true);
expect(runSubagentAnnounceFlow).not.toHaveBeenCalled();
const sendFn = deps[testCase.sendKey];
expect(sendFn).toHaveBeenCalledTimes(1);
expect(sendFn).toHaveBeenCalledWith(
testCase.expectedTo,
"hello from cron",
expect.any(Object),
);
});
});
}
});