diff --git a/src/auto-reply/reply/route-reply.test.ts b/src/auto-reply/reply/route-reply.test.ts index 776a2374fbc..ed507607c83 100644 --- a/src/auto-reply/reply/route-reply.test.ts +++ b/src/auto-reply/reply/route-reply.test.ts @@ -81,6 +81,12 @@ const createRegistry = (channels: PluginRegistry["channels"]): PluginRegistry => typedHooks: [], commands: [], channels, + channelSetups: channels.map((entry) => ({ + pluginId: entry.pluginId, + plugin: entry.plugin, + source: entry.source, + enabled: true, + })), providers: [], gatewayHandlers: {}, httpRoutes: [], diff --git a/src/channels/plugins/setup-registry.ts b/src/channels/plugins/setup-registry.ts new file mode 100644 index 00000000000..493b14351cc --- /dev/null +++ b/src/channels/plugins/setup-registry.ts @@ -0,0 +1,80 @@ +import { + getActivePluginRegistryVersion, + requireActivePluginRegistry, +} from "../../plugins/runtime.js"; +import { CHAT_CHANNEL_ORDER, type ChatChannelId } from "../registry.js"; +import type { ChannelId, ChannelPlugin } from "./types.js"; + +type CachedChannelSetupPlugins = { + registryVersion: number; + sorted: ChannelPlugin[]; + byId: Map; +}; + +const EMPTY_CHANNEL_SETUP_CACHE: CachedChannelSetupPlugins = { + registryVersion: -1, + sorted: [], + byId: new Map(), +}; + +let cachedChannelSetupPlugins = EMPTY_CHANNEL_SETUP_CACHE; + +function dedupeSetupPlugins(plugins: ChannelPlugin[]): ChannelPlugin[] { + const seen = new Set(); + const resolved: ChannelPlugin[] = []; + for (const plugin of plugins) { + const id = String(plugin.id).trim(); + if (!id || seen.has(id)) { + continue; + } + seen.add(id); + resolved.push(plugin); + } + return resolved; +} + +function resolveCachedChannelSetupPlugins(): CachedChannelSetupPlugins { + const registry = requireActivePluginRegistry(); + const registryVersion = getActivePluginRegistryVersion(); + const cached = cachedChannelSetupPlugins; + if (cached.registryVersion === registryVersion) { + return cached; + } + + const sorted = dedupeSetupPlugins( + (registry.channelSetups ?? []).map((entry) => entry.plugin), + ).toSorted((a, b) => { + const indexA = CHAT_CHANNEL_ORDER.indexOf(a.id as ChatChannelId); + const indexB = CHAT_CHANNEL_ORDER.indexOf(b.id as ChatChannelId); + const orderA = a.meta.order ?? (indexA === -1 ? 999 : indexA); + const orderB = b.meta.order ?? (indexB === -1 ? 999 : indexB); + if (orderA !== orderB) { + return orderA - orderB; + } + return a.id.localeCompare(b.id); + }); + const byId = new Map(); + for (const plugin of sorted) { + byId.set(plugin.id, plugin); + } + + const next: CachedChannelSetupPlugins = { + registryVersion, + sorted, + byId, + }; + cachedChannelSetupPlugins = next; + return next; +} + +export function listChannelSetupPlugins(): ChannelPlugin[] { + return resolveCachedChannelSetupPlugins().sorted.slice(); +} + +export function getChannelSetupPlugin(id: ChannelId): ChannelPlugin | undefined { + const resolvedId = String(id).trim(); + if (!resolvedId) { + return undefined; + } + return resolveCachedChannelSetupPlugins().byId.get(resolvedId); +} diff --git a/src/commands/onboard-channels.e2e.test.ts b/src/commands/onboard-channels.e2e.test.ts index 88606bcc3cc..b25bf35db78 100644 --- a/src/commands/onboard-channels.e2e.test.ts +++ b/src/commands/onboard-channels.e2e.test.ts @@ -307,12 +307,9 @@ describe("setupChannels", () => { it("adds disabled hint to channel selection when a channel is disabled", async () => { let selectionCount = 0; - const select = vi.fn(async ({ message, options }: { message: string; options: unknown[] }) => { + const select = vi.fn(async ({ message }: { message: string; options: unknown[] }) => { if (message === "Select a channel") { selectionCount += 1; - const opts = options as Array<{ value: string; hint?: string }>; - const telegram = opts.find((opt) => opt.value === "telegram"); - expect(telegram?.hint).toContain("disabled"); return selectionCount === 1 ? "telegram" : "__done__"; } if (message.includes("already configured")) { @@ -332,6 +329,13 @@ describe("setupChannels", () => { await runSetupChannels(createTelegramCfg("token", false), prompter); expect(select).toHaveBeenCalledWith(expect.objectContaining({ message: "Select a channel" })); + const channelSelectCall = select.mock.calls.find( + ([params]) => (params as { message?: string }).message === "Select a channel", + ); + const telegramOption = ( + channelSelectCall?.[0] as { options?: Array<{ value: string; hint?: string }> } | undefined + )?.options?.find((opt) => opt.value === "telegram"); + expect(telegramOption?.hint).toContain("disabled"); expect(multiselect).not.toHaveBeenCalled(); }); diff --git a/src/commands/onboard-channels.ts b/src/commands/onboard-channels.ts index 6e79379e1f1..ca4b090ce5a 100644 --- a/src/commands/onboard-channels.ts +++ b/src/commands/onboard-channels.ts @@ -1,7 +1,10 @@ import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js"; import { listChannelPluginCatalogEntries } from "../channels/plugins/catalog.js"; import { resolveChannelDefaultAccountId } from "../channels/plugins/helpers.js"; -import { listChannelPlugins, getChannelPlugin } from "../channels/plugins/index.js"; +import { + getChannelSetupPlugin, + listChannelSetupPlugins, +} from "../channels/plugins/setup-registry.js"; import type { ChannelMeta } from "../channels/plugins/types.js"; import { formatChannelPrimerLine, @@ -37,7 +40,7 @@ import type { type ConfiguredChannelAction = "update" | "disable" | "delete" | "skip"; type ChannelStatusSummary = { - installedPlugins: ReturnType; + installedPlugins: ReturnType; catalogEntries: ReturnType; statusByChannel: Map; statusLines: string[]; @@ -90,7 +93,7 @@ async function promptRemovalAccountId(params: { channel: ChannelChoice; }): Promise { const { cfg, prompter, label, channel } = params; - const plugin = getChannelPlugin(channel); + const plugin = getChannelSetupPlugin(channel); if (!plugin) { return DEFAULT_ACCOUNT_ID; } @@ -115,7 +118,7 @@ async function collectChannelStatus(params: { options?: SetupChannelsOptions; accountOverrides: Partial>; }): Promise { - const installedPlugins = listChannelPlugins(); + const installedPlugins = listChannelSetupPlugins(); const installedIds = new Set(installedPlugins.map((plugin) => plugin.id)); const workspaceDir = resolveAgentWorkspaceDir(params.cfg, resolveDefaultAgentId(params.cfg)); const catalogEntries = listChannelPluginCatalogEntries({ workspaceDir }).filter( @@ -297,6 +300,13 @@ export async function setupChannels( options?: SetupChannelsOptions, ): Promise { let next = cfg; + if (listChannelOnboardingAdapters().length === 0) { + reloadOnboardingPluginRegistry({ + cfg: next, + runtime, + workspaceDir: resolveAgentWorkspaceDir(next, resolveDefaultAgentId(next)), + }); + } const forceAllowFromChannels = new Set(options?.forceAllowFromChannels ?? []); const accountOverrides: Partial> = { ...options?.accountIds, @@ -366,7 +376,15 @@ export async function setupChannels( }; const resolveDisabledHint = (channel: ChannelChoice): string | undefined => { - const plugin = getChannelPlugin(channel); + const plugin = getChannelSetupPlugin(channel); + if ( + typeof (next.channels as Record | undefined)?.[channel] + ?.enabled === "boolean" + ) { + return (next.channels as Record)[channel]?.enabled === false + ? "disabled" + : undefined; + } if (!plugin) { if (next.plugins?.entries?.[channel]?.enabled === false) { return "plugin disabled"; @@ -383,11 +401,6 @@ export async function setupChannels( enabled = plugin.config.isEnabled(account, next); } else if (typeof (account as { enabled?: boolean })?.enabled === "boolean") { enabled = (account as { enabled?: boolean }).enabled; - } else if ( - typeof (next.channels as Record | undefined)?.[channel] - ?.enabled === "boolean" - ) { - enabled = (next.channels as Record)[channel]?.enabled; } return enabled === false ? "disabled" : undefined; }; @@ -411,7 +424,7 @@ export async function setupChannels( const getChannelEntries = () => { const core = listChatChannels(); - const installed = listChannelPlugins(); + const installed = listChannelSetupPlugins(); const installedIds = new Set(installed.map((plugin) => plugin.id)); const workspaceDir = resolveAgentWorkspaceDir(next, resolveDefaultAgentId(next)); const catalog = listChannelPluginCatalogEntries({ workspaceDir }).filter( @@ -449,10 +462,7 @@ export async function setupChannels( statusByChannel.set(channel, status); }; - const ensureBundledPluginEnabled = async (channel: ChannelChoice): Promise => { - if (getChannelPlugin(channel)) { - return true; - } + const enableBundledPluginForSetup = async (channel: ChannelChoice): Promise => { const result = enablePluginInConfig(next, channel); next = result.config; if (!result.enabled) { @@ -468,24 +478,6 @@ export async function setupChannels( runtime, workspaceDir, }); - if (!getChannelPlugin(channel)) { - // Some installs/environments can fail to populate the plugin registry during onboarding, - // even for built-in channels. If the channel supports onboarding, proceed with config - // so setup isn't blocked; the gateway can still load plugins on startup. - const adapter = getChannelOnboardingAdapter(channel); - if (adapter) { - await prompter.note( - `${channel} plugin not available (continuing with onboarding). If the channel still doesn't work after setup, run \`${formatCliCommand( - "openclaw plugins list", - )}\` and \`${formatCliCommand("openclaw plugins enable " + channel)}\`, then restart the gateway.`, - "Channel setup", - ); - await refreshStatus(channel); - return true; - } - await prompter.note(`${channel} plugin not available.`, "Channel setup"); - return false; - } await refreshStatus(channel); return true; }; @@ -529,7 +521,7 @@ export async function setupChannels( }; const handleConfiguredChannel = async (channel: ChannelChoice, label: string) => { - const plugin = getChannelPlugin(channel); + const plugin = getChannelSetupPlugin(channel); const adapter = getChannelOnboardingAdapter(channel); if (adapter?.configureWhenConfigured) { const custom = await adapter.configureWhenConfigured({ @@ -642,13 +634,13 @@ export async function setupChannels( }); await refreshStatus(channel); } else { - const enabled = await ensureBundledPluginEnabled(channel); + const enabled = await enableBundledPluginForSetup(channel); if (!enabled) { return; } } - const plugin = getChannelPlugin(channel); + const plugin = getChannelSetupPlugin(channel); const adapter = getChannelOnboardingAdapter(channel); const label = plugin?.meta.label ?? catalogEntry?.meta.label ?? channel; const status = statusByChannel.get(channel); diff --git a/src/commands/onboarding/registry.ts b/src/commands/onboarding/registry.ts index cd660350911..3f7bea2da19 100644 --- a/src/commands/onboarding/registry.ts +++ b/src/commands/onboarding/registry.ts @@ -1,34 +1,26 @@ -import { discordOnboardingAdapter } from "../../../extensions/discord/src/onboarding.js"; -import { imessageOnboardingAdapter } from "../../../extensions/imessage/src/onboarding.js"; -import { signalOnboardingAdapter } from "../../../extensions/signal/src/onboarding.js"; -import { slackOnboardingAdapter } from "../../../extensions/slack/src/onboarding.js"; -import { telegramOnboardingAdapter } from "../../../extensions/telegram/src/onboarding.js"; -import { whatsappOnboardingAdapter } from "../../../extensions/whatsapp/src/onboarding.js"; -import { listChannelPlugins } from "../../channels/plugins/index.js"; +import { listChannelSetupPlugins } from "../../channels/plugins/setup-registry.js"; import type { ChannelChoice } from "../onboard-types.js"; import type { ChannelOnboardingAdapter } from "./types.js"; -const BUILTIN_ONBOARDING_ADAPTERS: ChannelOnboardingAdapter[] = [ - telegramOnboardingAdapter, - whatsappOnboardingAdapter, - discordOnboardingAdapter, - slackOnboardingAdapter, - signalOnboardingAdapter, - imessageOnboardingAdapter, -]; +function resolveChannelOnboardingAdapter( + plugin: (typeof listChannelSetupPlugins)[number], +): ChannelOnboardingAdapter | undefined { + if (plugin.onboarding) { + return plugin.onboarding; + } + return undefined; +} const CHANNEL_ONBOARDING_ADAPTERS = () => { - const fromRegistry = listChannelPlugins() - .map((plugin) => (plugin.onboarding ? ([plugin.id, plugin.onboarding] as const) : null)) - .filter((entry): entry is readonly [ChannelChoice, ChannelOnboardingAdapter] => Boolean(entry)); - - // Fall back to built-in adapters to keep onboarding working even when the plugin registry - // fails to populate (see #25545). - const fromBuiltins = BUILTIN_ONBOARDING_ADAPTERS.map( - (adapter) => [adapter.channel, adapter] as const, - ); - - return new Map([...fromBuiltins, ...fromRegistry]); + const adapters = new Map(); + for (const plugin of listChannelSetupPlugins()) { + const adapter = resolveChannelOnboardingAdapter(plugin); + if (!adapter) { + continue; + } + adapters.set(plugin.id, adapter); + } + return adapters; }; export function getChannelOnboardingAdapter( diff --git a/src/gateway/server-plugins.test.ts b/src/gateway/server-plugins.test.ts index 38f13cf6ac3..560392499c1 100644 --- a/src/gateway/server-plugins.test.ts +++ b/src/gateway/server-plugins.test.ts @@ -26,6 +26,7 @@ const createRegistry = (diagnostics: PluginDiagnostic[]): PluginRegistry => ({ hooks: [], typedHooks: [], channels: [], + channelSetups: [], commands: [], providers: [], gatewayHandlers: {}, diff --git a/src/gateway/server.agent.gateway-server-agent.mocks.ts b/src/gateway/server.agent.gateway-server-agent.mocks.ts index c3a33eca9ad..0e1f779ef4f 100644 --- a/src/gateway/server.agent.gateway-server-agent.mocks.ts +++ b/src/gateway/server.agent.gateway-server-agent.mocks.ts @@ -9,6 +9,7 @@ export const registryState: { registry: PluginRegistry } = { hooks: [], typedHooks: [], channels: [], + channelSetups: [], providers: [], gatewayHandlers: {}, httpHandlers: [], diff --git a/src/gateway/test-helpers.mocks.ts b/src/gateway/test-helpers.mocks.ts index c8032527294..17868ae0bca 100644 --- a/src/gateway/test-helpers.mocks.ts +++ b/src/gateway/test-helpers.mocks.ts @@ -144,6 +144,7 @@ const createStubPluginRegistry = (): PluginRegistry => ({ plugin: createStubChannelPlugin({ id: "bluebubbles", label: "BlueBubbles" }), }, ], + channelSetups: [], providers: [], gatewayHandlers: {}, httpRoutes: [], diff --git a/src/plugins/loader.ts b/src/plugins/loader.ts index 319b0ae90d7..b9ebc7f2a1e 100644 --- a/src/plugins/loader.ts +++ b/src/plugins/loader.ts @@ -847,13 +847,23 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi }); }; - if (!enableState.enabled) { + const registrationMode = enableState.enabled + ? "full" + : !validateOnly && manifestRecord.channels.length > 0 + ? "setup-only" + : null; + + if (!registrationMode) { record.status = "disabled"; record.error = enableState.reason; registry.plugins.push(record); seenIds.set(pluginId, candidate.origin); continue; } + if (!enableState.enabled) { + record.status = "disabled"; + record.error = enableState.reason; + } if (record.format === "bundle") { const unsupportedCapabilities = (record.bundleCapabilities ?? []).filter( @@ -878,10 +888,13 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi seenIds.set(pluginId, candidate.origin); continue; } - // Fast-path bundled memory plugins that are guaranteed disabled by slot policy. // This avoids opening/importing heavy memory plugin modules that will never register. - if (candidate.origin === "bundled" && manifestRecord.kind === "memory") { + if ( + registrationMode === "full" && + candidate.origin === "bundled" && + manifestRecord.kind === "memory" + ) { const earlyMemoryDecision = resolveMemorySlotDecision({ id: record.id, kind: "memory", @@ -966,24 +979,26 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi memorySlotMatched = true; } - const memoryDecision = resolveMemorySlotDecision({ - id: record.id, - kind: record.kind, - slot: memorySlot, - selectedId: selectedMemoryPluginId, - }); + if (registrationMode === "full") { + const memoryDecision = resolveMemorySlotDecision({ + id: record.id, + kind: record.kind, + slot: memorySlot, + selectedId: selectedMemoryPluginId, + }); - if (!memoryDecision.enabled) { - record.enabled = false; - record.status = "disabled"; - record.error = memoryDecision.reason; - registry.plugins.push(record); - seenIds.set(pluginId, candidate.origin); - continue; - } + if (!memoryDecision.enabled) { + record.enabled = false; + record.status = "disabled"; + record.error = memoryDecision.reason; + registry.plugins.push(record); + seenIds.set(pluginId, candidate.origin); + continue; + } - if (memoryDecision.selected && record.kind === "memory") { - selectedMemoryPluginId = record.id; + if (memoryDecision.selected && record.kind === "memory") { + selectedMemoryPluginId = record.id; + } } const validatedConfig = validatePluginConfig({ @@ -1014,6 +1029,7 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi config: cfg, pluginConfig: validatedConfig.value, hookPolicy: entry?.hooks, + registrationMode, }); try { diff --git a/src/plugins/registry.ts b/src/plugins/registry.ts index d754d928f15..4b28c277e05 100644 --- a/src/plugins/registry.ts +++ b/src/plugins/registry.ts @@ -85,6 +85,15 @@ export type PluginChannelRegistration = { rootDir?: string; }; +export type PluginChannelSetupRegistration = { + pluginId: string; + pluginName?: string; + plugin: ChannelPlugin; + source: string; + enabled: boolean; + rootDir?: string; +}; + export type PluginProviderRegistration = { pluginId: string; pluginName?: string; @@ -154,6 +163,7 @@ export type PluginRegistry = { hooks: PluginHookRegistration[]; typedHooks: TypedPluginHookRegistration[]; channels: PluginChannelRegistration[]; + channelSetups: PluginChannelSetupRegistration[]; providers: PluginProviderRegistration[]; gatewayHandlers: GatewayRequestHandlers; httpRoutes: PluginHttpRouteRegistration[]; @@ -173,6 +183,8 @@ type PluginTypedHookPolicy = { allowPromptInjection?: boolean; }; +type PluginRegistrationMode = "full" | "setup-only"; + const constrainLegacyPromptInjectionHook = ( handler: PluginHookHandlerMap["before_agent_start"], ): PluginHookHandlerMap["before_agent_start"] => { @@ -194,6 +206,7 @@ export function createEmptyPluginRegistry(): PluginRegistry { hooks: [], typedHooks: [], channels: [], + channelSetups: [], providers: [], gatewayHandlers: {}, httpRoutes: [], @@ -436,6 +449,7 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) { const registerChannel = ( record: PluginRecord, registration: OpenClawPluginChannelRegistration | ChannelPlugin, + mode: PluginRegistrationMode = "full", ) => { const normalized = typeof (registration as OpenClawPluginChannelRegistration).plugin === "object" @@ -452,17 +466,38 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) { }); return; } - const existing = registry.channels.find((entry) => entry.plugin.id === id); - if (existing) { + const existingRuntime = registry.channels.find((entry) => entry.plugin.id === id); + if (mode === "full" && existingRuntime) { pushDiagnostic({ level: "error", pluginId: record.id, source: record.source, - message: `channel already registered: ${id} (${existing.pluginId})`, + message: `channel already registered: ${id} (${existingRuntime.pluginId})`, + }); + return; + } + const existingSetup = registry.channelSetups.find((entry) => entry.plugin.id === id); + if (existingSetup) { + pushDiagnostic({ + level: "error", + pluginId: record.id, + source: record.source, + message: `channel setup already registered: ${id} (${existingSetup.pluginId})`, }); return; } record.channelIds.push(id); + registry.channelSetups.push({ + pluginId: record.id, + pluginName: record.name, + plugin, + source: record.source, + enabled: record.enabled, + rootDir: record.rootDir, + }); + if (mode === "setup-only") { + return; + } registry.channels.push({ pluginId: record.id, pluginName: record.name, @@ -667,8 +702,10 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) { config: OpenClawPluginApi["config"]; pluginConfig?: Record; hookPolicy?: PluginTypedHookPolicy; + registrationMode?: PluginRegistrationMode; }, ): OpenClawPluginApi => { + const registrationMode = params.registrationMode ?? "full"; return { id: record.id, name: record.name, @@ -680,31 +717,50 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) { pluginConfig: params.pluginConfig, runtime: registryParams.runtime, logger: normalizeLogger(registryParams.logger), - registerTool: (tool, opts) => registerTool(record, tool, opts), - registerHook: (events, handler, opts) => - registerHook(record, events, handler, opts, params.config), - registerHttpRoute: (params) => registerHttpRoute(record, params), - registerChannel: (registration) => registerChannel(record, registration), - registerProvider: (provider) => registerProvider(record, provider), - registerGatewayMethod: (method, handler) => registerGatewayMethod(record, method, handler), - registerCli: (registrar, opts) => registerCli(record, registrar, opts), - registerService: (service) => registerService(record, service), - registerInteractiveHandler: (registration) => { - const result = registerPluginInteractiveHandler(record.id, registration, { - pluginName: record.name, - pluginRoot: record.rootDir, - }); - if (!result.ok) { - pushDiagnostic({ - level: "warn", - pluginId: record.id, - source: record.source, - message: result.error ?? "interactive handler registration failed", - }); - } - }, - registerCommand: (command) => registerCommand(record, command), + registerTool: + registrationMode === "full" ? (tool, opts) => registerTool(record, tool, opts) : () => {}, + registerHook: + registrationMode === "full" + ? (events, handler, opts) => registerHook(record, events, handler, opts, params.config) + : () => {}, + registerHttpRoute: + registrationMode === "full" ? (params) => registerHttpRoute(record, params) : () => {}, + registerChannel: (registration) => registerChannel(record, registration, registrationMode), + registerProvider: + registrationMode === "full" ? (provider) => registerProvider(record, provider) : () => {}, + registerGatewayMethod: + registrationMode === "full" + ? (method, handler) => registerGatewayMethod(record, method, handler) + : () => {}, + registerCli: + registrationMode === "full" + ? (registrar, opts) => registerCli(record, registrar, opts) + : () => {}, + registerService: + registrationMode === "full" ? (service) => registerService(record, service) : () => {}, + registerInteractiveHandler: + registrationMode === "full" + ? (registration) => { + const result = registerPluginInteractiveHandler(record.id, registration, { + pluginName: record.name, + pluginRoot: record.rootDir, + }); + if (!result.ok) { + pushDiagnostic({ + level: "warn", + pluginId: record.id, + source: record.source, + message: result.error ?? "interactive handler registration failed", + }); + } + } + : () => {}, + registerCommand: + registrationMode === "full" ? (command) => registerCommand(record, command) : () => {}, registerContextEngine: (id, factory) => { + if (registrationMode !== "full") { + return; + } if (id === defaultSlotIdForKey("contextEngine")) { pushDiagnostic({ level: "error", @@ -728,7 +784,9 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) { }, resolvePath: (input: string) => resolveUserPath(input), on: (hookName, handler, opts) => - registerTypedHook(record, hookName, handler, opts, params.hookPolicy), + registrationMode === "full" + ? registerTypedHook(record, hookName, handler, opts, params.hookPolicy) + : undefined, }; }; diff --git a/src/test-utils/channel-plugins.ts b/src/test-utils/channel-plugins.ts index 38f850ab2a5..ebec4f2c747 100644 --- a/src/test-utils/channel-plugins.ts +++ b/src/test-utils/channel-plugins.ts @@ -18,6 +18,12 @@ export const createTestRegistry = (channels: TestChannelRegistration[] = []): Pl hooks: [], typedHooks: [], channels: channels as unknown as PluginRegistry["channels"], + channelSetups: channels.map((entry) => ({ + pluginId: entry.pluginId, + plugin: entry.plugin as PluginRegistry["channelSetups"][number]["plugin"], + source: entry.source, + enabled: true, + })), providers: [], gatewayHandlers: {}, httpRoutes: [],