From d537904abb52c4e99ca6697bcf3a8d3c6d6fd136 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 13 Mar 2026 20:54:31 +0000 Subject: [PATCH] test: extract outbound session route coverage --- src/infra/outbound/outbound-session.test.ts | 259 ++++++++++++++++++++ src/infra/outbound/outbound.test.ts | 258 ------------------- 2 files changed, 259 insertions(+), 258 deletions(-) create mode 100644 src/infra/outbound/outbound-session.test.ts diff --git a/src/infra/outbound/outbound-session.test.ts b/src/infra/outbound/outbound-session.test.ts new file mode 100644 index 00000000000..17367f4a128 --- /dev/null +++ b/src/infra/outbound/outbound-session.test.ts @@ -0,0 +1,259 @@ +import { describe, expect, it } from "vitest"; +import type { OpenClawConfig } from "../../config/config.js"; +import { resolveOutboundSessionRoute } from "./outbound-session.js"; + +describe("resolveOutboundSessionRoute", () => { + const baseConfig = {} as OpenClawConfig; + + it("resolves provider-specific session routes", async () => { + const perChannelPeerCfg = { session: { dmScope: "per-channel-peer" } } as OpenClawConfig; + const identityLinksCfg = { + session: { + dmScope: "per-peer", + identityLinks: { + alice: ["discord:123"], + }, + }, + } as OpenClawConfig; + const slackMpimCfg = { + channels: { + slack: { + dm: { + groupChannels: ["G123"], + }, + }, + }, + } as OpenClawConfig; + const cases: Array<{ + name: string; + cfg: OpenClawConfig; + channel: string; + target: string; + replyToId?: string; + threadId?: string; + expected: { + sessionKey: string; + from?: string; + to?: string; + threadId?: string | number; + chatType?: "direct" | "group"; + }; + }> = [ + { + name: "Slack thread", + cfg: baseConfig, + channel: "slack", + target: "channel:C123", + replyToId: "456", + expected: { + sessionKey: "agent:main:slack:channel:c123:thread:456", + from: "slack:channel:C123", + to: "channel:C123", + threadId: "456", + }, + }, + { + name: "Telegram topic group", + cfg: baseConfig, + channel: "telegram", + target: "-100123456:topic:42", + expected: { + sessionKey: "agent:main:telegram:group:-100123456:topic:42", + from: "telegram:group:-100123456:topic:42", + to: "telegram:-100123456", + threadId: 42, + }, + }, + { + name: "Telegram DM with topic", + cfg: perChannelPeerCfg, + channel: "telegram", + target: "123456789:topic:99", + expected: { + sessionKey: "agent:main:telegram:direct:123456789:thread:99", + from: "telegram:123456789:topic:99", + to: "telegram:123456789", + threadId: 99, + chatType: "direct", + }, + }, + { + name: "Telegram unresolved username DM", + cfg: perChannelPeerCfg, + channel: "telegram", + target: "@alice", + expected: { + sessionKey: "agent:main:telegram:direct:@alice", + chatType: "direct", + }, + }, + { + name: "Telegram DM scoped threadId fallback", + cfg: perChannelPeerCfg, + channel: "telegram", + target: "12345", + threadId: "12345:99", + expected: { + sessionKey: "agent:main:telegram:direct:12345:thread:99", + from: "telegram:12345:topic:99", + to: "telegram:12345", + threadId: 99, + chatType: "direct", + }, + }, + { + name: "identity-links per-peer", + cfg: identityLinksCfg, + channel: "discord", + target: "user:123", + expected: { + sessionKey: "agent:main:direct:alice", + }, + }, + { + name: "BlueBubbles chat_* prefix stripping", + cfg: baseConfig, + channel: "bluebubbles", + target: "chat_guid:ABC123", + expected: { + sessionKey: "agent:main:bluebubbles:group:abc123", + from: "group:ABC123", + }, + }, + { + name: "Zalo Personal DM target", + cfg: perChannelPeerCfg, + channel: "zalouser", + target: "123456", + expected: { + sessionKey: "agent:main:zalouser:direct:123456", + chatType: "direct", + }, + }, + { + name: "Slack mpim allowlist -> group key", + cfg: slackMpimCfg, + channel: "slack", + target: "channel:G123", + expected: { + sessionKey: "agent:main:slack:group:g123", + from: "slack:group:G123", + }, + }, + { + name: "Feishu explicit group prefix keeps group routing", + cfg: baseConfig, + channel: "feishu", + target: "group:oc_group_chat", + expected: { + sessionKey: "agent:main:feishu:group:oc_group_chat", + from: "feishu:group:oc_group_chat", + to: "oc_group_chat", + chatType: "group", + }, + }, + { + name: "Feishu explicit dm prefix keeps direct routing", + cfg: perChannelPeerCfg, + channel: "feishu", + target: "dm:oc_dm_chat", + expected: { + sessionKey: "agent:main:feishu:direct:oc_dm_chat", + from: "feishu:oc_dm_chat", + to: "oc_dm_chat", + chatType: "direct", + }, + }, + { + name: "Feishu bare oc_ target defaults to direct routing", + cfg: perChannelPeerCfg, + channel: "feishu", + target: "oc_ambiguous_chat", + expected: { + sessionKey: "agent:main:feishu:direct:oc_ambiguous_chat", + from: "feishu:oc_ambiguous_chat", + to: "oc_ambiguous_chat", + chatType: "direct", + }, + }, + ]; + + for (const testCase of cases) { + const route = await resolveOutboundSessionRoute({ + cfg: testCase.cfg, + channel: testCase.channel, + agentId: "main", + target: testCase.target, + replyToId: testCase.replyToId, + threadId: testCase.threadId, + }); + expect(route?.sessionKey, testCase.name).toBe(testCase.expected.sessionKey); + if (testCase.expected.from !== undefined) { + expect(route?.from, testCase.name).toBe(testCase.expected.from); + } + if (testCase.expected.to !== undefined) { + expect(route?.to, testCase.name).toBe(testCase.expected.to); + } + if (testCase.expected.threadId !== undefined) { + expect(route?.threadId, testCase.name).toBe(testCase.expected.threadId); + } + if (testCase.expected.chatType !== undefined) { + expect(route?.chatType, testCase.name).toBe(testCase.expected.chatType); + } + } + }); + + it("uses resolved Discord user targets to route bare numeric ids as DMs", async () => { + const route = await resolveOutboundSessionRoute({ + cfg: { session: { dmScope: "per-channel-peer" } } as OpenClawConfig, + channel: "discord", + agentId: "main", + target: "123", + resolvedTarget: { + to: "user:123", + kind: "user", + source: "directory", + }, + }); + + expect(route).toMatchObject({ + sessionKey: "agent:main:discord:direct:123", + from: "discord:123", + to: "user:123", + chatType: "direct", + }); + }); + + it("uses resolved Mattermost user targets to route bare ids as DMs", async () => { + const userId = "dthcxgoxhifn3pwh65cut3ud3w"; + const route = await resolveOutboundSessionRoute({ + cfg: { session: { dmScope: "per-channel-peer" } } as OpenClawConfig, + channel: "mattermost", + agentId: "main", + target: userId, + resolvedTarget: { + to: `user:${userId}`, + kind: "user", + source: "directory", + }, + }); + + expect(route).toMatchObject({ + sessionKey: `agent:main:mattermost:direct:${userId}`, + from: `mattermost:${userId}`, + to: `user:${userId}`, + chatType: "direct", + }); + }); + + it("rejects bare numeric Discord targets when the caller has no kind hint", async () => { + await expect( + resolveOutboundSessionRoute({ + cfg: { session: { dmScope: "per-channel-peer" } } as OpenClawConfig, + channel: "discord", + agentId: "main", + target: "123", + }), + ).rejects.toThrow(/Ambiguous Discord recipient/); + }); +}); diff --git a/src/infra/outbound/outbound.test.ts b/src/infra/outbound/outbound.test.ts index 84a38298145..8e3d0fca0bf 100644 --- a/src/infra/outbound/outbound.test.ts +++ b/src/infra/outbound/outbound.test.ts @@ -2,7 +2,6 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import type { OpenClawConfig } from "../../config/config.js"; import { ackDelivery, computeBackoffMs, @@ -16,7 +15,6 @@ import { moveToFailed, recoverPendingDeliveries, } from "./delivery-queue.js"; -import { resolveOutboundSessionRoute } from "./outbound-session.js"; import { runResolveOutboundTargetCoreTests } from "./targets.shared-test.js"; describe("delivery-queue", () => { @@ -598,260 +596,4 @@ describe("delivery-queue", () => { }); }); -describe("resolveOutboundSessionRoute", () => { - const baseConfig = {} as OpenClawConfig; - - it("resolves provider-specific session routes", async () => { - const perChannelPeerCfg = { session: { dmScope: "per-channel-peer" } } as OpenClawConfig; - const identityLinksCfg = { - session: { - dmScope: "per-peer", - identityLinks: { - alice: ["discord:123"], - }, - }, - } as OpenClawConfig; - const slackMpimCfg = { - channels: { - slack: { - dm: { - groupChannels: ["G123"], - }, - }, - }, - } as OpenClawConfig; - const cases: Array<{ - name: string; - cfg: OpenClawConfig; - channel: string; - target: string; - replyToId?: string; - threadId?: string; - expected: { - sessionKey: string; - from?: string; - to?: string; - threadId?: string | number; - chatType?: "direct" | "group"; - }; - }> = [ - { - name: "Slack thread", - cfg: baseConfig, - channel: "slack", - target: "channel:C123", - replyToId: "456", - expected: { - sessionKey: "agent:main:slack:channel:c123:thread:456", - from: "slack:channel:C123", - to: "channel:C123", - threadId: "456", - }, - }, - { - name: "Telegram topic group", - cfg: baseConfig, - channel: "telegram", - target: "-100123456:topic:42", - expected: { - sessionKey: "agent:main:telegram:group:-100123456:topic:42", - from: "telegram:group:-100123456:topic:42", - to: "telegram:-100123456", - threadId: 42, - }, - }, - { - name: "Telegram DM with topic", - cfg: perChannelPeerCfg, - channel: "telegram", - target: "123456789:topic:99", - expected: { - sessionKey: "agent:main:telegram:direct:123456789:thread:99", - from: "telegram:123456789:topic:99", - to: "telegram:123456789", - threadId: 99, - chatType: "direct", - }, - }, - { - name: "Telegram unresolved username DM", - cfg: perChannelPeerCfg, - channel: "telegram", - target: "@alice", - expected: { - sessionKey: "agent:main:telegram:direct:@alice", - chatType: "direct", - }, - }, - { - name: "Telegram DM scoped threadId fallback", - cfg: perChannelPeerCfg, - channel: "telegram", - target: "12345", - threadId: "12345:99", - expected: { - sessionKey: "agent:main:telegram:direct:12345:thread:99", - from: "telegram:12345:topic:99", - to: "telegram:12345", - threadId: 99, - chatType: "direct", - }, - }, - { - name: "identity-links per-peer", - cfg: identityLinksCfg, - channel: "discord", - target: "user:123", - expected: { - sessionKey: "agent:main:direct:alice", - }, - }, - { - name: "BlueBubbles chat_* prefix stripping", - cfg: baseConfig, - channel: "bluebubbles", - target: "chat_guid:ABC123", - expected: { - sessionKey: "agent:main:bluebubbles:group:abc123", - from: "group:ABC123", - }, - }, - { - name: "Zalo Personal DM target", - cfg: perChannelPeerCfg, - channel: "zalouser", - target: "123456", - expected: { - sessionKey: "agent:main:zalouser:direct:123456", - chatType: "direct", - }, - }, - { - name: "Slack mpim allowlist -> group key", - cfg: slackMpimCfg, - channel: "slack", - target: "channel:G123", - expected: { - sessionKey: "agent:main:slack:group:g123", - from: "slack:group:G123", - }, - }, - { - name: "Feishu explicit group prefix keeps group routing", - cfg: baseConfig, - channel: "feishu", - target: "group:oc_group_chat", - expected: { - sessionKey: "agent:main:feishu:group:oc_group_chat", - from: "feishu:group:oc_group_chat", - to: "oc_group_chat", - chatType: "group", - }, - }, - { - name: "Feishu explicit dm prefix keeps direct routing", - cfg: perChannelPeerCfg, - channel: "feishu", - target: "dm:oc_dm_chat", - expected: { - sessionKey: "agent:main:feishu:direct:oc_dm_chat", - from: "feishu:oc_dm_chat", - to: "oc_dm_chat", - chatType: "direct", - }, - }, - { - name: "Feishu bare oc_ target defaults to direct routing", - cfg: perChannelPeerCfg, - channel: "feishu", - target: "oc_ambiguous_chat", - expected: { - sessionKey: "agent:main:feishu:direct:oc_ambiguous_chat", - from: "feishu:oc_ambiguous_chat", - to: "oc_ambiguous_chat", - chatType: "direct", - }, - }, - ]; - - for (const testCase of cases) { - const route = await resolveOutboundSessionRoute({ - cfg: testCase.cfg, - channel: testCase.channel, - agentId: "main", - target: testCase.target, - replyToId: testCase.replyToId, - threadId: testCase.threadId, - }); - expect(route?.sessionKey, testCase.name).toBe(testCase.expected.sessionKey); - if (testCase.expected.from !== undefined) { - expect(route?.from, testCase.name).toBe(testCase.expected.from); - } - if (testCase.expected.to !== undefined) { - expect(route?.to, testCase.name).toBe(testCase.expected.to); - } - if (testCase.expected.threadId !== undefined) { - expect(route?.threadId, testCase.name).toBe(testCase.expected.threadId); - } - if (testCase.expected.chatType !== undefined) { - expect(route?.chatType, testCase.name).toBe(testCase.expected.chatType); - } - } - }); - - it("uses resolved Discord user targets to route bare numeric ids as DMs", async () => { - const route = await resolveOutboundSessionRoute({ - cfg: { session: { dmScope: "per-channel-peer" } } as OpenClawConfig, - channel: "discord", - agentId: "main", - target: "123", - resolvedTarget: { - to: "user:123", - kind: "user", - source: "directory", - }, - }); - - expect(route).toMatchObject({ - sessionKey: "agent:main:discord:direct:123", - from: "discord:123", - to: "user:123", - chatType: "direct", - }); - }); - - it("uses resolved Mattermost user targets to route bare ids as DMs", async () => { - const userId = "dthcxgoxhifn3pwh65cut3ud3w"; - const route = await resolveOutboundSessionRoute({ - cfg: { session: { dmScope: "per-channel-peer" } } as OpenClawConfig, - channel: "mattermost", - agentId: "main", - target: userId, - resolvedTarget: { - to: `user:${userId}`, - kind: "user", - source: "directory", - }, - }); - - expect(route).toMatchObject({ - sessionKey: `agent:main:mattermost:direct:${userId}`, - from: `mattermost:${userId}`, - to: `user:${userId}`, - chatType: "direct", - }); - }); - - it("rejects bare numeric Discord targets when the caller has no kind hint", async () => { - await expect( - resolveOutboundSessionRoute({ - cfg: { session: { dmScope: "per-channel-peer" } } as OpenClawConfig, - channel: "discord", - agentId: "main", - target: "123", - }), - ).rejects.toThrow(/Ambiguous Discord recipient/); - }); -}); - runResolveOutboundTargetCoreTests();