diff --git a/src/agents/openai-ws-stream.test.ts b/src/agents/openai-ws-stream.test.ts index 14a05d02e19..73019004f6c 100644 --- a/src/agents/openai-ws-stream.test.ts +++ b/src/agents/openai-ws-stream.test.ts @@ -512,7 +512,9 @@ describe("convertTools", () => { parameters: { type: "object", properties: {}, additionalProperties: false }, }, ]; - const result = convertTools(tools as Parameters[0], { strict: true }); + const result = convertTools(tools as unknown as Parameters[0], { + strict: true, + }); expect(result[0]).toEqual({ type: "function", @@ -540,7 +542,9 @@ describe("convertTools", () => { }, }, ]; - const result = convertTools(tools as Parameters[0], { strict: true }); + const result = convertTools(tools as unknown as Parameters[0], { + strict: true, + }); expect(result[0]).toEqual({ type: "function", diff --git a/src/channels/chat-meta-shared.ts b/src/channels/chat-meta-shared.ts index d34befacedd..22502a0f688 100644 --- a/src/channels/chat-meta-shared.ts +++ b/src/channels/chat-meta-shared.ts @@ -1,4 +1,4 @@ -import { listBundledPluginMetadata } from "../plugins/bundled-plugin-metadata.js"; +import { listChannelCatalogEntries } from "../plugins/channel-catalog-registry.js"; import type { PluginPackageChannel } from "../plugins/manifest.js"; import { CHAT_CHANNEL_ORDER, type ChatChannelId } from "./ids.js"; import type { ChannelMeta } from "./plugins/types.js"; @@ -64,14 +64,8 @@ function toChatChannelMeta(params: { export function buildChatChannelMetaById(): Record { const entries = new Map(); - for (const entry of listBundledPluginMetadata({ - includeChannelConfigs: true, - includeSyntheticChannelConfigs: false, - })) { - const channel = - entry.packageManifest && "channel" in entry.packageManifest - ? entry.packageManifest.channel - : undefined; + for (const entry of listChannelCatalogEntries({ origin: "bundled" })) { + const channel = entry.channel; if (!channel) { continue; } diff --git a/src/channels/ids.test.ts b/src/channels/ids.test.ts index 32fec40db26..2bf4454b5b5 100644 --- a/src/channels/ids.test.ts +++ b/src/channels/ids.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { listBundledPluginMetadata } from "../plugins/bundled-plugin-metadata.js"; +import { listChannelCatalogEntries } from "../plugins/channel-catalog-registry.js"; import { CHAT_CHANNEL_ALIASES, CHAT_CHANNEL_ORDER, @@ -10,14 +10,8 @@ import { function collectBundledChatChannelAliases(): Record { const aliases = new Map(); - for (const entry of listBundledPluginMetadata({ - includeChannelConfigs: true, - includeSyntheticChannelConfigs: false, - })) { - const channel = - entry.packageManifest && "channel" in entry.packageManifest - ? entry.packageManifest.channel - : undefined; + for (const entry of listChannelCatalogEntries({ origin: "bundled" })) { + const channel = entry.channel; const rawId = channel?.id?.trim(); if (!rawId || !CHAT_CHANNEL_ORDER.includes(rawId)) { continue; diff --git a/src/channels/ids.ts b/src/channels/ids.ts index ff00e1f5f4e..d45d3100e51 100644 --- a/src/channels/ids.ts +++ b/src/channels/ids.ts @@ -1,4 +1,4 @@ -import { listBundledPluginMetadata } from "../plugins/bundled-plugin-metadata.js"; +import { listChannelCatalogEntries } from "../plugins/channel-catalog-registry.js"; export type ChatChannelId = string; @@ -14,17 +14,10 @@ function normalizeChannelKey(raw?: string | null): string | undefined { } function listBundledChatChannelEntries(): BundledChatChannelEntry[] { - return listBundledPluginMetadata({ - includeChannelConfigs: false, - includeSyntheticChannelConfigs: false, - }) - .flatMap((entry) => { - const channel = - entry.packageManifest && "channel" in entry.packageManifest - ? entry.packageManifest.channel - : undefined; - const id = normalizeChannelKey(channel?.id); - if (!channel || !id) { + return listChannelCatalogEntries({ origin: "bundled" }) + .flatMap(({ channel }) => { + const id = normalizeChannelKey(channel.id); + if (!id) { return []; } const aliases = (channel.aliases ?? []) diff --git a/src/channels/plugins/bundled-ids.ts b/src/channels/plugins/bundled-ids.ts index 7bccb4c7717..2b4b32d036f 100644 --- a/src/channels/plugins/bundled-ids.ts +++ b/src/channels/plugins/bundled-ids.ts @@ -1,11 +1,7 @@ -import { listBundledPluginMetadata } from "../../plugins/bundled-plugin-metadata.js"; +import { listChannelCatalogEntries } from "../../plugins/channel-catalog-registry.js"; -export const BUNDLED_CHANNEL_PLUGIN_IDS = listBundledPluginMetadata({ - includeChannelConfigs: false, - includeSyntheticChannelConfigs: false, -}) - .filter(({ manifest }) => Array.isArray(manifest.channels) && manifest.channels.length > 0) - .map(({ manifest }) => manifest.id) +export const BUNDLED_CHANNEL_PLUGIN_IDS = listChannelCatalogEntries({ origin: "bundled" }) + .map((entry) => entry.pluginId) .toSorted((left, right) => left.localeCompare(right)); export function listBundledChannelPluginIds(): string[] { diff --git a/src/channels/plugins/bundled.ts b/src/channels/plugins/bundled.ts index cfac448c8e7..0e029e531f5 100644 --- a/src/channels/plugins/bundled.ts +++ b/src/channels/plugins/bundled.ts @@ -8,7 +8,6 @@ import type { BundledChannelEntryContract, BundledChannelSetupEntryContract, } from "../../plugin-sdk/channel-entry-contract.js"; -import { discoverOpenClawPlugins } from "../../plugins/discovery.js"; import { loadPluginManifestRegistry } from "../../plugins/manifest-registry.js"; import type { PluginRuntime } from "../../plugins/runtime/types.js"; import { @@ -24,20 +23,6 @@ type GeneratedBundledChannelEntry = { setupEntry?: BundledChannelSetupEntryContract; }; -type BundledChannelDiscoveryCandidate = { - rootDir: string; - packageManifest?: { - extensions?: string[]; - }; -}; - -const BUNDLED_CHANNEL_ENTRY_BASENAMES = [ - "channel-entry.ts", - "channel-entry.mts", - "channel-entry.js", - "channel-entry.mjs", -] as const; - const log = createSubsystemLogger("channels"); const nodeRequire = createRequire(import.meta.url); @@ -155,53 +140,19 @@ function resolveCompiledBundledModulePath(modulePath: string): string { : modulePath; } -function resolvePreferredBundledChannelSource( - candidate: BundledChannelDiscoveryCandidate, - manifest: ReturnType["plugins"][number], -): string { - for (const basename of BUNDLED_CHANNEL_ENTRY_BASENAMES) { - const preferred = resolveCompiledBundledModulePath(path.resolve(candidate.rootDir, basename)); - if (fs.existsSync(preferred)) { - return preferred; - } - } - const declaredEntry = candidate.packageManifest?.extensions?.find( - (entry): entry is string => typeof entry === "string" && entry.trim().length > 0, - ); - if (declaredEntry) { - return resolveCompiledBundledModulePath(path.resolve(candidate.rootDir, declaredEntry)); - } - return resolveCompiledBundledModulePath(manifest.source); -} - function loadGeneratedBundledChannelEntries(): readonly GeneratedBundledChannelEntry[] { - const discovery = discoverOpenClawPlugins({ cache: false }); - const manifestRegistry = loadPluginManifestRegistry({ - cache: false, - config: {}, - candidates: discovery.candidates, - diagnostics: discovery.diagnostics, - }); - const manifestByRoot = new Map( - manifestRegistry.plugins.map((plugin) => [plugin.rootDir, plugin] as const), - ); - const seenIds = new Set(); + const manifestRegistry = loadPluginManifestRegistry({ cache: false, config: {} }); const entries: GeneratedBundledChannelEntry[] = []; - for (const candidate of discovery.candidates) { - const manifest = manifestByRoot.get(candidate.rootDir); - if (!manifest || manifest.origin !== "bundled" || manifest.channels.length === 0) { + for (const manifest of manifestRegistry.plugins) { + if (manifest.origin !== "bundled" || manifest.channels.length === 0) { continue; } - if (seenIds.has(manifest.id)) { - continue; - } - seenIds.add(manifest.id); try { - const sourcePath = resolvePreferredBundledChannelSource(candidate, manifest); + const sourcePath = resolveCompiledBundledModulePath(manifest.source); const entry = resolveChannelPluginModuleEntry( - loadBundledModule(sourcePath, candidate.rootDir), + loadBundledModule(sourcePath, manifest.rootDir), ); if (!entry) { log.warn( @@ -213,7 +164,7 @@ function loadGeneratedBundledChannelEntries(): readonly GeneratedBundledChannelE ? resolveChannelSetupModuleEntry( loadBundledModule( resolveCompiledBundledModulePath(manifest.setupSource), - candidate.rootDir, + manifest.rootDir, ), ) : null; @@ -225,7 +176,7 @@ function loadGeneratedBundledChannelEntries(): readonly GeneratedBundledChannelE } catch (error) { const detail = error instanceof Error ? error.message : String(error); log.warn( - `[channels] failed to load bundled channel ${manifest.id} from ${candidate.source}: ${detail}`, + `[channels] failed to load bundled channel ${manifest.id} from ${manifest.source}: ${detail}`, ); } } diff --git a/src/channels/plugins/catalog.ts b/src/channels/plugins/catalog.ts index 473c4b31019..009efb6c3ea 100644 --- a/src/channels/plugins/catalog.ts +++ b/src/channels/plugins/catalog.ts @@ -2,11 +2,9 @@ import fs from "node:fs"; import path from "node:path"; import { MANIFEST_KEY } from "../../compat/legacy-names.js"; import { resolveOpenClawPackageRootSync } from "../../infra/openclaw-root.js"; -import { resolveBundledPluginsDir } from "../../plugins/bundled-dir.js"; -import { discoverOpenClawPlugins } from "../../plugins/discovery.js"; -import { loadPluginManifest } from "../../plugins/manifest.js"; +import { listChannelCatalogEntries } from "../../plugins/channel-catalog-registry.js"; import type { OpenClawPackageManifest } from "../../plugins/manifest.js"; -import type { PackageManifest as PluginPackageManifest } from "../../plugins/manifest.js"; +import type { PluginPackageChannel, PluginPackageInstall } from "../../plugins/manifest.js"; import type { PluginOrigin } from "../../plugins/types.js"; import { isRecord, resolveConfigDir, resolveUserPath } from "../../utils.js"; import type { ChannelMeta } from "./types.js"; @@ -223,20 +221,20 @@ function toChannelMeta(params: { } function resolveInstallInfo(params: { - manifest: OpenClawPackageManifest; + install?: PluginPackageInstall; packageName?: string; packageDir?: string; workspaceDir?: string; }): ChannelPluginCatalogEntry["install"] | null { - const npmSpec = params.manifest.install?.npmSpec?.trim() ?? params.packageName?.trim(); + const npmSpec = params.install?.npmSpec?.trim() ?? params.packageName?.trim(); if (!npmSpec) { return null; } - let localPath = params.manifest.install?.localPath?.trim() || undefined; + let localPath = params.install?.localPath?.trim() || undefined; if (!localPath && params.workspaceDir && params.packageDir) { localPath = path.relative(params.workspaceDir, params.packageDir) || undefined; } - const defaultChoice = params.manifest.install?.defaultChoice ?? (localPath ? "local" : "npm"); + const defaultChoice = params.install?.defaultChoice ?? (localPath ? "local" : "npm"); return { npmSpec, ...(localPath ? { localPath } : {}), @@ -244,59 +242,46 @@ function resolveInstallInfo(params: { }; } -function resolveCatalogPluginId(params: { - packageDir?: string; - rootDir?: string; - origin?: PluginOrigin; -}): string | undefined { - const manifestDir = params.packageDir ?? params.rootDir; - if (manifestDir) { - const manifest = loadPluginManifest(manifestDir, params.origin !== "bundled"); - if (manifest.ok) { - return manifest.manifest.id; - } - } - return undefined; +function resolveCatalogPluginId(params: { pluginId?: string }): string | undefined { + return params.pluginId?.trim() || undefined; } -function buildCatalogEntry(candidate: { +function buildCatalogEntryFromManifest(params: { + pluginId?: string; packageName?: string; packageDir?: string; - rootDir?: string; origin?: PluginOrigin; workspaceDir?: string; - packageManifest?: OpenClawPackageManifest; + channel?: PluginPackageChannel; + install?: PluginPackageInstall; }): ChannelPluginCatalogEntry | null { - const manifest = candidate.packageManifest; - if (!manifest?.channel) { + if (!params.channel) { return null; } - const id = manifest.channel.id?.trim(); + const id = params.channel.id?.trim(); if (!id) { return null; } - const meta = toChannelMeta({ channel: manifest.channel, id }); + const meta = toChannelMeta({ channel: params.channel, id }); if (!meta) { return null; } const install = resolveInstallInfo({ - manifest, - packageName: candidate.packageName, - packageDir: candidate.packageDir, - workspaceDir: candidate.workspaceDir, + install: params.install, + packageName: params.packageName, + packageDir: params.packageDir, + workspaceDir: params.workspaceDir, }); if (!install) { return null; } const pluginId = resolveCatalogPluginId({ - packageDir: candidate.packageDir, - rootDir: candidate.rootDir, - origin: candidate.origin, + pluginId: params.pluginId, }); return { id, ...(pluginId ? { pluginId } : {}), - ...(candidate.origin ? { origin: candidate.origin } : {}), + ...(params.origin ? { origin: params.origin } : {}), meta, install, }; @@ -304,52 +289,13 @@ function buildCatalogEntry(candidate: { function buildExternalCatalogEntry(entry: ExternalCatalogEntry): ChannelPluginCatalogEntry | null { const manifest = entry[MANIFEST_KEY]; - return buildCatalogEntry({ + return buildCatalogEntryFromManifest({ packageName: entry.name, - packageManifest: manifest, + channel: manifest?.channel, + install: manifest?.install, }); } -function loadBundledMetadataCatalogEntries(options: CatalogOptions): ChannelPluginCatalogEntry[] { - const bundledDir = resolveBundledPluginsDir(options.env ?? process.env); - if (!bundledDir || !fs.existsSync(bundledDir)) { - return []; - } - - const entries: ChannelPluginCatalogEntry[] = []; - for (const dirent of fs.readdirSync(bundledDir, { withFileTypes: true })) { - if (!dirent.isDirectory()) { - continue; - } - const pluginDir = path.join(bundledDir, dirent.name); - const packageJsonPath = path.join(pluginDir, "package.json"); - if (!fs.existsSync(packageJsonPath)) { - continue; - } - - let packageJson: PluginPackageManifest; - try { - packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")) as PluginPackageManifest; - } catch { - continue; - } - - const entry = buildCatalogEntry({ - packageName: packageJson.name, - packageDir: pluginDir, - rootDir: pluginDir, - origin: "bundled", - workspaceDir: options.workspaceDir, - packageManifest: packageJson.openclaw, - }); - if (entry) { - entries.push(entry); - } - } - - return entries; -} - export function buildChannelUiCatalog( plugins: Array<{ id: string; meta: ChannelMeta }>, ): ChannelUiCatalog { @@ -381,17 +327,25 @@ export function buildChannelUiCatalog( export function listChannelPluginCatalogEntries( options: CatalogOptions = {}, ): ChannelPluginCatalogEntry[] { - const discovery = discoverOpenClawPlugins({ + const manifestEntries = listChannelCatalogEntries({ workspaceDir: options.workspaceDir, env: options.env, }); const resolved = new Map(); - for (const candidate of discovery.candidates) { + for (const candidate of manifestEntries) { if (options.excludeWorkspace && candidate.origin === "workspace") { continue; } - const entry = buildCatalogEntry(candidate); + const entry = buildCatalogEntryFromManifest({ + pluginId: candidate.pluginId, + packageName: candidate.packageName, + packageDir: candidate.rootDir, + origin: candidate.origin, + workspaceDir: candidate.workspaceDir ?? options.workspaceDir, + channel: candidate.channel, + install: candidate.install, + }); if (!entry) { continue; } @@ -402,14 +356,6 @@ export function listChannelPluginCatalogEntries( } } - for (const entry of loadBundledMetadataCatalogEntries(options)) { - const priority = FALLBACK_CATALOG_PRIORITY; - const existing = resolved.get(entry.id); - if (!existing || priority < existing.priority) { - resolved.set(entry.id, { entry, priority }); - } - } - for (const entry of loadOfficialCatalogEntries(options)) { const priority = FALLBACK_CATALOG_PRIORITY; const existing = resolved.get(entry.id); diff --git a/src/gateway/test-helpers.mocks.ts b/src/gateway/test-helpers.mocks.ts index 21b9baf60d6..ab010a41ae7 100644 --- a/src/gateway/test-helpers.mocks.ts +++ b/src/gateway/test-helpers.mocks.ts @@ -3,216 +3,61 @@ import fsSync from "node:fs"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import { Mock, vi } from "vitest"; -import type { MsgContext } from "../auto-reply/templating.js"; -import type { GetReplyOptions, ReplyPayload } from "../auto-reply/types.js"; +import { vi } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; import { applyPluginAutoEnable } from "../config/plugin-auto-enable.js"; import type { AgentBinding } from "../config/types.agents.js"; -import type { HooksConfig } from "../config/types.hooks.js"; -import type { TailscaleWhoisIdentity } from "../infra/tailscale.js"; -import type { PluginRegistry } from "../plugins/registry.js"; -import { setActivePluginRegistry } from "../plugins/runtime.js"; -import { resolveGlobalSingleton } from "../shared/global-singleton.js"; -import { createDefaultGatewayTestChannels } from "./test-helpers.channels.js"; -import { createDefaultGatewayTestSpeechProviders } from "./test-helpers.speech.js"; +import { + getTestPluginRegistry, + resetTestPluginRegistry, + setTestPluginRegistry, +} from "./test-helpers.plugin-registry.js"; +import { + agentCommand, + cronIsolatedRun, + dispatchInboundMessageMock, + embeddedRunMock, + type GetReplyFromConfigFn, + getReplyFromConfig, + getGatewayTestHoistedState, + mockGetReplyFromConfigOnce, + piSdkMock, + runBtwSideQuestion, + sendWhatsAppMock, + sessionStoreSaveDelayMs, + setTestConfigRoot, + testConfigRoot, + testIsNixMode, + testState, + testTailnetIPv4, + testTailscaleWhois, + type RunBtwSideQuestionFn, +} from "./test-helpers.runtime-state.js"; + +export { getTestPluginRegistry, resetTestPluginRegistry, setTestPluginRegistry }; +export { + agentCommand, + cronIsolatedRun, + dispatchInboundMessageMock, + embeddedRunMock, + getReplyFromConfig, + mockGetReplyFromConfigOnce, + piSdkMock, + runBtwSideQuestion, + sendWhatsAppMock, + sessionStoreSaveDelayMs, + setTestConfigRoot, + testIsNixMode, + testState, + testTailnetIPv4, + testTailscaleWhois, +}; function buildBundledPluginModuleId(pluginId: string, artifactBasename: string): string { return ["..", "..", "extensions", pluginId, artifactBasename].join("/"); } -type GetReplyFromConfigFn = ( - ctx: MsgContext, - opts?: GetReplyOptions, - configOverride?: OpenClawConfig, -) => Promise; -type CronIsolatedRunFn = (...args: unknown[]) => Promise<{ status: string; summary: string }>; -type AgentCommandFn = (...args: unknown[]) => Promise; -type SendWhatsAppFn = (...args: unknown[]) => Promise<{ messageId: string; toJid: string }>; -type RunBtwSideQuestionFn = (...args: unknown[]) => Promise; -type DispatchInboundMessageFn = (...args: unknown[]) => Promise; - -const createStubPluginRegistry = (): PluginRegistry => ({ - plugins: [], - tools: [], - hooks: [], - typedHooks: [], - channels: createDefaultGatewayTestChannels(), - channelSetups: [], - providers: [], - speechProviders: createDefaultGatewayTestSpeechProviders(), - realtimeTranscriptionProviders: [], - realtimeVoiceProviders: [], - mediaUnderstandingProviders: [], - imageGenerationProviders: [], - videoGenerationProviders: [], - webFetchProviders: [], - webSearchProviders: [], - memoryEmbeddingProviders: [], - gatewayHandlers: {}, - httpRoutes: [], - cliRegistrars: [], - services: [], - commands: [], - conversationBindingResolvedHandlers: [], - diagnostics: [], -}); - -const GATEWAY_TEST_PLUGIN_REGISTRY_STATE_KEY = Symbol.for( - "openclaw.gatewayTestHelpers.pluginRegistryState", -); -const GATEWAY_TEST_CONFIG_ROOT_KEY = Symbol.for("openclaw.gatewayTestHelpers.configRoot"); - -const hoisted = vi.hoisted(() => { - const key = Symbol.for("openclaw.gatewayTestHelpers.hoisted"); - const store = globalThis as Record; - if (Object.prototype.hasOwnProperty.call(store, key)) { - return store[key] as { - testTailnetIPv4: { value: string | undefined }; - piSdkMock: { - enabled: boolean; - discoverCalls: number; - models: Array<{ - id: string; - name?: string; - provider: string; - contextWindow?: number; - reasoning?: boolean; - }>; - }; - cronIsolatedRun: Mock; - agentCommand: Mock; - runBtwSideQuestion: Mock; - dispatchInboundMessage: Mock; - testIsNixMode: { value: boolean }; - sessionStoreSaveDelayMs: { value: number }; - embeddedRunMock: { - activeIds: Set; - abortCalls: string[]; - waitCalls: string[]; - waitResults: Map; - }; - testTailscaleWhois: { value: TailscaleWhoisIdentity | null }; - getReplyFromConfig: Mock; - sendWhatsAppMock: Mock; - testState: { - agentConfig: Record | undefined; - agentsConfig: Record | undefined; - bindingsConfig: AgentBinding[] | undefined; - channelsConfig: Record | undefined; - sessionStorePath: string | undefined; - sessionConfig: Record | undefined; - allowFrom: string[] | undefined; - cronStorePath: string | undefined; - cronEnabled: boolean | undefined; - gatewayBind: "auto" | "lan" | "tailnet" | "loopback" | undefined; - gatewayAuth: Record | undefined; - gatewayControlUi: Record | undefined; - hooksConfig: HooksConfig | undefined; - canvasHostPort: number | undefined; - legacyIssues: Array<{ path: string; message: string }>; - legacyParsed: Record; - migrationConfig: Record | null; - migrationChanges: string[]; - }; - }; - } - const created = { - testTailnetIPv4: { value: undefined as string | undefined }, - piSdkMock: { - enabled: false, - discoverCalls: 0, - models: [] as Array<{ - id: string; - name?: string; - provider: string; - contextWindow?: number; - reasoning?: boolean; - }>, - }, - cronIsolatedRun: vi.fn(async () => ({ status: "ok", summary: "ok" })), - agentCommand: vi.fn().mockResolvedValue(undefined), - runBtwSideQuestion: vi.fn().mockResolvedValue(undefined), - dispatchInboundMessage: vi.fn(), - testIsNixMode: { value: false }, - sessionStoreSaveDelayMs: { value: 0 }, - embeddedRunMock: { - activeIds: new Set(), - abortCalls: [] as string[], - waitCalls: [] as string[], - waitResults: new Map(), - }, - testTailscaleWhois: { value: null as TailscaleWhoisIdentity | null }, - getReplyFromConfig: vi.fn().mockResolvedValue(undefined), - sendWhatsAppMock: vi.fn().mockResolvedValue({ messageId: "msg-1", toJid: "jid-1" }), - testState: { - agentConfig: undefined as Record | undefined, - agentsConfig: undefined as Record | undefined, - bindingsConfig: undefined as AgentBinding[] | undefined, - channelsConfig: undefined as Record | undefined, - sessionStorePath: undefined as string | undefined, - sessionConfig: undefined as Record | undefined, - allowFrom: undefined as string[] | undefined, - cronStorePath: undefined as string | undefined, - cronEnabled: false as boolean | undefined, - gatewayBind: undefined as "auto" | "lan" | "tailnet" | "loopback" | undefined, - gatewayAuth: undefined as Record | undefined, - gatewayControlUi: undefined as Record | undefined, - hooksConfig: undefined as HooksConfig | undefined, - canvasHostPort: undefined as number | undefined, - legacyIssues: [] as Array<{ path: string; message: string }>, - legacyParsed: {} as Record, - migrationConfig: null as Record | null, - migrationChanges: [] as string[], - }, - }; - store[key] = created; - return created; -}); - -const pluginRegistryState = resolveGlobalSingleton(GATEWAY_TEST_PLUGIN_REGISTRY_STATE_KEY, () => ({ - registry: createStubPluginRegistry(), -})); -setActivePluginRegistry(pluginRegistryState.registry); - -export const setTestPluginRegistry = (registry: PluginRegistry) => { - pluginRegistryState.registry = registry; - setActivePluginRegistry(registry); -}; - -export const resetTestPluginRegistry = () => { - pluginRegistryState.registry = createStubPluginRegistry(); - setActivePluginRegistry(pluginRegistryState.registry); -}; - -const testConfigRoot = resolveGlobalSingleton(GATEWAY_TEST_CONFIG_ROOT_KEY, () => ({ - value: path.join(os.tmpdir(), `openclaw-gateway-test-${process.pid}-${crypto.randomUUID()}`), -})); - -export const setTestConfigRoot = (root: string) => { - testConfigRoot.value = root; - process.env.OPENCLAW_CONFIG_PATH = path.join(root, "openclaw.json"); -}; - -export const testTailnetIPv4 = hoisted.testTailnetIPv4; -export const testTailscaleWhois = hoisted.testTailscaleWhois; -export const piSdkMock = hoisted.piSdkMock; -export const cronIsolatedRun: Mock = hoisted.cronIsolatedRun; -export const agentCommand: Mock = hoisted.agentCommand; -export const runBtwSideQuestion: Mock = hoisted.runBtwSideQuestion; -export const dispatchInboundMessageMock: Mock = - hoisted.dispatchInboundMessage; -export const getReplyFromConfig: Mock = hoisted.getReplyFromConfig; -export const mockGetReplyFromConfigOnce = (impl: GetReplyFromConfigFn) => { - getReplyFromConfig.mockImplementationOnce(impl); -}; -export const sendWhatsAppMock: Mock = hoisted.sendWhatsAppMock; - -export const testState = hoisted.testState; - -export const testIsNixMode = hoisted.testIsNixMode; -export const sessionStoreSaveDelayMs = hoisted.sessionStoreSaveDelayMs; -export const embeddedRunMock = hoisted.embeddedRunMock; +const gatewayTestHoisted = getGatewayTestHoistedState(); function createEmbeddedRunMockExports() { return { @@ -462,7 +307,7 @@ vi.mock("../config/config.js", async () => { } const canvasHost = Object.keys(fileCanvasHost).length > 0 ? fileCanvasHost : undefined; - const hooks = testState.hooksConfig ?? (baseConfig.hooks as HooksConfig | undefined); + const hooks = testState.hooksConfig ?? baseConfig.hooks; const fileCron = baseConfig.cron && typeof baseConfig.cron === "object" && !Array.isArray(baseConfig.cron) @@ -665,9 +510,9 @@ vi.mock("../commands/status.js", () => ({ })); vi.mock(buildBundledPluginModuleId("whatsapp", "runtime-api.js"), () => ({ sendMessageWhatsApp: (...args: unknown[]) => - (hoisted.sendWhatsAppMock as (...args: unknown[]) => unknown)(...args), + (gatewayTestHoisted.sendWhatsAppMock as (...args: unknown[]) => unknown)(...args), sendPollWhatsApp: (...args: unknown[]) => - (hoisted.sendWhatsAppMock as (...args: unknown[]) => unknown)(...args), + (gatewayTestHoisted.sendWhatsAppMock as (...args: unknown[]) => unknown)(...args), })); vi.mock("../channels/web/index.js", async () => { const actual = await vi.importActual( @@ -676,7 +521,7 @@ vi.mock("../channels/web/index.js", async () => { return { ...actual, sendMessageWhatsApp: (...args: unknown[]) => - (hoisted.sendWhatsAppMock as (...args: unknown[]) => unknown)(...args), + (gatewayTestHoisted.sendWhatsAppMock as (...args: unknown[]) => unknown)(...args), }; }); vi.mock("../commands/agent.js", () => ({ @@ -685,11 +530,11 @@ vi.mock("../commands/agent.js", () => ({ })); vi.mock("../agents/btw.js", () => ({ runBtwSideQuestion: (...args: Parameters) => - hoisted.runBtwSideQuestion(...args), + gatewayTestHoisted.runBtwSideQuestion(...args), })); vi.mock("/src/agents/btw.js", () => ({ runBtwSideQuestion: (...args: Parameters) => - hoisted.runBtwSideQuestion(...args), + gatewayTestHoisted.runBtwSideQuestion(...args), })); vi.mock("../auto-reply/dispatch.js", async () => { const actual = await vi.importActual( @@ -698,9 +543,9 @@ vi.mock("../auto-reply/dispatch.js", async () => { return { ...actual, dispatchInboundMessage: (...args: Parameters) => { - const impl = hoisted.dispatchInboundMessage.getMockImplementation(); + const impl = gatewayTestHoisted.dispatchInboundMessage.getMockImplementation(); return impl - ? hoisted.dispatchInboundMessage(...args) + ? gatewayTestHoisted.dispatchInboundMessage(...args) : actual.dispatchInboundMessage(...args); }, }; @@ -712,29 +557,29 @@ vi.mock("/src/auto-reply/dispatch.js", async () => { return { ...actual, dispatchInboundMessage: (...args: Parameters) => { - const impl = hoisted.dispatchInboundMessage.getMockImplementation(); + const impl = gatewayTestHoisted.dispatchInboundMessage.getMockImplementation(); return impl - ? hoisted.dispatchInboundMessage(...args) + ? gatewayTestHoisted.dispatchInboundMessage(...args) : actual.dispatchInboundMessage(...args); }, }; }); vi.mock("../auto-reply/reply.js", () => ({ getReplyFromConfig: (...args: Parameters) => - hoisted.getReplyFromConfig(...args), + gatewayTestHoisted.getReplyFromConfig(...args), })); vi.mock("/src/auto-reply/reply.js", () => ({ getReplyFromConfig: (...args: Parameters) => - hoisted.getReplyFromConfig(...args), + gatewayTestHoisted.getReplyFromConfig(...args), })); vi.mock("../auto-reply/reply/get-reply-from-config.runtime.js", () => ({ getReplyFromConfig: (...args: Parameters) => - hoisted.getReplyFromConfig(...args), + gatewayTestHoisted.getReplyFromConfig(...args), })); vi.mock("/src/auto-reply/reply/get-reply-from-config.runtime.js", () => ({ getReplyFromConfig: (...args: Parameters) => - hoisted.getReplyFromConfig(...args), + gatewayTestHoisted.getReplyFromConfig(...args), })); vi.mock("../cli/deps.js", async () => { const actual = await vi.importActual("../cli/deps.js"); @@ -744,7 +589,7 @@ vi.mock("../cli/deps.js", async () => { createDefaultDeps: () => ({ ...base, sendMessageWhatsApp: (...args: unknown[]) => - (hoisted.sendWhatsAppMock as (...args: unknown[]) => unknown)(...args), + (gatewayTestHoisted.sendWhatsAppMock as (...args: unknown[]) => unknown)(...args), }), }; }); @@ -754,16 +599,16 @@ vi.mock("../plugins/loader.js", async () => { await vi.importActual("../plugins/loader.js"); return { ...actual, - loadOpenClawPlugins: () => pluginRegistryState.registry, + loadOpenClawPlugins: () => getTestPluginRegistry(), }; }); vi.mock("../plugins/runtime/runtime-web-channel-plugin.js", () => ({ sendWebChannelMessage: (...args: unknown[]) => - (hoisted.sendWhatsAppMock as (...args: unknown[]) => unknown)(...args), + (gatewayTestHoisted.sendWhatsAppMock as (...args: unknown[]) => unknown)(...args), })); vi.mock("/src/plugins/runtime/runtime-web-channel-plugin.js", () => ({ sendWebChannelMessage: (...args: unknown[]) => - (hoisted.sendWhatsAppMock as (...args: unknown[]) => unknown)(...args), + (gatewayTestHoisted.sendWhatsAppMock as (...args: unknown[]) => unknown)(...args), })); process.env.OPENCLAW_SKIP_CHANNELS = "1"; diff --git a/src/gateway/test-helpers.plugin-registry.ts b/src/gateway/test-helpers.plugin-registry.ts new file mode 100644 index 00000000000..c3c39398cc5 --- /dev/null +++ b/src/gateway/test-helpers.plugin-registry.ts @@ -0,0 +1,57 @@ +import type { PluginRegistry } from "../plugins/registry.js"; +import { setActivePluginRegistry } from "../plugins/runtime.js"; +import { resolveGlobalSingleton } from "../shared/global-singleton.js"; +import { createDefaultGatewayTestChannels } from "./test-helpers.channels.js"; +import { createDefaultGatewayTestSpeechProviders } from "./test-helpers.speech.js"; + +function createStubPluginRegistry(): PluginRegistry { + return { + plugins: [], + tools: [], + hooks: [], + typedHooks: [], + channels: createDefaultGatewayTestChannels(), + channelSetups: [], + providers: [], + speechProviders: createDefaultGatewayTestSpeechProviders(), + realtimeTranscriptionProviders: [], + realtimeVoiceProviders: [], + mediaUnderstandingProviders: [], + imageGenerationProviders: [], + videoGenerationProviders: [], + webFetchProviders: [], + webSearchProviders: [], + memoryEmbeddingProviders: [], + gatewayHandlers: {}, + httpRoutes: [], + cliRegistrars: [], + services: [], + commands: [], + conversationBindingResolvedHandlers: [], + diagnostics: [], + }; +} + +const GATEWAY_TEST_PLUGIN_REGISTRY_STATE_KEY = Symbol.for( + "openclaw.gatewayTestHelpers.pluginRegistryState", +); + +const pluginRegistryState = resolveGlobalSingleton(GATEWAY_TEST_PLUGIN_REGISTRY_STATE_KEY, () => ({ + registry: createStubPluginRegistry(), +})); + +setActivePluginRegistry(pluginRegistryState.registry); + +export function setTestPluginRegistry(registry: PluginRegistry): void { + pluginRegistryState.registry = registry; + setActivePluginRegistry(registry); +} + +export function resetTestPluginRegistry(): void { + pluginRegistryState.registry = createStubPluginRegistry(); + setActivePluginRegistry(pluginRegistryState.registry); +} + +export function getTestPluginRegistry(): PluginRegistry { + return pluginRegistryState.registry; +} diff --git a/src/gateway/test-helpers.runtime-state.ts b/src/gateway/test-helpers.runtime-state.ts new file mode 100644 index 00000000000..b509801cbcf --- /dev/null +++ b/src/gateway/test-helpers.runtime-state.ts @@ -0,0 +1,159 @@ +import crypto from "node:crypto"; +import os from "node:os"; +import path from "node:path"; +import { Mock, vi } from "vitest"; +import type { MsgContext } from "../auto-reply/templating.js"; +import type { GetReplyOptions, ReplyPayload } from "../auto-reply/types.js"; +import type { OpenClawConfig } from "../config/config.js"; +import type { AgentBinding } from "../config/types.agents.js"; +import type { HooksConfig } from "../config/types.hooks.js"; +import type { TailscaleWhoisIdentity } from "../infra/tailscale.js"; +import { resolveGlobalSingleton } from "../shared/global-singleton.js"; + +export type GetReplyFromConfigFn = ( + ctx: MsgContext, + opts?: GetReplyOptions, + configOverride?: OpenClawConfig, +) => Promise; +export type CronIsolatedRunFn = ( + ...args: unknown[] +) => Promise<{ status: string; summary: string }>; +export type AgentCommandFn = (...args: unknown[]) => Promise; +export type SendWhatsAppFn = (...args: unknown[]) => Promise<{ messageId: string; toJid: string }>; +export type RunBtwSideQuestionFn = (...args: unknown[]) => Promise; +export type DispatchInboundMessageFn = (...args: unknown[]) => Promise; + +const GATEWAY_TEST_CONFIG_ROOT_KEY = Symbol.for("openclaw.gatewayTestHelpers.configRoot"); + +export type GatewayTestHoistedState = { + testTailnetIPv4: { value: string | undefined }; + piSdkMock: { + enabled: boolean; + discoverCalls: number; + models: Array<{ + id: string; + name?: string; + provider: string; + contextWindow?: number; + reasoning?: boolean; + }>; + }; + cronIsolatedRun: Mock; + agentCommand: Mock; + runBtwSideQuestion: Mock; + dispatchInboundMessage: Mock; + testIsNixMode: { value: boolean }; + sessionStoreSaveDelayMs: { value: number }; + embeddedRunMock: { + activeIds: Set; + abortCalls: string[]; + waitCalls: string[]; + waitResults: Map; + }; + testTailscaleWhois: { value: TailscaleWhoisIdentity | null }; + getReplyFromConfig: Mock; + sendWhatsAppMock: Mock; + testState: { + agentConfig: Record | undefined; + agentsConfig: Record | undefined; + bindingsConfig: AgentBinding[] | undefined; + channelsConfig: Record | undefined; + sessionStorePath: string | undefined; + sessionConfig: Record | undefined; + allowFrom: string[] | undefined; + cronStorePath: string | undefined; + cronEnabled: boolean | undefined; + gatewayBind: "auto" | "lan" | "tailnet" | "loopback" | undefined; + gatewayAuth: Record | undefined; + gatewayControlUi: Record | undefined; + hooksConfig: HooksConfig | undefined; + canvasHostPort: number | undefined; + legacyIssues: Array<{ path: string; message: string }>; + legacyParsed: Record; + migrationConfig: Record | null; + migrationChanges: string[]; + }; +}; + +const gatewayTestHoisted = vi.hoisted(() => { + const key = Symbol.for("openclaw.gatewayTestHelpers.hoisted"); + const store = globalThis as Record; + if (Object.prototype.hasOwnProperty.call(store, key)) { + return store[key] as GatewayTestHoistedState; + } + const created: GatewayTestHoistedState = { + testTailnetIPv4: { value: undefined }, + piSdkMock: { + enabled: false, + discoverCalls: 0, + models: [], + }, + cronIsolatedRun: vi.fn(async () => ({ status: "ok", summary: "ok" })), + agentCommand: vi.fn().mockResolvedValue(undefined), + runBtwSideQuestion: vi.fn().mockResolvedValue(undefined), + dispatchInboundMessage: vi.fn(), + testIsNixMode: { value: false }, + sessionStoreSaveDelayMs: { value: 0 }, + embeddedRunMock: { + activeIds: new Set(), + abortCalls: [], + waitCalls: [], + waitResults: new Map(), + }, + testTailscaleWhois: { value: null }, + getReplyFromConfig: vi.fn().mockResolvedValue(undefined), + sendWhatsAppMock: vi.fn().mockResolvedValue({ messageId: "msg-1", toJid: "jid-1" }), + testState: { + agentConfig: undefined, + agentsConfig: undefined, + bindingsConfig: undefined, + channelsConfig: undefined, + sessionStorePath: undefined, + sessionConfig: undefined, + allowFrom: undefined, + cronStorePath: undefined, + cronEnabled: false, + gatewayBind: undefined, + gatewayAuth: undefined, + gatewayControlUi: undefined, + hooksConfig: undefined, + canvasHostPort: undefined, + legacyIssues: [], + legacyParsed: {}, + migrationConfig: null, + migrationChanges: [], + }, + }; + store[key] = created; + return created; +}); + +export function getGatewayTestHoistedState(): GatewayTestHoistedState { + return gatewayTestHoisted; +} + +export const testTailnetIPv4 = gatewayTestHoisted.testTailnetIPv4; +export const testTailscaleWhois = gatewayTestHoisted.testTailscaleWhois; +export const piSdkMock = gatewayTestHoisted.piSdkMock; +export const cronIsolatedRun = gatewayTestHoisted.cronIsolatedRun; +export const agentCommand = gatewayTestHoisted.agentCommand; +export const runBtwSideQuestion = gatewayTestHoisted.runBtwSideQuestion; +export const dispatchInboundMessageMock = gatewayTestHoisted.dispatchInboundMessage; +export const getReplyFromConfig = gatewayTestHoisted.getReplyFromConfig; +export const mockGetReplyFromConfigOnce = (impl: GetReplyFromConfigFn) => { + getReplyFromConfig.mockImplementationOnce(impl); +}; +export const sendWhatsAppMock = gatewayTestHoisted.sendWhatsAppMock; +export const testState = gatewayTestHoisted.testState; +export const testIsNixMode = gatewayTestHoisted.testIsNixMode; +export const sessionStoreSaveDelayMs = gatewayTestHoisted.sessionStoreSaveDelayMs; +export const embeddedRunMock = gatewayTestHoisted.embeddedRunMock; + +export const testConfigRoot = resolveGlobalSingleton(GATEWAY_TEST_CONFIG_ROOT_KEY, () => ({ + value: path.join(os.tmpdir(), `openclaw-gateway-test-${process.pid}-${crypto.randomUUID()}`), +})); + +export function setTestConfigRoot(root: string): void { + testConfigRoot.value = root; + process.env.OPENCLAW_CONFIG_PATH = path.join(root, "openclaw.json"); +} diff --git a/src/plugins/channel-catalog-registry.ts b/src/plugins/channel-catalog-registry.ts new file mode 100644 index 00000000000..500a74f2cac --- /dev/null +++ b/src/plugins/channel-catalog-registry.ts @@ -0,0 +1,55 @@ +import { discoverOpenClawPlugins } from "./discovery.js"; +import { + loadPluginManifest, + type PluginPackageChannel, + type PluginPackageInstall, +} from "./manifest.js"; +import type { PluginOrigin } from "./types.js"; + +export type PluginChannelCatalogEntry = { + pluginId: string; + origin: PluginOrigin; + packageName?: string; + workspaceDir?: string; + rootDir: string; + channel: PluginPackageChannel; + install?: PluginPackageInstall; +}; + +export function listChannelCatalogEntries( + params: { + origin?: PluginOrigin; + workspaceDir?: string; + env?: NodeJS.ProcessEnv; + } = {}, +): PluginChannelCatalogEntry[] { + return discoverOpenClawPlugins({ + workspaceDir: params.workspaceDir, + env: params.env, + }).candidates.flatMap((candidate) => { + if (params.origin && candidate.origin !== params.origin) { + return []; + } + const channel = candidate.packageManifest?.channel; + if (!channel?.id) { + return []; + } + const manifest = loadPluginManifest(candidate.rootDir, candidate.origin !== "bundled"); + if (!manifest.ok) { + return []; + } + return [ + { + pluginId: manifest.manifest.id, + origin: candidate.origin, + packageName: candidate.packageName, + workspaceDir: candidate.workspaceDir, + rootDir: candidate.rootDir, + channel, + ...(candidate.packageManifest?.install + ? { install: candidate.packageManifest.install } + : {}), + }, + ]; + }); +} diff --git a/src/plugins/contracts/speech-vitest-registry.ts b/src/plugins/contracts/speech-vitest-registry.ts index 4c797a01c03..304cfa8d1af 100644 --- a/src/plugins/contracts/speech-vitest-registry.ts +++ b/src/plugins/contracts/speech-vitest-registry.ts @@ -1,10 +1,5 @@ -import fs from "node:fs"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; -import { createJiti } from "jiti"; import { loadBundledCapabilityRuntimeRegistry } from "../bundled-capability-runtime.js"; -import { loadPluginManifestRegistry } from "../manifest-registry.js"; -import { buildPluginLoaderAliasMap, buildPluginLoaderJitiOptions } from "../sdk-alias.js"; +import { resolveManifestContractPluginIds } from "../manifest-registry.js"; import type { ImageGenerationProviderPlugin, MediaUnderstandingProviderPlugin, @@ -52,366 +47,90 @@ type ManifestContractKey = | "imageGenerationProviders" | "videoGenerationProviders"; -function buildVitestCapabilityAliasMap(modulePath: string): Record { - const { ["openclaw/plugin-sdk"]: _ignoredRootAlias, ...scopedAliasMap } = - buildPluginLoaderAliasMap(modulePath, process.argv[1], import.meta.url, "dist"); - return { - ...scopedAliasMap, - "openclaw/plugin-sdk/llm-task": fileURLToPath( - new URL("../capability-runtime-vitest-shims/llm-task.ts", import.meta.url), - ), - "openclaw/plugin-sdk/media-runtime": fileURLToPath( - new URL("../capability-runtime-vitest-shims/media-runtime.ts", import.meta.url), - ), - "openclaw/plugin-sdk/provider-onboard": fileURLToPath( - new URL("../../plugin-sdk/provider-onboard.ts", import.meta.url), - ), - "openclaw/plugin-sdk/speech-core": fileURLToPath( - new URL("../capability-runtime-vitest-shims/speech-core.ts", import.meta.url), - ), - }; -} - -function resolveNamedBuilder(moduleExport: unknown, pattern: RegExp): (() => T) | undefined { - if (!moduleExport || typeof moduleExport !== "object") { - return undefined; - } - for (const [key, value] of Object.entries(moduleExport as Record)) { - if (pattern.test(key) && typeof value === "function") { - return value as () => T; - } - } - return undefined; -} - -function resolveNamedBuilders(moduleExport: unknown, pattern: RegExp): Array<() => T> { - if (!moduleExport || typeof moduleExport !== "object") { - return []; - } - const matches: Array<() => T> = []; - for (const [key, value] of Object.entries(moduleExport as Record)) { - if (pattern.test(key) && typeof value === "function") { - matches.push(value as () => T); - } - } - return matches; -} - -function resolveNamedValues( - moduleExport: unknown, - pattern: RegExp, - isMatch: (value: unknown) => value is T, -): T[] { - if (!moduleExport || typeof moduleExport !== "object") { - return []; - } - const matches: T[] = []; - for (const [key, value] of Object.entries(moduleExport as Record)) { - if (pattern.test(key) && isMatch(value)) { - matches.push(value); - } - } - return matches; -} - -function resolveBundledManifestPluginIds(contract: ManifestContractKey): string[] { - return loadPluginManifestRegistry({}) - .plugins.filter( - (plugin) => plugin.origin === "bundled" && (plugin.contracts?.[contract]?.length ?? 0) > 0, - ) - .map((plugin) => plugin.id); -} - -function resolveTestApiModuleRecords(pluginIds: readonly string[]) { - const unresolvedPluginIds = new Set(pluginIds); - const manifests = loadPluginManifestRegistry({}).plugins.filter( - (plugin) => plugin.origin === "bundled" && unresolvedPluginIds.has(plugin.id), - ); - return { manifests, unresolvedPluginIds }; -} - -function createVitestCapabilityLoader(modulePath: string) { - return createJiti(import.meta.url, { - ...buildPluginLoaderJitiOptions(buildVitestCapabilityAliasMap(modulePath)), - tryNative: false, +function loadVitestCapabilityContractEntries(params: { + contract: ManifestContractKey; + pickEntries: (registry: ReturnType) => Array<{ + pluginId: string; + provider: T; + }>; +}): Array<{ pluginId: string; provider: T }> { + const pluginIds = resolveManifestContractPluginIds({ + contract: params.contract, + origin: "bundled", }); -} - -function isMediaUnderstandingProvider(value: unknown): value is MediaUnderstandingProviderPlugin { - return ( - typeof value === "object" && - value !== null && - typeof (value as { id?: unknown }).id === "string" && - typeof (value as { describeImage?: unknown }).describeImage === "function" + if (pluginIds.length === 0) { + return []; + } + return params.pickEntries( + loadBundledCapabilityRuntimeRegistry({ + pluginIds, + pluginSdkResolution: "dist", + }), ); } export function loadVitestSpeechProviderContractRegistry(): SpeechProviderContractEntry[] { - const registrations: SpeechProviderContractEntry[] = []; - const { manifests, unresolvedPluginIds } = resolveTestApiModuleRecords( - resolveBundledManifestPluginIds("speechProviders"), - ); - - for (const plugin of manifests) { - if (!plugin.rootDir) { - continue; - } - const testApiPath = path.join(plugin.rootDir, "test-api.ts"); - if (!fs.existsSync(testApiPath)) { - continue; - } - const builder = resolveNamedBuilder( - createVitestCapabilityLoader(testApiPath)(testApiPath), - /^build.+SpeechProvider$/u, - ); - if (!builder) { - continue; - } - registrations.push({ - pluginId: plugin.id, - provider: builder(), - }); - unresolvedPluginIds.delete(plugin.id); - } - - if (unresolvedPluginIds.size === 0) { - return registrations; - } - - const runtimeRegistry = loadBundledCapabilityRuntimeRegistry({ - pluginIds: [...unresolvedPluginIds], - pluginSdkResolution: "dist", + return loadVitestCapabilityContractEntries({ + contract: "speechProviders", + pickEntries: (registry) => + registry.speechProviders.map((entry) => ({ + pluginId: entry.pluginId, + provider: entry.provider, + })), }); - registrations.push( - ...runtimeRegistry.speechProviders.map((entry) => ({ - pluginId: entry.pluginId, - provider: entry.provider, - })), - ); - return registrations; } export function loadVitestMediaUnderstandingProviderContractRegistry(): MediaUnderstandingProviderContractEntry[] { - const registrations: MediaUnderstandingProviderContractEntry[] = []; - const { manifests, unresolvedPluginIds } = resolveTestApiModuleRecords( - resolveBundledManifestPluginIds("mediaUnderstandingProviders"), - ); - - for (const plugin of manifests) { - if (!plugin.rootDir) { - continue; - } - const testApiPath = path.join(plugin.rootDir, "test-api.ts"); - if (!fs.existsSync(testApiPath)) { - continue; - } - const providers = resolveNamedValues( - createVitestCapabilityLoader(testApiPath)(testApiPath), - /MediaUnderstandingProvider$/u, - isMediaUnderstandingProvider, - ); - if (providers.length === 0) { - continue; - } - registrations.push(...providers.map((provider) => ({ pluginId: plugin.id, provider }))); - unresolvedPluginIds.delete(plugin.id); - } - - if (unresolvedPluginIds.size === 0) { - return registrations; - } - - const runtimeRegistry = loadBundledCapabilityRuntimeRegistry({ - pluginIds: [...unresolvedPluginIds], - pluginSdkResolution: "dist", + return loadVitestCapabilityContractEntries({ + contract: "mediaUnderstandingProviders", + pickEntries: (registry) => + registry.mediaUnderstandingProviders.map((entry) => ({ + pluginId: entry.pluginId, + provider: entry.provider, + })), }); - registrations.push( - ...runtimeRegistry.mediaUnderstandingProviders.map((entry) => ({ - pluginId: entry.pluginId, - provider: entry.provider, - })), - ); - return registrations; } export function loadVitestRealtimeVoiceProviderContractRegistry(): RealtimeVoiceProviderContractEntry[] { - const registrations: RealtimeVoiceProviderContractEntry[] = []; - const { manifests, unresolvedPluginIds } = resolveTestApiModuleRecords( - resolveBundledManifestPluginIds("realtimeVoiceProviders"), - ); - - for (const plugin of manifests) { - if (!plugin.rootDir) { - continue; - } - const testApiPath = path.join(plugin.rootDir, "test-api.ts"); - if (!fs.existsSync(testApiPath)) { - continue; - } - const builder = resolveNamedBuilder( - createVitestCapabilityLoader(testApiPath)(testApiPath), - /^build.+RealtimeVoiceProvider$/u, - ); - if (!builder) { - continue; - } - registrations.push({ - pluginId: plugin.id, - provider: builder(), - }); - unresolvedPluginIds.delete(plugin.id); - } - - if (unresolvedPluginIds.size === 0) { - return registrations; - } - - const runtimeRegistry = loadBundledCapabilityRuntimeRegistry({ - pluginIds: [...unresolvedPluginIds], - pluginSdkResolution: "dist", + return loadVitestCapabilityContractEntries({ + contract: "realtimeVoiceProviders", + pickEntries: (registry) => + registry.realtimeVoiceProviders.map((entry) => ({ + pluginId: entry.pluginId, + provider: entry.provider, + })), }); - registrations.push( - ...runtimeRegistry.realtimeVoiceProviders.map((entry) => ({ - pluginId: entry.pluginId, - provider: entry.provider, - })), - ); - return registrations; } export function loadVitestRealtimeTranscriptionProviderContractRegistry(): RealtimeTranscriptionProviderContractEntry[] { - const registrations: RealtimeTranscriptionProviderContractEntry[] = []; - const { manifests, unresolvedPluginIds } = resolveTestApiModuleRecords( - resolveBundledManifestPluginIds("realtimeTranscriptionProviders"), - ); - - for (const plugin of manifests) { - if (!plugin.rootDir) { - continue; - } - const testApiPath = path.join(plugin.rootDir, "test-api.ts"); - if (!fs.existsSync(testApiPath)) { - continue; - } - const builder = resolveNamedBuilder( - createVitestCapabilityLoader(testApiPath)(testApiPath), - /^build.+RealtimeTranscriptionProvider$/u, - ); - if (!builder) { - continue; - } - registrations.push({ - pluginId: plugin.id, - provider: builder(), - }); - unresolvedPluginIds.delete(plugin.id); - } - - if (unresolvedPluginIds.size === 0) { - return registrations; - } - - const runtimeRegistry = loadBundledCapabilityRuntimeRegistry({ - pluginIds: [...unresolvedPluginIds], - pluginSdkResolution: "dist", + return loadVitestCapabilityContractEntries({ + contract: "realtimeTranscriptionProviders", + pickEntries: (registry) => + registry.realtimeTranscriptionProviders.map((entry) => ({ + pluginId: entry.pluginId, + provider: entry.provider, + })), }); - registrations.push( - ...runtimeRegistry.realtimeTranscriptionProviders.map((entry) => ({ - pluginId: entry.pluginId, - provider: entry.provider, - })), - ); - return registrations; } export function loadVitestImageGenerationProviderContractRegistry(): ImageGenerationProviderContractEntry[] { - const registrations: ImageGenerationProviderContractEntry[] = []; - const { manifests, unresolvedPluginIds } = resolveTestApiModuleRecords( - resolveBundledManifestPluginIds("imageGenerationProviders"), - ); - - for (const plugin of manifests) { - if (!plugin.rootDir) { - continue; - } - const testApiPath = path.join(plugin.rootDir, "test-api.ts"); - if (!fs.existsSync(testApiPath)) { - continue; - } - const builders = resolveNamedBuilders( - createVitestCapabilityLoader(testApiPath)(testApiPath), - /ImageGenerationProvider$/u, - ); - if (builders.length === 0) { - continue; - } - registrations.push( - ...builders.map((builder) => ({ - pluginId: plugin.id, - provider: builder(), + return loadVitestCapabilityContractEntries({ + contract: "imageGenerationProviders", + pickEntries: (registry) => + registry.imageGenerationProviders.map((entry) => ({ + pluginId: entry.pluginId, + provider: entry.provider, })), - ); - unresolvedPluginIds.delete(plugin.id); - } - - if (unresolvedPluginIds.size === 0) { - return registrations; - } - - const runtimeRegistry = loadBundledCapabilityRuntimeRegistry({ - pluginIds: [...unresolvedPluginIds], - pluginSdkResolution: "dist", }); - registrations.push( - ...runtimeRegistry.imageGenerationProviders.map((entry) => ({ - pluginId: entry.pluginId, - provider: entry.provider, - })), - ); - return registrations; } export function loadVitestVideoGenerationProviderContractRegistry(): VideoGenerationProviderContractEntry[] { - const registrations: VideoGenerationProviderContractEntry[] = []; - const { manifests, unresolvedPluginIds } = resolveTestApiModuleRecords( - resolveBundledManifestPluginIds("videoGenerationProviders"), - ); - - for (const plugin of manifests) { - if (!plugin.rootDir) { - continue; - } - const testApiPath = path.join(plugin.rootDir, "test-api.ts"); - if (!fs.existsSync(testApiPath)) { - continue; - } - const builder = resolveNamedBuilder( - createVitestCapabilityLoader(testApiPath)(testApiPath), - /^build.+VideoGenerationProvider$/u, - ); - if (!builder) { - continue; - } - registrations.push({ - pluginId: plugin.id, - provider: builder(), - }); - unresolvedPluginIds.delete(plugin.id); - } - - if (unresolvedPluginIds.size === 0) { - return registrations; - } - - const runtimeRegistry = loadBundledCapabilityRuntimeRegistry({ - pluginIds: [...unresolvedPluginIds], - pluginSdkResolution: "dist", + return loadVitestCapabilityContractEntries({ + contract: "videoGenerationProviders", + pickEntries: (registry) => + registry.videoGenerationProviders.map((entry) => ({ + pluginId: entry.pluginId, + provider: entry.provider, + })), }); - registrations.push( - ...runtimeRegistry.videoGenerationProviders.map((entry) => ({ - pluginId: entry.pluginId, - provider: entry.provider, - })), - ); - return registrations; }