Files
openclaw/src/infra/outbound/targets.test.ts
Kirill Shchetynin ee519086f6 Feature/default messenger delivery target (openclaw#16985) thanks @KirillShchetinin
Verified:
- pnpm build
- pnpm check
- pnpm test:macmini

Co-authored-by: KirillShchetinin <13061871+KirillShchetinin@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
2026-02-19 22:37:19 -06:00

383 lines
11 KiB
TypeScript

import { beforeEach, describe, expect, it } from "vitest";
import { telegramPlugin } from "../../../extensions/telegram/src/channel.js";
import { whatsappPlugin } from "../../../extensions/whatsapp/src/channel.js";
import type { OpenClawConfig } from "../../config/config.js";
import { setActivePluginRegistry } from "../../plugins/runtime.js";
import { createTestRegistry } from "../../test-utils/channel-plugins.js";
import { resolveOutboundTarget, resolveSessionDeliveryTarget } from "./targets.js";
describe("resolveOutboundTarget", () => {
beforeEach(() => {
setActivePluginRegistry(
createTestRegistry([
{ pluginId: "whatsapp", plugin: whatsappPlugin, source: "test" },
{ pluginId: "telegram", plugin: telegramPlugin, source: "test" },
]),
);
});
it("rejects whatsapp with empty target even when allowFrom configured", () => {
const cfg: OpenClawConfig = {
channels: { whatsapp: { allowFrom: ["+1555"] } },
};
const res = resolveOutboundTarget({
channel: "whatsapp",
to: "",
cfg,
mode: "explicit",
});
expect(res.ok).toBe(false);
if (!res.ok) {
expect(res.error.message).toContain("WhatsApp");
}
});
it.each([
{
name: "normalizes whatsapp target when provided",
input: { channel: "whatsapp" as const, to: " (555) 123-4567 " },
expected: { ok: true as const, to: "+5551234567" },
},
{
name: "keeps whatsapp group targets",
input: { channel: "whatsapp" as const, to: "120363401234567890@g.us" },
expected: { ok: true as const, to: "120363401234567890@g.us" },
},
{
name: "normalizes prefixed/uppercase whatsapp group targets",
input: {
channel: "whatsapp" as const,
to: " WhatsApp:120363401234567890@G.US ",
},
expected: { ok: true as const, to: "120363401234567890@g.us" },
},
{
name: "rejects whatsapp with empty target and allowFrom (no silent fallback)",
input: { channel: "whatsapp" as const, to: "", allowFrom: ["+1555"] },
expectedErrorIncludes: "WhatsApp",
},
{
name: "rejects whatsapp with empty target and prefixed allowFrom (no silent fallback)",
input: {
channel: "whatsapp" as const,
to: "",
allowFrom: ["whatsapp:(555) 123-4567"],
},
expectedErrorIncludes: "WhatsApp",
},
{
name: "rejects invalid whatsapp target",
input: { channel: "whatsapp" as const, to: "wat" },
expectedErrorIncludes: "WhatsApp",
},
{
name: "rejects whatsapp without to when allowFrom missing",
input: { channel: "whatsapp" as const, to: " " },
expectedErrorIncludes: "WhatsApp",
},
{
name: "rejects whatsapp allowFrom fallback when invalid",
input: { channel: "whatsapp" as const, to: "", allowFrom: ["wat"] },
expectedErrorIncludes: "WhatsApp",
},
])("$name", ({ input, expected, expectedErrorIncludes }) => {
const res = resolveOutboundTarget(input);
if (expected) {
expect(res).toEqual(expected);
return;
}
expect(res.ok).toBe(false);
if (!res.ok) {
expect(res.error.message).toContain(expectedErrorIncludes);
}
});
it("rejects telegram with missing target", () => {
const res = resolveOutboundTarget({ channel: "telegram", to: " " });
expect(res.ok).toBe(false);
if (!res.ok) {
expect(res.error.message).toContain("Telegram");
}
});
it("rejects webchat delivery", () => {
const res = resolveOutboundTarget({ channel: "webchat", to: "x" });
expect(res.ok).toBe(false);
if (!res.ok) {
expect(res.error.message).toContain("WebChat");
}
});
describe("defaultTo config fallback", () => {
it("uses whatsapp defaultTo when no explicit target is provided", () => {
const cfg: OpenClawConfig = {
channels: { whatsapp: { defaultTo: "+15551234567", allowFrom: ["*"] } },
};
const res = resolveOutboundTarget({
channel: "whatsapp",
to: undefined,
cfg,
mode: "implicit",
});
expect(res).toEqual({ ok: true, to: "+15551234567" });
});
it("uses telegram defaultTo when no explicit target is provided", () => {
const cfg: OpenClawConfig = {
channels: { telegram: { defaultTo: "123456789" } },
};
const res = resolveOutboundTarget({
channel: "telegram",
to: "",
cfg,
mode: "implicit",
});
expect(res).toEqual({ ok: true, to: "123456789" });
});
it("explicit --reply-to overrides defaultTo", () => {
const cfg: OpenClawConfig = {
channels: { whatsapp: { defaultTo: "+15551234567", allowFrom: ["*"] } },
};
const res = resolveOutboundTarget({
channel: "whatsapp",
to: "+15559999999",
cfg,
mode: "explicit",
});
expect(res).toEqual({ ok: true, to: "+15559999999" });
});
it("still errors when no defaultTo and no explicit target", () => {
const cfg: OpenClawConfig = {
channels: { whatsapp: { allowFrom: ["+1555"] } },
};
const res = resolveOutboundTarget({
channel: "whatsapp",
to: "",
cfg,
mode: "implicit",
});
expect(res.ok).toBe(false);
});
});
});
describe("resolveSessionDeliveryTarget", () => {
it("derives implicit delivery from the last route", () => {
const resolved = resolveSessionDeliveryTarget({
entry: {
sessionId: "sess-1",
updatedAt: 1,
lastChannel: " whatsapp ",
lastTo: " +1555 ",
lastAccountId: " acct-1 ",
},
requestedChannel: "last",
});
expect(resolved).toEqual({
channel: "whatsapp",
to: "+1555",
accountId: "acct-1",
threadId: undefined,
threadIdExplicit: false,
mode: "implicit",
lastChannel: "whatsapp",
lastTo: "+1555",
lastAccountId: "acct-1",
lastThreadId: undefined,
});
});
it("prefers explicit targets without reusing lastTo", () => {
const resolved = resolveSessionDeliveryTarget({
entry: {
sessionId: "sess-2",
updatedAt: 1,
lastChannel: "whatsapp",
lastTo: "+1555",
},
requestedChannel: "telegram",
});
expect(resolved).toEqual({
channel: "telegram",
to: undefined,
accountId: undefined,
threadId: undefined,
threadIdExplicit: false,
mode: "implicit",
lastChannel: "whatsapp",
lastTo: "+1555",
lastAccountId: undefined,
lastThreadId: undefined,
});
});
it("allows mismatched lastTo when configured", () => {
const resolved = resolveSessionDeliveryTarget({
entry: {
sessionId: "sess-3",
updatedAt: 1,
lastChannel: "whatsapp",
lastTo: "+1555",
},
requestedChannel: "telegram",
allowMismatchedLastTo: true,
});
expect(resolved).toEqual({
channel: "telegram",
to: "+1555",
accountId: undefined,
threadId: undefined,
threadIdExplicit: false,
mode: "implicit",
lastChannel: "whatsapp",
lastTo: "+1555",
lastAccountId: undefined,
lastThreadId: undefined,
});
});
it("passes through explicitThreadId when provided", () => {
const resolved = resolveSessionDeliveryTarget({
entry: {
sessionId: "sess-thread",
updatedAt: 1,
lastChannel: "telegram",
lastTo: "-100123",
lastThreadId: 999,
},
requestedChannel: "last",
explicitThreadId: 42,
});
expect(resolved.threadId).toBe(42);
expect(resolved.channel).toBe("telegram");
expect(resolved.to).toBe("-100123");
});
it("uses session lastThreadId when no explicitThreadId", () => {
const resolved = resolveSessionDeliveryTarget({
entry: {
sessionId: "sess-thread-2",
updatedAt: 1,
lastChannel: "telegram",
lastTo: "-100123",
lastThreadId: 999,
},
requestedChannel: "last",
});
expect(resolved.threadId).toBe(999);
});
it("falls back to a provided channel when requested is unsupported", () => {
const resolved = resolveSessionDeliveryTarget({
entry: {
sessionId: "sess-4",
updatedAt: 1,
lastChannel: "whatsapp",
lastTo: "+1555",
},
requestedChannel: "webchat",
fallbackChannel: "slack",
});
expect(resolved).toEqual({
channel: "slack",
to: undefined,
accountId: undefined,
threadId: undefined,
threadIdExplicit: false,
mode: "implicit",
lastChannel: "whatsapp",
lastTo: "+1555",
lastAccountId: undefined,
lastThreadId: undefined,
});
});
it("parses :topic:NNN from explicitTo into threadId", () => {
const resolved = resolveSessionDeliveryTarget({
entry: {
sessionId: "sess-topic",
updatedAt: 1,
lastChannel: "telegram",
lastTo: "63448508",
},
requestedChannel: "last",
explicitTo: "63448508:topic:1008013",
});
expect(resolved.to).toBe("63448508");
expect(resolved.threadId).toBe(1008013);
});
it("parses :topic:NNN even when lastTo is absent", () => {
const resolved = resolveSessionDeliveryTarget({
entry: {
sessionId: "sess-no-last",
updatedAt: 1,
lastChannel: "telegram",
},
requestedChannel: "last",
explicitTo: "63448508:topic:1008013",
});
expect(resolved.to).toBe("63448508");
expect(resolved.threadId).toBe(1008013);
});
it("skips :topic: parsing for non-telegram channels", () => {
const resolved = resolveSessionDeliveryTarget({
entry: {
sessionId: "sess-slack",
updatedAt: 1,
lastChannel: "slack",
lastTo: "C12345",
},
requestedChannel: "last",
explicitTo: "C12345:topic:999",
});
expect(resolved.to).toBe("C12345:topic:999");
expect(resolved.threadId).toBeUndefined();
});
it("skips :topic: parsing when channel is explicitly non-telegram even if lastChannel was telegram", () => {
const resolved = resolveSessionDeliveryTarget({
entry: {
sessionId: "sess-cross",
updatedAt: 1,
lastChannel: "telegram",
lastTo: "63448508",
},
requestedChannel: "slack",
explicitTo: "C12345:topic:999",
});
expect(resolved.to).toBe("C12345:topic:999");
expect(resolved.threadId).toBeUndefined();
});
it("explicitThreadId takes priority over :topic: parsed value", () => {
const resolved = resolveSessionDeliveryTarget({
entry: {
sessionId: "sess-priority",
updatedAt: 1,
lastChannel: "telegram",
lastTo: "63448508",
},
requestedChannel: "last",
explicitTo: "63448508:topic:1008013",
explicitThreadId: 42,
});
expect(resolved.threadId).toBe(42);
expect(resolved.to).toBe("63448508");
});
});