695 lines
20 KiB
TypeScript
695 lines
20 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { telegramOutbound } from "../../channels/plugins/outbound/telegram.js";
|
|
import type { OpenClawConfig } from "../../config/config.js";
|
|
import { setActivePluginRegistry } from "../../plugins/runtime.js";
|
|
import { createOutboundTestPlugin, createTestRegistry } from "../../test-utils/channel-plugins.js";
|
|
import {
|
|
resolveHeartbeatDeliveryTarget,
|
|
resolveOutboundTarget,
|
|
resolveSessionDeliveryTarget,
|
|
} from "./targets.js";
|
|
import type { SessionDeliveryTarget } from "./targets.js";
|
|
import {
|
|
installResolveOutboundTargetPluginRegistryHooks,
|
|
runResolveOutboundTargetCoreTests,
|
|
} from "./targets.shared-test.js";
|
|
|
|
runResolveOutboundTargetCoreTests();
|
|
|
|
describe("resolveOutboundTarget defaultTo config fallback", () => {
|
|
installResolveOutboundTargetPluginRegistryHooks();
|
|
const whatsappDefaultCfg: OpenClawConfig = {
|
|
channels: { whatsapp: { defaultTo: "+15551234567", allowFrom: ["*"] } },
|
|
};
|
|
|
|
it("uses whatsapp defaultTo when no explicit target is provided", () => {
|
|
const res = resolveOutboundTarget({
|
|
channel: "whatsapp",
|
|
to: undefined,
|
|
cfg: whatsappDefaultCfg,
|
|
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 res = resolveOutboundTarget({
|
|
channel: "whatsapp",
|
|
to: "+15559999999",
|
|
cfg: whatsappDefaultCfg,
|
|
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);
|
|
});
|
|
|
|
it("falls back to the active registry when the cached channel map is stale", () => {
|
|
const registry = createTestRegistry([]);
|
|
setActivePluginRegistry(registry, "stale-registry-test");
|
|
|
|
// Warm the cached channel map before mutating the registry in place.
|
|
expect(resolveOutboundTarget({ channel: "telegram", to: "123", mode: "explicit" }).ok).toBe(
|
|
false,
|
|
);
|
|
|
|
registry.channels.push({
|
|
pluginId: "telegram",
|
|
plugin: createOutboundTestPlugin({ id: "telegram", outbound: telegramOutbound }),
|
|
source: "test",
|
|
});
|
|
|
|
expect(resolveOutboundTarget({ channel: "telegram", to: "123", mode: "explicit" })).toEqual({
|
|
ok: true,
|
|
to: "123",
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("resolveSessionDeliveryTarget", () => {
|
|
const expectImplicitRoute = (
|
|
resolved: SessionDeliveryTarget,
|
|
params: {
|
|
channel?: SessionDeliveryTarget["channel"];
|
|
to?: string;
|
|
lastChannel?: SessionDeliveryTarget["lastChannel"];
|
|
lastTo?: string;
|
|
},
|
|
) => {
|
|
expect(resolved).toEqual({
|
|
channel: params.channel,
|
|
to: params.to,
|
|
accountId: undefined,
|
|
threadId: undefined,
|
|
threadIdExplicit: false,
|
|
mode: "implicit",
|
|
lastChannel: params.lastChannel,
|
|
lastTo: params.lastTo,
|
|
lastAccountId: undefined,
|
|
lastThreadId: undefined,
|
|
});
|
|
};
|
|
|
|
const expectTopicParsedFromExplicitTo = (
|
|
entry: Parameters<typeof resolveSessionDeliveryTarget>[0]["entry"],
|
|
) => {
|
|
const resolved = resolveSessionDeliveryTarget({
|
|
entry,
|
|
requestedChannel: "last",
|
|
explicitTo: "63448508:topic:1008013",
|
|
});
|
|
expect(resolved.to).toBe("63448508");
|
|
expect(resolved.threadId).toBe(1008013);
|
|
};
|
|
|
|
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",
|
|
});
|
|
|
|
expectImplicitRoute(resolved, {
|
|
channel: "telegram",
|
|
to: undefined,
|
|
lastChannel: "whatsapp",
|
|
lastTo: "+1555",
|
|
});
|
|
});
|
|
|
|
it("allows mismatched lastTo when configured", () => {
|
|
const resolved = resolveSessionDeliveryTarget({
|
|
entry: {
|
|
sessionId: "sess-3",
|
|
updatedAt: 1,
|
|
lastChannel: "whatsapp",
|
|
lastTo: "+1555",
|
|
},
|
|
requestedChannel: "telegram",
|
|
allowMismatchedLastTo: true,
|
|
});
|
|
|
|
expectImplicitRoute(resolved, {
|
|
channel: "telegram",
|
|
to: "+1555",
|
|
lastChannel: "whatsapp",
|
|
lastTo: "+1555",
|
|
});
|
|
});
|
|
|
|
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("does not inherit lastThreadId in heartbeat mode", () => {
|
|
const resolved = resolveSessionDeliveryTarget({
|
|
entry: {
|
|
sessionId: "sess-heartbeat-thread",
|
|
updatedAt: 1,
|
|
lastChannel: "slack",
|
|
lastTo: "user:U123",
|
|
lastThreadId: "1739142736.000100",
|
|
},
|
|
requestedChannel: "last",
|
|
mode: "heartbeat",
|
|
});
|
|
|
|
expect(resolved.threadId).toBeUndefined();
|
|
});
|
|
|
|
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",
|
|
});
|
|
|
|
expectImplicitRoute(resolved, {
|
|
channel: "slack",
|
|
to: undefined,
|
|
lastChannel: "whatsapp",
|
|
lastTo: "+1555",
|
|
});
|
|
});
|
|
|
|
it("parses :topic:NNN from explicitTo into threadId", () => {
|
|
expectTopicParsedFromExplicitTo({
|
|
sessionId: "sess-topic",
|
|
updatedAt: 1,
|
|
lastChannel: "telegram",
|
|
lastTo: "63448508",
|
|
});
|
|
});
|
|
|
|
it("parses :topic:NNN even when lastTo is absent", () => {
|
|
expectTopicParsedFromExplicitTo({
|
|
sessionId: "sess-no-last",
|
|
updatedAt: 1,
|
|
lastChannel: "telegram",
|
|
});
|
|
});
|
|
|
|
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");
|
|
});
|
|
|
|
const resolveHeartbeatTarget = (
|
|
entry: Parameters<typeof resolveHeartbeatDeliveryTarget>[0]["entry"],
|
|
directPolicy?: "allow" | "block",
|
|
) =>
|
|
resolveHeartbeatDeliveryTarget({
|
|
cfg: {},
|
|
entry,
|
|
heartbeat: {
|
|
target: "last",
|
|
...(directPolicy ? { directPolicy } : {}),
|
|
},
|
|
});
|
|
|
|
const expectHeartbeatTarget = (params: {
|
|
name: string;
|
|
entry: Parameters<typeof resolveHeartbeatDeliveryTarget>[0]["entry"];
|
|
directPolicy?: "allow" | "block";
|
|
expectedChannel: string;
|
|
expectedTo?: string;
|
|
expectedReason?: string;
|
|
expectedThreadId?: string | number;
|
|
}) => {
|
|
const resolved = resolveHeartbeatTarget(params.entry, params.directPolicy);
|
|
expect(resolved.channel, params.name).toBe(params.expectedChannel);
|
|
expect(resolved.to, params.name).toBe(params.expectedTo);
|
|
expect(resolved.reason, params.name).toBe(params.expectedReason);
|
|
expect(resolved.threadId, params.name).toBe(params.expectedThreadId);
|
|
};
|
|
|
|
const heartbeatTargetCases = [
|
|
{
|
|
name: "allows heartbeat delivery to Slack DMs by default and drops inherited thread ids",
|
|
entry: {
|
|
sessionId: "sess-heartbeat-slack-direct",
|
|
updatedAt: 1,
|
|
lastChannel: "slack",
|
|
lastTo: "user:U123",
|
|
lastThreadId: "1739142736.000100",
|
|
},
|
|
expectedChannel: "slack",
|
|
expectedTo: "user:U123",
|
|
},
|
|
{
|
|
name: "blocks heartbeat delivery to Slack DMs when directPolicy is block",
|
|
entry: {
|
|
sessionId: "sess-heartbeat-slack-direct-blocked",
|
|
updatedAt: 1,
|
|
lastChannel: "slack",
|
|
lastTo: "user:U123",
|
|
lastThreadId: "1739142736.000100",
|
|
},
|
|
directPolicy: "block" as const,
|
|
expectedChannel: "none",
|
|
expectedReason: "dm-blocked",
|
|
},
|
|
{
|
|
name: "allows heartbeat delivery to Telegram direct chats by default",
|
|
entry: {
|
|
sessionId: "sess-heartbeat-telegram-direct",
|
|
updatedAt: 1,
|
|
lastChannel: "telegram",
|
|
lastTo: "5232990709",
|
|
},
|
|
expectedChannel: "telegram",
|
|
expectedTo: "5232990709",
|
|
},
|
|
{
|
|
name: "blocks heartbeat delivery to Telegram direct chats when directPolicy is block",
|
|
entry: {
|
|
sessionId: "sess-heartbeat-telegram-direct-blocked",
|
|
updatedAt: 1,
|
|
lastChannel: "telegram",
|
|
lastTo: "5232990709",
|
|
},
|
|
directPolicy: "block" as const,
|
|
expectedChannel: "none",
|
|
expectedReason: "dm-blocked",
|
|
},
|
|
{
|
|
name: "keeps heartbeat delivery to Telegram groups",
|
|
entry: {
|
|
sessionId: "sess-heartbeat-telegram-group",
|
|
updatedAt: 1,
|
|
lastChannel: "telegram",
|
|
lastTo: "-1001234567890",
|
|
},
|
|
expectedChannel: "telegram",
|
|
expectedTo: "-1001234567890",
|
|
},
|
|
{
|
|
name: "allows heartbeat delivery to WhatsApp direct chats by default",
|
|
entry: {
|
|
sessionId: "sess-heartbeat-whatsapp-direct",
|
|
updatedAt: 1,
|
|
lastChannel: "whatsapp",
|
|
lastTo: "+15551234567",
|
|
},
|
|
expectedChannel: "whatsapp",
|
|
expectedTo: "+15551234567",
|
|
},
|
|
{
|
|
name: "keeps heartbeat delivery to WhatsApp groups",
|
|
entry: {
|
|
sessionId: "sess-heartbeat-whatsapp-group",
|
|
updatedAt: 1,
|
|
lastChannel: "whatsapp",
|
|
lastTo: "120363140186826074@g.us",
|
|
},
|
|
expectedChannel: "whatsapp",
|
|
expectedTo: "120363140186826074@g.us",
|
|
},
|
|
{
|
|
name: "uses session chatType hints when target parsing cannot classify a direct chat",
|
|
entry: {
|
|
sessionId: "sess-heartbeat-imessage-direct",
|
|
updatedAt: 1,
|
|
lastChannel: "imessage",
|
|
lastTo: "chat-guid-unknown-shape",
|
|
chatType: "direct",
|
|
},
|
|
expectedChannel: "imessage",
|
|
expectedTo: "chat-guid-unknown-shape",
|
|
},
|
|
{
|
|
name: "blocks session chatType direct hints when directPolicy is block",
|
|
entry: {
|
|
sessionId: "sess-heartbeat-imessage-direct-blocked",
|
|
updatedAt: 1,
|
|
lastChannel: "imessage",
|
|
lastTo: "chat-guid-unknown-shape",
|
|
chatType: "direct",
|
|
},
|
|
directPolicy: "block" as const,
|
|
expectedChannel: "none",
|
|
expectedReason: "dm-blocked",
|
|
},
|
|
] satisfies Array<{
|
|
name: string;
|
|
entry: Parameters<typeof resolveHeartbeatDeliveryTarget>[0]["entry"];
|
|
directPolicy?: "allow" | "block";
|
|
expectedChannel: string;
|
|
expectedTo?: string;
|
|
expectedReason?: string;
|
|
}>;
|
|
|
|
it.each(heartbeatTargetCases)(
|
|
"$name",
|
|
({ name, entry, directPolicy, expectedChannel, expectedTo, expectedReason }) => {
|
|
expectHeartbeatTarget({
|
|
name,
|
|
entry,
|
|
directPolicy,
|
|
expectedChannel,
|
|
expectedTo,
|
|
expectedReason,
|
|
});
|
|
},
|
|
);
|
|
|
|
it("allows heartbeat delivery to Discord DMs by default", () => {
|
|
const cfg: OpenClawConfig = {};
|
|
const resolved = resolveHeartbeatDeliveryTarget({
|
|
cfg,
|
|
entry: {
|
|
sessionId: "sess-heartbeat-discord-dm",
|
|
updatedAt: 1,
|
|
lastChannel: "discord",
|
|
lastTo: "user:12345",
|
|
},
|
|
heartbeat: {
|
|
target: "last",
|
|
},
|
|
});
|
|
|
|
expect(resolved.channel).toBe("discord");
|
|
expect(resolved.to).toBe("user:12345");
|
|
});
|
|
|
|
it("keeps heartbeat delivery to Discord channels", () => {
|
|
const cfg: OpenClawConfig = {};
|
|
const resolved = resolveHeartbeatDeliveryTarget({
|
|
cfg,
|
|
entry: {
|
|
sessionId: "sess-heartbeat-discord-channel",
|
|
updatedAt: 1,
|
|
lastChannel: "discord",
|
|
lastTo: "channel:999",
|
|
},
|
|
heartbeat: {
|
|
target: "last",
|
|
},
|
|
});
|
|
|
|
expect(resolved.channel).toBe("discord");
|
|
expect(resolved.to).toBe("channel:999");
|
|
});
|
|
|
|
it("keeps explicit threadId in heartbeat mode", () => {
|
|
const resolved = resolveSessionDeliveryTarget({
|
|
entry: {
|
|
sessionId: "sess-heartbeat-explicit-thread",
|
|
updatedAt: 1,
|
|
lastChannel: "telegram",
|
|
lastTo: "-100123",
|
|
lastThreadId: 999,
|
|
},
|
|
requestedChannel: "last",
|
|
mode: "heartbeat",
|
|
explicitThreadId: 42,
|
|
});
|
|
|
|
expect(resolved.channel).toBe("telegram");
|
|
expect(resolved.to).toBe("-100123");
|
|
expect(resolved.threadId).toBe(42);
|
|
expect(resolved.threadIdExplicit).toBe(true);
|
|
});
|
|
|
|
it("parses explicit heartbeat topic targets into threadId", () => {
|
|
const cfg: OpenClawConfig = {};
|
|
const resolved = resolveHeartbeatDeliveryTarget({
|
|
cfg,
|
|
heartbeat: {
|
|
target: "telegram",
|
|
to: "-10063448508:topic:1008013",
|
|
},
|
|
});
|
|
|
|
expect(resolved.channel).toBe("telegram");
|
|
expect(resolved.to).toBe("-10063448508");
|
|
expect(resolved.threadId).toBe(1008013);
|
|
});
|
|
});
|
|
|
|
describe("resolveSessionDeliveryTarget — cross-channel reply guard (#24152)", () => {
|
|
it("uses turnSourceChannel over session lastChannel when provided", () => {
|
|
// Simulate: WhatsApp message originated the turn, but a Slack message
|
|
// arrived concurrently and updated lastChannel to "slack"
|
|
const resolved = resolveSessionDeliveryTarget({
|
|
entry: {
|
|
sessionId: "sess-shared",
|
|
updatedAt: 1,
|
|
lastChannel: "slack", // <- concurrently overwritten
|
|
lastTo: "U0AEMECNCBV", // <- Slack user (wrong target)
|
|
},
|
|
requestedChannel: "last",
|
|
turnSourceChannel: "whatsapp", // <- originated from WhatsApp
|
|
turnSourceTo: "+66972796305", // <- WhatsApp user (correct target)
|
|
});
|
|
|
|
expect(resolved.channel).toBe("whatsapp");
|
|
expect(resolved.to).toBe("+66972796305");
|
|
});
|
|
|
|
it("falls back to session lastChannel when turnSourceChannel is not set", () => {
|
|
const resolved = resolveSessionDeliveryTarget({
|
|
entry: {
|
|
sessionId: "sess-normal",
|
|
updatedAt: 1,
|
|
lastChannel: "telegram",
|
|
lastTo: "8587265585",
|
|
},
|
|
requestedChannel: "last",
|
|
});
|
|
|
|
expect(resolved.channel).toBe("telegram");
|
|
expect(resolved.to).toBe("8587265585");
|
|
});
|
|
|
|
it("respects explicit requestedChannel over turnSourceChannel", () => {
|
|
const resolved = resolveSessionDeliveryTarget({
|
|
entry: {
|
|
sessionId: "sess-explicit",
|
|
updatedAt: 1,
|
|
lastChannel: "slack",
|
|
lastTo: "U12345",
|
|
},
|
|
requestedChannel: "telegram",
|
|
explicitTo: "8587265585",
|
|
turnSourceChannel: "whatsapp",
|
|
turnSourceTo: "+66972796305",
|
|
});
|
|
|
|
// Explicit requestedChannel "telegram" is not "last", so it takes priority
|
|
expect(resolved.channel).toBe("telegram");
|
|
});
|
|
|
|
it("preserves turnSourceAccountId and turnSourceThreadId", () => {
|
|
const resolved = resolveSessionDeliveryTarget({
|
|
entry: {
|
|
sessionId: "sess-meta",
|
|
updatedAt: 1,
|
|
lastChannel: "slack",
|
|
lastTo: "U_WRONG",
|
|
lastAccountId: "wrong-account",
|
|
},
|
|
requestedChannel: "last",
|
|
turnSourceChannel: "telegram",
|
|
turnSourceTo: "8587265585",
|
|
turnSourceAccountId: "bot-123",
|
|
turnSourceThreadId: 42,
|
|
});
|
|
|
|
expect(resolved.channel).toBe("telegram");
|
|
expect(resolved.to).toBe("8587265585");
|
|
expect(resolved.accountId).toBe("bot-123");
|
|
expect(resolved.threadId).toBe(42);
|
|
});
|
|
|
|
it("does not fall back to session target metadata when turnSourceChannel is set", () => {
|
|
const resolved = resolveSessionDeliveryTarget({
|
|
entry: {
|
|
sessionId: "sess-no-fallback",
|
|
updatedAt: 1,
|
|
lastChannel: "slack",
|
|
lastTo: "U_WRONG",
|
|
lastAccountId: "wrong-account",
|
|
lastThreadId: "1739142736.000100",
|
|
},
|
|
requestedChannel: "last",
|
|
turnSourceChannel: "whatsapp",
|
|
});
|
|
|
|
expect(resolved.channel).toBe("whatsapp");
|
|
expect(resolved.to).toBeUndefined();
|
|
expect(resolved.accountId).toBeUndefined();
|
|
expect(resolved.threadId).toBeUndefined();
|
|
expect(resolved.lastTo).toBeUndefined();
|
|
expect(resolved.lastAccountId).toBeUndefined();
|
|
expect(resolved.lastThreadId).toBeUndefined();
|
|
});
|
|
|
|
it("uses explicitTo even when turnSourceTo is omitted", () => {
|
|
const resolved = resolveSessionDeliveryTarget({
|
|
entry: {
|
|
sessionId: "sess-explicit-to",
|
|
updatedAt: 1,
|
|
lastChannel: "slack",
|
|
lastTo: "U_WRONG",
|
|
},
|
|
requestedChannel: "last",
|
|
explicitTo: "+15551234567",
|
|
turnSourceChannel: "whatsapp",
|
|
});
|
|
|
|
expect(resolved.channel).toBe("whatsapp");
|
|
expect(resolved.to).toBe("+15551234567");
|
|
});
|
|
|
|
it("still allows mismatched lastTo only from turn-scoped metadata", () => {
|
|
const resolved = resolveSessionDeliveryTarget({
|
|
entry: {
|
|
sessionId: "sess-mismatch-turn",
|
|
updatedAt: 1,
|
|
lastChannel: "slack",
|
|
lastTo: "U_WRONG",
|
|
},
|
|
requestedChannel: "telegram",
|
|
allowMismatchedLastTo: true,
|
|
turnSourceChannel: "whatsapp",
|
|
turnSourceTo: "+15550000000",
|
|
});
|
|
|
|
expect(resolved.channel).toBe("telegram");
|
|
expect(resolved.to).toBe("+15550000000");
|
|
});
|
|
});
|