diff --git a/src/infra/outbound/channel-adapters.test.ts b/src/infra/outbound/channel-adapters.test.ts index 0e6bdc969bb..bd9d110bc44 100644 --- a/src/infra/outbound/channel-adapters.test.ts +++ b/src/infra/outbound/channel-adapters.test.ts @@ -15,15 +15,15 @@ class TestSeparator { constructor(readonly options: { divider: boolean; spacing: string }) {} } -class TestDiscordUiContainer { +class TestRichUiContainer { constructor(readonly components: Array) {} } -const discordCrossContextPlugin: Pick< +const richCrossContextPlugin: Pick< ChannelPlugin, "id" | "meta" | "capabilities" | "config" | "messaging" > = { - ...createChannelTestPluginBase({ id: "discord" }), + ...createChannelTestPluginBase({ id: "rich-chat" }), messaging: { buildCrossContextComponents: ({ originLabel, message, cfg, accountId }) => { const trimmed = message.trim(); @@ -35,7 +35,7 @@ const discordCrossContextPlugin: Pick< components.push(new TestTextDisplay(`*From ${originLabel}*`)); void cfg; void accountId; - return [new TestDiscordUiContainer(components)]; + return [new TestRichUiContainer(components)]; }, }, }; @@ -44,38 +44,38 @@ describe("getChannelMessageAdapter", () => { beforeEach(() => { setActivePluginRegistry( createTestRegistry([ - { pluginId: "discord", plugin: discordCrossContextPlugin, source: "test" }, + { pluginId: "rich-chat", plugin: richCrossContextPlugin, source: "test" }, { - pluginId: "telegram", - plugin: createChannelTestPluginBase({ id: "telegram" }), + pluginId: "plain-chat", + plugin: createChannelTestPluginBase({ id: "plain-chat" }), source: "test", }, ]), ); }); - it("returns the default adapter for non-discord channels", () => { - expect(getChannelMessageAdapter("telegram")).toEqual({ + it("returns the default adapter for channels without structured component support", () => { + expect(getChannelMessageAdapter("plain-chat")).toEqual({ supportsComponentsV2: false, }); }); - it("returns the discord adapter with a cross-context component builder", () => { - const adapter = getChannelMessageAdapter("discord"); + it("returns an adapter with a cross-context component builder", () => { + const adapter = getChannelMessageAdapter("rich-chat"); expect(adapter.supportsComponentsV2).toBe(true); expect(adapter.buildCrossContextComponents).toBeTypeOf("function"); const components = adapter.buildCrossContextComponents?.({ - originLabel: "Telegram", + originLabel: "Forum", message: "Hello from chat", cfg: {} as never, accountId: "primary", }); - const container = components?.[0] as TestDiscordUiContainer | undefined; + const container = components?.[0] as TestRichUiContainer | undefined; expect(components).toHaveLength(1); - expect(container).toBeInstanceOf(TestDiscordUiContainer); + expect(container).toBeInstanceOf(TestRichUiContainer); expect(container?.components).toEqual([ expect.any(TestTextDisplay), expect.any(TestSeparator), @@ -86,7 +86,7 @@ describe("getChannelMessageAdapter", () => { it.each([ { message: "Hello from chat", - originLabel: "Telegram", + originLabel: "Forum", accountId: "primary", expectedComponents: [ expect.any(TestTextDisplay), @@ -96,20 +96,20 @@ describe("getChannelMessageAdapter", () => { }, { message: " ", - originLabel: "Signal", + originLabel: "Pager", expectedComponents: [expect.any(TestTextDisplay)], }, ])( "builds cross-context components for %j", ({ message, originLabel, accountId, expectedComponents }) => { - const adapter = getChannelMessageAdapter("discord"); + const adapter = getChannelMessageAdapter("rich-chat"); const components = adapter.buildCrossContextComponents?.({ originLabel, message, cfg: {} as never, ...(accountId ? { accountId } : {}), }); - const container = components?.[0] as TestDiscordUiContainer | undefined; + const container = components?.[0] as TestRichUiContainer | undefined; expect(components).toHaveLength(1); expect(container?.components).toEqual(expectedComponents); diff --git a/src/infra/outbound/channel-resolution.test.ts b/src/infra/outbound/channel-resolution.test.ts index 6dbfa19c3a3..975b19271ba 100644 --- a/src/infra/outbound/channel-resolution.test.ts +++ b/src/infra/outbound/channel-resolution.test.ts @@ -83,7 +83,7 @@ describe("outbound channel resolution", () => { typeof value === "string" ? value.trim().toLowerCase() : undefined, ); isDeliverableMessageChannelMock.mockImplementation((value?: string) => - ["telegram", "discord", "slack"].includes(String(value)), + ["alpha", "beta", "gamma"].includes(String(value)), ); getActivePluginRegistryMock.mockReturnValue({ channels: [] }); getActivePluginChannelRegistryMock.mockReturnValue({ channels: [] }); @@ -100,7 +100,7 @@ describe("outbound channel resolution", () => { }); it.each([ - { input: " Telegram ", expected: "telegram" }, + { input: " Alpha ", expected: "alpha" }, { input: "unknown", expected: undefined }, { input: null, expected: undefined }, ])("normalizes deliverable outbound channel for %j", async ({ input, expected }) => { @@ -109,13 +109,13 @@ describe("outbound channel resolution", () => { }); it("returns the already-registered plugin without bootstrapping", async () => { - const plugin = { id: "telegram" }; + const plugin = { id: "alpha" }; getLoadedChannelPluginMock.mockReturnValueOnce(plugin); const channelResolution = await importChannelResolution("existing-plugin"); expect( channelResolution.resolveOutboundChannelPlugin({ - channel: "telegram", + channel: "alpha", cfg: {} as never, }), ).toBe(plugin); @@ -123,7 +123,7 @@ describe("outbound channel resolution", () => { }); it("falls back to the active registry when getChannelPlugin misses", async () => { - const plugin = { id: "telegram" }; + const plugin = { id: "alpha" }; getChannelPluginMock.mockReturnValue(undefined); getActivePluginRegistryMock.mockReturnValue({ channels: [{ plugin }], @@ -135,20 +135,20 @@ describe("outbound channel resolution", () => { expect( channelResolution.resolveOutboundChannelPlugin({ - channel: "telegram", + channel: "alpha", cfg: {} as never, }), ).toBe(plugin); }); it("bootstraps plugins once per registry key and returns the newly loaded plugin", async () => { - const plugin = { id: "telegram" }; + const plugin = { id: "alpha" }; getLoadedChannelPluginMock.mockReturnValueOnce(undefined).mockReturnValueOnce(plugin); const channelResolution = await importChannelResolution("bootstrap-success"); expect( channelResolution.resolveOutboundChannelPlugin({ - channel: "telegram", + channel: "alpha", cfg: { channels: {} } as never, }), ).toBe(plugin); @@ -156,7 +156,7 @@ describe("outbound channel resolution", () => { getChannelPluginMock.mockReturnValue(undefined); channelResolution.resolveOutboundChannelPlugin({ - channel: "telegram", + channel: "alpha", cfg: { channels: {} } as never, }); expect(resolveRuntimePluginRegistryMock).toHaveBeenCalledTimes(1); @@ -164,19 +164,19 @@ describe("outbound channel resolution", () => { }); it("bootstraps when the active registry has other channels but not the requested one", async () => { - const plugin = { id: "telegram" }; + const plugin = { id: "alpha" }; getLoadedChannelPluginMock.mockReturnValueOnce(undefined).mockReturnValueOnce(plugin); getActivePluginRegistryMock.mockReturnValue({ - channels: [{ plugin: { id: "discord" } }], + channels: [{ plugin: { id: "beta" } }], }); getActivePluginChannelRegistryMock.mockReturnValue({ - channels: [{ plugin: { id: "discord" } }], + channels: [{ plugin: { id: "beta" } }], }); const channelResolution = await importChannelResolution("bootstrap-missing-target"); expect( channelResolution.resolveOutboundChannelPlugin({ - channel: "telegram", + channel: "alpha", cfg: { channels: {} } as never, }), ).toBe(plugin); @@ -192,13 +192,13 @@ describe("outbound channel resolution", () => { expect( channelResolution.resolveOutboundChannelPlugin({ - channel: "telegram", + channel: "alpha", cfg: { channels: {} } as never, }), ).toBeUndefined(); channelResolution.resolveOutboundChannelPlugin({ - channel: "telegram", + channel: "alpha", cfg: { channels: {} } as never, }); expect(resolveRuntimePluginRegistryMock).toHaveBeenCalledTimes(2); @@ -209,14 +209,14 @@ describe("outbound channel resolution", () => { const channelResolution = await importChannelResolution("channel-version-change"); channelResolution.resolveOutboundChannelPlugin({ - channel: "telegram", + channel: "alpha", cfg: { channels: {} } as never, }); expect(resolveRuntimePluginRegistryMock).toHaveBeenCalledTimes(1); getActivePluginChannelRegistryVersionMock.mockReturnValue(2); channelResolution.resolveOutboundChannelPlugin({ - channel: "telegram", + channel: "alpha", cfg: { channels: {} } as never, }); expect(resolveRuntimePluginRegistryMock).toHaveBeenCalledTimes(2); diff --git a/src/infra/outbound/channel-selection.test.ts b/src/infra/outbound/channel-selection.test.ts index aa9bf18070c..17b7b91c357 100644 --- a/src/infra/outbound/channel-selection.test.ts +++ b/src/infra/outbound/channel-selection.test.ts @@ -5,11 +5,20 @@ const mocks = vi.hoisted(() => ({ resolveOutboundChannelPlugin: vi.fn(), })); +const deliverableChannelIds = vi.hoisted(() => ["alpha", "beta", "gamma", "delta", "muted"]); + vi.mock("../../channels/plugins/index.js", () => ({ getLoadedChannelPlugin: vi.fn(), listChannelPlugins: mocks.listChannelPlugins, })); +vi.mock("../../utils/message-channel.js", () => ({ + listDeliverableMessageChannels: () => deliverableChannelIds, + isDeliverableMessageChannel: (value: string) => deliverableChannelIds.includes(value), + normalizeMessageChannel: (value?: string | null) => + typeof value === "string" ? value.trim().toLowerCase() : undefined, +})); + vi.mock("./channel-resolution.js", () => ({ resolveOutboundChannelPlugin: mocks.resolveOutboundChannelPlugin, })); @@ -70,37 +79,37 @@ describe("listConfiguredMessageChannels", () => { it.each([ { - plugins: [makePlugin({ id: "not-a-channel" }), makePlugin({ id: "slack", accountIds: [] })], + plugins: [makePlugin({ id: "not-a-channel" }), makePlugin({ id: "alpha", accountIds: [] })], expected: [], expectedErrors: 0, }, { plugins: [ makePlugin({ - id: "discord", + id: "beta", resolveAccount: () => ({ enabled: true }), }), ], - expected: ["discord"], + expected: ["beta"], expectedErrors: 0, }, { plugins: [ makePlugin({ - id: "telegram", + id: "gamma", accountIds: ["disabled", "enabled"], resolveAccount: (accountId) => accountId === "disabled" ? { enabled: false } : { enabled: true }, isConfigured: (account) => (account as { enabled?: boolean }).enabled === true, }), ], - expected: ["telegram"], + expected: ["gamma"], expectedErrors: 0, }, { plugins: [ makePlugin({ - id: "signal", + id: "muted", resolveAccount: () => ({ token: "x" }), isEnabled: () => false, isConfigured: () => true, @@ -112,7 +121,7 @@ describe("listConfiguredMessageChannels", () => { { plugins: [ makePlugin({ - id: "discord", + id: "beta", resolveAccount: () => { throw new Error("boom"); }, @@ -136,9 +145,9 @@ describe("resolveMessageChannelSelection", () => { it.each([ { - params: { cfg: {} as never, channel: "telegram" }, + params: { cfg: {} as never, channel: "alpha" }, expected: { - channel: "telegram", + channel: "alpha", configured: [], source: "explicit", }, @@ -146,12 +155,12 @@ describe("resolveMessageChannelSelection", () => { { setup: () => { const isConfigured = vi.fn(async () => true); - mocks.listChannelPlugins.mockReturnValue([makePlugin({ id: "slack", isConfigured })]); + mocks.listChannelPlugins.mockReturnValue([makePlugin({ id: "beta", isConfigured })]); return { isConfigured }; }, - params: { cfg: {} as never, channel: "slack" }, + params: { cfg: {} as never, channel: "beta" }, expected: { - channel: "slack", + channel: "beta", configured: [], source: "explicit", }, @@ -160,17 +169,17 @@ describe("resolveMessageChannelSelection", () => { }, }, { - params: { cfg: {} as never, channel: "channel:C123", fallbackChannel: "slack" }, + params: { cfg: {} as never, channel: "channel:C123", fallbackChannel: "beta" }, expected: { - channel: "slack", + channel: "beta", configured: [], source: "tool-context-fallback", }, }, { - params: { cfg: {} as never, fallbackChannel: "signal" }, + params: { cfg: {} as never, fallbackChannel: "gamma" }, expected: { - channel: "signal", + channel: "gamma", configured: [], source: "tool-context-fallback", }, @@ -178,25 +187,25 @@ describe("resolveMessageChannelSelection", () => { { setup: () => { mocks.listChannelPlugins.mockReturnValue([ - makePlugin({ id: "discord", isConfigured: async () => true }), + makePlugin({ id: "delta", isConfigured: async () => true }), ]); }, params: { cfg: {} as never }, expected: { - channel: "discord", - configured: ["discord"], + channel: "delta", + configured: ["delta"], source: "single-configured", }, }, { setup: () => { mocks.resolveOutboundChannelPlugin.mockImplementation(({ channel }: { channel: string }) => - channel === "slack" ? { id: "slack" } : undefined, + channel === "beta" ? { id: "beta" } : undefined, ); }, - params: { cfg: {} as never, channel: "discord", fallbackChannel: "slack" }, + params: { cfg: {} as never, channel: "alpha", fallbackChannel: "beta" }, expected: { - channel: "slack", + channel: "beta", configured: [], source: "tool-context-fallback", }, @@ -216,8 +225,8 @@ describe("resolveMessageChannelSelection", () => { setup: () => { mocks.resolveOutboundChannelPlugin.mockReturnValue(undefined); }, - params: { cfg: {} as never, channel: "discord" }, - expectedMessage: "Channel is unavailable: discord", + params: { cfg: {} as never, channel: "alpha" }, + expectedMessage: "Channel is unavailable: alpha", }, { params: { cfg: {} as never }, @@ -226,13 +235,12 @@ describe("resolveMessageChannelSelection", () => { { setup: () => { mocks.listChannelPlugins.mockReturnValue([ - makePlugin({ id: "discord", isConfigured: async () => true }), - makePlugin({ id: "telegram", isConfigured: async () => true }), + makePlugin({ id: "beta", isConfigured: async () => true }), + makePlugin({ id: "gamma", isConfigured: async () => true }), ]); }, params: { cfg: {} as never }, - expectedMessage: - "Channel is required when multiple channels are configured: discord, telegram", + expectedMessage: "Channel is required when multiple channels are configured: beta, gamma", }, ])("rejects invalid channel selection for %j", async ({ setup, params, expectedMessage }) => { setup?.(); diff --git a/src/infra/outbound/message-action-runner.poll.test.ts b/src/infra/outbound/message-action-runner.poll.test.ts index 2de12b44034..36340eb2009 100644 --- a/src/infra/outbound/message-action-runner.poll.test.ts +++ b/src/infra/outbound/message-action-runner.poll.test.ts @@ -32,27 +32,27 @@ vi.mock("./message-action-threading.js", async () => { await import("./message-action-threading.test-helpers.js"); return createOutboundThreadingMock(); }); -const telegramConfig = { +const pollerConfig = { channels: { - telegram: { - botToken: "telegram-test", + poller: { + botToken: "poller-test", }, }, } as OpenClawConfig; -const telegramPollTestPlugin: ChannelPlugin = { - id: "telegram", +const pollerTestPlugin: ChannelPlugin = { + id: "poller", meta: { - id: "telegram", - label: "Telegram", - selectionLabel: "Telegram", - docsPath: "/channels/telegram", - blurb: "Telegram poll test plugin.", + id: "poller", + label: "Poller", + selectionLabel: "Poller", + docsPath: "/channels/poller", + blurb: "Poller test plugin.", }, capabilities: { chatTypes: ["direct", "group"] }, config: { listAccountIds: () => ["default"], - resolveAccount: () => ({ botToken: "telegram-test" }), + resolveAccount: () => ({ botToken: "poller-test" }), isConfigured: () => true, }, outbound: { @@ -119,9 +119,9 @@ describe("runMessageAction poll handling", () => { setActivePluginRegistry( createTestRegistry([ { - pluginId: "telegram", + pluginId: "poller", source: "test", - plugin: telegramPollTestPlugin, + plugin: pollerTestPlugin, }, ]), ); @@ -146,10 +146,10 @@ describe("runMessageAction poll handling", () => { it("requires at least two poll options", async () => { await expect( runPollAction({ - cfg: telegramConfig, + cfg: pollerConfig, actionParams: { - channel: "telegram", - target: "telegram:123", + channel: "poller", + target: "poller:123", pollQuestion: "Lunch?", pollOption: ["Pizza"], }, @@ -160,16 +160,16 @@ describe("runMessageAction poll handling", () => { it("passes shared poll fields and auto threadId to executePollAction", async () => { const call = await runPollAction({ - cfg: telegramConfig, + cfg: pollerConfig, actionParams: { - channel: "telegram", - target: "telegram:123", + channel: "poller", + target: "poller:123", pollQuestion: "Lunch?", pollOption: ["Pizza", "Sushi"], pollDurationHours: 2, }, toolContext: { - currentChannelId: "telegram:123", + currentChannelId: "poller:123", currentThreadTs: "42", }, }); @@ -181,10 +181,10 @@ describe("runMessageAction poll handling", () => { it("expands maxSelections when pollMulti is enabled", async () => { const call = await runPollAction({ - cfg: telegramConfig, + cfg: pollerConfig, actionParams: { - channel: "telegram", - target: "telegram:123", + channel: "poller", + target: "poller:123", pollQuestion: "Lunch?", pollOption: ["Pizza", "Sushi", "Soup"], pollMulti: true, @@ -196,10 +196,10 @@ describe("runMessageAction poll handling", () => { it("defaults maxSelections to one choice when pollMulti is omitted", async () => { const call = await runPollAction({ - cfg: telegramConfig, + cfg: pollerConfig, actionParams: { - channel: "telegram", - target: "telegram:123", + channel: "poller", + target: "poller:123", pollQuestion: "Lunch?", pollOption: ["Pizza", "Sushi", "Soup"], }, diff --git a/src/infra/outbound/message-action-runner.threading.test.ts b/src/infra/outbound/message-action-runner.threading.test.ts index be2b3bafa13..8818a84eefc 100644 --- a/src/infra/outbound/message-action-runner.threading.test.ts +++ b/src/infra/outbound/message-action-runner.threading.test.ts @@ -8,24 +8,24 @@ import { const ensureOutboundSessionEntry = vi.fn(async () => undefined); const resolveOutboundSessionRoute = vi.fn(); -const slackConfig = { +const workspaceConfig = { channels: { - slack: { + workspace: { botToken: "xoxb-test", }, }, } as OpenClawConfig; -const telegramConfig = { +const forumConfig = { channels: { - telegram: { - botToken: "telegram-test", + forum: { + botToken: "forum-test", }, }, } as OpenClawConfig; -const defaultTelegramToolContext = { - currentChannelId: "telegram:123", +const defaultForumToolContext = { + currentChannelId: "forum:123", currentThreadTs: "42", } as const; @@ -40,17 +40,17 @@ describe("message action threading helpers", () => { name: "exact channel id", target: "channel:C123", threadTs: "111.222", - expectedSessionKey: "agent:main:slack:channel:c123:thread:111.222", + expectedSessionKey: "agent:main:workspace:channel:c123:thread:111.222", }, { name: "case-insensitive channel id", target: "channel:c123", threadTs: "333.444", - expectedSessionKey: "agent:main:slack:channel:c123:thread:333.444", + expectedSessionKey: "agent:main:workspace:channel:c123:thread:333.444", }, - ] as const)("prepares outbound routes for slack using $name", async (testCase) => { + ] as const)("prepares outbound routes for workspace using $name", async (testCase) => { const actionParams: Record = { - channel: "slack", + channel: "workspace", target: testCase.target, message: "hi", }; @@ -65,8 +65,8 @@ describe("message action threading helpers", () => { }); const result = await prepareOutboundMirrorRoute({ - cfg: slackConfig, - channel: "slack", + cfg: workspaceConfig, + channel: "workspace", to: testCase.target, actionParams, toolContext: { @@ -89,30 +89,30 @@ describe("message action threading helpers", () => { it.each([ { name: "injects threadId for matching target", - target: "telegram:123", + target: "forum:123", expectedThreadId: "42", }, { name: "injects threadId for prefixed group target", - target: "telegram:group:123", + target: "forum:group:123", expectedThreadId: "42", }, { name: "skips threadId when target chat differs", - target: "telegram:999", + target: "forum:999", expectedThreadId: undefined, }, - ] as const)("telegram auto-threading: $name", (testCase) => { + ] as const)("forum auto-threading: $name", (testCase) => { const actionParams: Record = { - channel: "telegram", + channel: "forum", target: testCase.target, message: "hi", }; const resolved = resolveAndApplyOutboundThreadId(actionParams, { - cfg: telegramConfig, + cfg: forumConfig, to: testCase.target, - toolContext: defaultTelegramToolContext, + toolContext: defaultForumToolContext, resolveAutoThreadId: ({ to, toolContext }) => to.includes("123") ? toolContext?.currentThreadTs : undefined, }); @@ -121,18 +121,18 @@ describe("message action threading helpers", () => { expect(resolved).toBe(testCase.expectedThreadId); }); - it("uses explicit telegram threadId when provided", () => { + it("uses explicit forum threadId when provided", () => { const actionParams: Record = { - channel: "telegram", - target: "telegram:123", + channel: "forum", + target: "forum:123", message: "hi", threadId: "999", }; const resolved = resolveAndApplyOutboundThreadId(actionParams, { - cfg: telegramConfig, - to: "telegram:123", - toolContext: defaultTelegramToolContext, + cfg: forumConfig, + to: "forum:123", + toolContext: defaultForumToolContext, resolveAutoThreadId: () => "42", }); @@ -143,16 +143,16 @@ describe("message action threading helpers", () => { it("passes explicit replyTo into auto-thread resolution", () => { const resolveAutoThreadId = vi.fn(() => "thread-777"); const actionParams: Record = { - channel: "telegram", - target: "telegram:123", + channel: "forum", + target: "forum:123", message: "hi", replyTo: "777", }; const resolved = resolveAndApplyOutboundThreadId(actionParams, { - cfg: telegramConfig, - to: "telegram:123", - toolContext: defaultTelegramToolContext, + cfg: forumConfig, + to: "forum:123", + toolContext: defaultForumToolContext, resolveAutoThreadId, });