| import { beforeEach, describe, expect, it, vi } from "vitest"; |
|
|
| const resolveDefaultAgentIdMock = vi.hoisted(() => vi.fn()); |
| const resolveAgentWorkspaceDirMock = vi.hoisted(() => vi.fn()); |
| const getChannelPluginMock = vi.hoisted(() => vi.fn()); |
| const applyPluginAutoEnableMock = vi.hoisted(() => vi.fn()); |
| const loadOpenClawPluginsMock = vi.hoisted(() => vi.fn()); |
| const getActivePluginRegistryMock = vi.hoisted(() => vi.fn()); |
| const getActivePluginRegistryKeyMock = vi.hoisted(() => vi.fn()); |
| const normalizeMessageChannelMock = vi.hoisted(() => vi.fn()); |
| const isDeliverableMessageChannelMock = vi.hoisted(() => vi.fn()); |
|
|
| vi.mock("../../agents/agent-scope.js", () => ({ |
| resolveDefaultAgentId: (...args: unknown[]) => resolveDefaultAgentIdMock(...args), |
| resolveAgentWorkspaceDir: (...args: unknown[]) => resolveAgentWorkspaceDirMock(...args), |
| })); |
|
|
| vi.mock("../../channels/plugins/index.js", () => ({ |
| getChannelPlugin: (...args: unknown[]) => getChannelPluginMock(...args), |
| })); |
|
|
| vi.mock("../../config/plugin-auto-enable.js", () => ({ |
| applyPluginAutoEnable: (...args: unknown[]) => applyPluginAutoEnableMock(...args), |
| })); |
|
|
| vi.mock("../../plugins/loader.js", () => ({ |
| loadOpenClawPlugins: (...args: unknown[]) => loadOpenClawPluginsMock(...args), |
| })); |
|
|
| vi.mock("../../plugins/runtime.js", () => ({ |
| getActivePluginRegistry: (...args: unknown[]) => getActivePluginRegistryMock(...args), |
| getActivePluginRegistryKey: (...args: unknown[]) => getActivePluginRegistryKeyMock(...args), |
| })); |
|
|
| vi.mock("../../utils/message-channel.js", () => ({ |
| normalizeMessageChannel: (...args: unknown[]) => normalizeMessageChannelMock(...args), |
| isDeliverableMessageChannel: (...args: unknown[]) => isDeliverableMessageChannelMock(...args), |
| })); |
|
|
| import { importFreshModule } from "../../../test/helpers/import-fresh.js"; |
|
|
| async function importChannelResolution(scope: string) { |
| return await importFreshModule<typeof import("./channel-resolution.js")>( |
| import.meta.url, |
| `./channel-resolution.js?scope=${scope}`, |
| ); |
| } |
|
|
| describe("outbound channel resolution", () => { |
| beforeEach(() => { |
| resolveDefaultAgentIdMock.mockReset(); |
| resolveAgentWorkspaceDirMock.mockReset(); |
| getChannelPluginMock.mockReset(); |
| applyPluginAutoEnableMock.mockReset(); |
| loadOpenClawPluginsMock.mockReset(); |
| getActivePluginRegistryMock.mockReset(); |
| getActivePluginRegistryKeyMock.mockReset(); |
| normalizeMessageChannelMock.mockReset(); |
| isDeliverableMessageChannelMock.mockReset(); |
|
|
| normalizeMessageChannelMock.mockImplementation((value?: string | null) => |
| typeof value === "string" ? value.trim().toLowerCase() : undefined, |
| ); |
| isDeliverableMessageChannelMock.mockImplementation((value?: string) => |
| ["telegram", "discord", "slack"].includes(String(value)), |
| ); |
| getActivePluginRegistryMock.mockReturnValue({ channels: [] }); |
| getActivePluginRegistryKeyMock.mockReturnValue("registry-key"); |
| applyPluginAutoEnableMock.mockReturnValue({ config: { autoEnabled: true } }); |
| resolveDefaultAgentIdMock.mockReturnValue("main"); |
| resolveAgentWorkspaceDirMock.mockReturnValue("/tmp/workspace"); |
| }); |
|
|
| it("normalizes deliverable channels and rejects unknown ones", async () => { |
| const channelResolution = await importChannelResolution("normalize"); |
|
|
| expect(channelResolution.normalizeDeliverableOutboundChannel(" Telegram ")).toBe("telegram"); |
| expect(channelResolution.normalizeDeliverableOutboundChannel("unknown")).toBeUndefined(); |
| expect(channelResolution.normalizeDeliverableOutboundChannel(null)).toBeUndefined(); |
| }); |
|
|
| it("returns the already-registered plugin without bootstrapping", async () => { |
| const plugin = { id: "telegram" }; |
| getChannelPluginMock.mockReturnValueOnce(plugin); |
| const channelResolution = await importChannelResolution("existing-plugin"); |
|
|
| expect( |
| channelResolution.resolveOutboundChannelPlugin({ |
| channel: "telegram", |
| cfg: {} as never, |
| }), |
| ).toBe(plugin); |
| expect(loadOpenClawPluginsMock).not.toHaveBeenCalled(); |
| }); |
|
|
| it("falls back to the active registry when getChannelPlugin misses", async () => { |
| const plugin = { id: "telegram" }; |
| getChannelPluginMock.mockReturnValue(undefined); |
| getActivePluginRegistryMock.mockReturnValue({ |
| channels: [{ plugin }], |
| }); |
| const channelResolution = await importChannelResolution("direct-registry"); |
|
|
| expect( |
| channelResolution.resolveOutboundChannelPlugin({ |
| channel: "telegram", |
| cfg: {} as never, |
| }), |
| ).toBe(plugin); |
| }); |
|
|
| it("bootstraps plugins once per registry key and returns the newly loaded plugin", async () => { |
| const plugin = { id: "telegram" }; |
| getChannelPluginMock.mockReturnValueOnce(undefined).mockReturnValueOnce(plugin); |
| const channelResolution = await importChannelResolution("bootstrap-success"); |
|
|
| expect( |
| channelResolution.resolveOutboundChannelPlugin({ |
| channel: "telegram", |
| cfg: { channels: {} } as never, |
| }), |
| ).toBe(plugin); |
| expect(loadOpenClawPluginsMock).toHaveBeenCalledWith({ |
| config: { autoEnabled: true }, |
| workspaceDir: "/tmp/workspace", |
| }); |
|
|
| getChannelPluginMock.mockReturnValue(undefined); |
| channelResolution.resolveOutboundChannelPlugin({ |
| channel: "telegram", |
| cfg: { channels: {} } as never, |
| }); |
| expect(loadOpenClawPluginsMock).toHaveBeenCalledTimes(1); |
| }); |
|
|
| it("retries bootstrap after a transient load failure", async () => { |
| getChannelPluginMock.mockReturnValue(undefined); |
| loadOpenClawPluginsMock.mockImplementationOnce(() => { |
| throw new Error("transient"); |
| }); |
| const channelResolution = await importChannelResolution("bootstrap-retry"); |
|
|
| expect( |
| channelResolution.resolveOutboundChannelPlugin({ |
| channel: "telegram", |
| cfg: { channels: {} } as never, |
| }), |
| ).toBeUndefined(); |
|
|
| channelResolution.resolveOutboundChannelPlugin({ |
| channel: "telegram", |
| cfg: { channels: {} } as never, |
| }); |
| expect(loadOpenClawPluginsMock).toHaveBeenCalledTimes(2); |
| }); |
| }); |
|
|