refactor: remove bundled channel discovery leaks

This commit is contained in:
Peter Steinberger 2026-04-05 20:34:15 +01:00
parent 604e16c765
commit 181a50e146
No known key found for this signature in database
12 changed files with 458 additions and 745 deletions

View File

@ -512,7 +512,9 @@ describe("convertTools", () => {
parameters: { type: "object", properties: {}, additionalProperties: false },
},
];
const result = convertTools(tools as Parameters<typeof convertTools>[0], { strict: true });
const result = convertTools(tools as unknown as Parameters<typeof convertTools>[0], {
strict: true,
});
expect(result[0]).toEqual({
type: "function",
@ -540,7 +542,9 @@ describe("convertTools", () => {
},
},
];
const result = convertTools(tools as Parameters<typeof convertTools>[0], { strict: true });
const result = convertTools(tools as unknown as Parameters<typeof convertTools>[0], {
strict: true,
});
expect(result[0]).toEqual({
type: "function",

View File

@ -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<ChatChannelId, ChatChannelMeta> {
const entries = new Map<ChatChannelId, ChatChannelMeta>();
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;
}

View File

@ -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<string, ChatChannelId> {
const aliases = new Map<string, ChatChannelId>();
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;

View File

@ -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 ?? [])

View File

@ -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[] {

View File

@ -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<typeof loadPluginManifestRegistry>["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<string>();
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}`,
);
}
}

View File

@ -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<string, { entry: ChannelPluginCatalogEntry; priority: number }>();
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);

View File

@ -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<ReplyPayload | ReplyPayload[] | undefined>;
type CronIsolatedRunFn = (...args: unknown[]) => Promise<{ status: string; summary: string }>;
type AgentCommandFn = (...args: unknown[]) => Promise<void>;
type SendWhatsAppFn = (...args: unknown[]) => Promise<{ messageId: string; toJid: string }>;
type RunBtwSideQuestionFn = (...args: unknown[]) => Promise<unknown>;
type DispatchInboundMessageFn = (...args: unknown[]) => Promise<unknown>;
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<PropertyKey, unknown>;
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<CronIsolatedRunFn>;
agentCommand: Mock<AgentCommandFn>;
runBtwSideQuestion: Mock<RunBtwSideQuestionFn>;
dispatchInboundMessage: Mock<DispatchInboundMessageFn>;
testIsNixMode: { value: boolean };
sessionStoreSaveDelayMs: { value: number };
embeddedRunMock: {
activeIds: Set<string>;
abortCalls: string[];
waitCalls: string[];
waitResults: Map<string, boolean>;
};
testTailscaleWhois: { value: TailscaleWhoisIdentity | null };
getReplyFromConfig: Mock<GetReplyFromConfigFn>;
sendWhatsAppMock: Mock<SendWhatsAppFn>;
testState: {
agentConfig: Record<string, unknown> | undefined;
agentsConfig: Record<string, unknown> | undefined;
bindingsConfig: AgentBinding[] | undefined;
channelsConfig: Record<string, unknown> | undefined;
sessionStorePath: string | undefined;
sessionConfig: Record<string, unknown> | undefined;
allowFrom: string[] | undefined;
cronStorePath: string | undefined;
cronEnabled: boolean | undefined;
gatewayBind: "auto" | "lan" | "tailnet" | "loopback" | undefined;
gatewayAuth: Record<string, unknown> | undefined;
gatewayControlUi: Record<string, unknown> | undefined;
hooksConfig: HooksConfig | undefined;
canvasHostPort: number | undefined;
legacyIssues: Array<{ path: string; message: string }>;
legacyParsed: Record<string, unknown>;
migrationConfig: Record<string, unknown> | 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<string>(),
abortCalls: [] as string[],
waitCalls: [] as string[],
waitResults: new Map<string, boolean>(),
},
testTailscaleWhois: { value: null as TailscaleWhoisIdentity | null },
getReplyFromConfig: vi.fn<GetReplyFromConfigFn>().mockResolvedValue(undefined),
sendWhatsAppMock: vi.fn().mockResolvedValue({ messageId: "msg-1", toJid: "jid-1" }),
testState: {
agentConfig: undefined as Record<string, unknown> | undefined,
agentsConfig: undefined as Record<string, unknown> | undefined,
bindingsConfig: undefined as AgentBinding[] | undefined,
channelsConfig: undefined as Record<string, unknown> | undefined,
sessionStorePath: undefined as string | undefined,
sessionConfig: undefined as Record<string, unknown> | 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<string, unknown> | undefined,
gatewayControlUi: undefined as Record<string, unknown> | undefined,
hooksConfig: undefined as HooksConfig | undefined,
canvasHostPort: undefined as number | undefined,
legacyIssues: [] as Array<{ path: string; message: string }>,
legacyParsed: {} as Record<string, unknown>,
migrationConfig: null as Record<string, unknown> | 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<CronIsolatedRunFn> = hoisted.cronIsolatedRun;
export const agentCommand: Mock<AgentCommandFn> = hoisted.agentCommand;
export const runBtwSideQuestion: Mock<RunBtwSideQuestionFn> = hoisted.runBtwSideQuestion;
export const dispatchInboundMessageMock: Mock<DispatchInboundMessageFn> =
hoisted.dispatchInboundMessage;
export const getReplyFromConfig: Mock<GetReplyFromConfigFn> = hoisted.getReplyFromConfig;
export const mockGetReplyFromConfigOnce = (impl: GetReplyFromConfigFn) => {
getReplyFromConfig.mockImplementationOnce(impl);
};
export const sendWhatsAppMock: Mock<SendWhatsAppFn> = 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<typeof import("../channels/web/index.js")>(
@ -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<RunBtwSideQuestionFn>) =>
hoisted.runBtwSideQuestion(...args),
gatewayTestHoisted.runBtwSideQuestion(...args),
}));
vi.mock("/src/agents/btw.js", () => ({
runBtwSideQuestion: (...args: Parameters<RunBtwSideQuestionFn>) =>
hoisted.runBtwSideQuestion(...args),
gatewayTestHoisted.runBtwSideQuestion(...args),
}));
vi.mock("../auto-reply/dispatch.js", async () => {
const actual = await vi.importActual<typeof import("../auto-reply/dispatch.js")>(
@ -698,9 +543,9 @@ vi.mock("../auto-reply/dispatch.js", async () => {
return {
...actual,
dispatchInboundMessage: (...args: Parameters<typeof actual.dispatchInboundMessage>) => {
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<typeof actual.dispatchInboundMessage>) => {
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<GetReplyFromConfigFn>) =>
hoisted.getReplyFromConfig(...args),
gatewayTestHoisted.getReplyFromConfig(...args),
}));
vi.mock("/src/auto-reply/reply.js", () => ({
getReplyFromConfig: (...args: Parameters<GetReplyFromConfigFn>) =>
hoisted.getReplyFromConfig(...args),
gatewayTestHoisted.getReplyFromConfig(...args),
}));
vi.mock("../auto-reply/reply/get-reply-from-config.runtime.js", () => ({
getReplyFromConfig: (...args: Parameters<GetReplyFromConfigFn>) =>
hoisted.getReplyFromConfig(...args),
gatewayTestHoisted.getReplyFromConfig(...args),
}));
vi.mock("/src/auto-reply/reply/get-reply-from-config.runtime.js", () => ({
getReplyFromConfig: (...args: Parameters<GetReplyFromConfigFn>) =>
hoisted.getReplyFromConfig(...args),
gatewayTestHoisted.getReplyFromConfig(...args),
}));
vi.mock("../cli/deps.js", async () => {
const actual = await vi.importActual<typeof import("../cli/deps.js")>("../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<typeof import("../plugins/loader.js")>("../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";

View File

@ -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;
}

View File

@ -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<ReplyPayload | ReplyPayload[] | undefined>;
export type CronIsolatedRunFn = (
...args: unknown[]
) => Promise<{ status: string; summary: string }>;
export type AgentCommandFn = (...args: unknown[]) => Promise<void>;
export type SendWhatsAppFn = (...args: unknown[]) => Promise<{ messageId: string; toJid: string }>;
export type RunBtwSideQuestionFn = (...args: unknown[]) => Promise<unknown>;
export type DispatchInboundMessageFn = (...args: unknown[]) => Promise<unknown>;
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<CronIsolatedRunFn>;
agentCommand: Mock<AgentCommandFn>;
runBtwSideQuestion: Mock<RunBtwSideQuestionFn>;
dispatchInboundMessage: Mock<DispatchInboundMessageFn>;
testIsNixMode: { value: boolean };
sessionStoreSaveDelayMs: { value: number };
embeddedRunMock: {
activeIds: Set<string>;
abortCalls: string[];
waitCalls: string[];
waitResults: Map<string, boolean>;
};
testTailscaleWhois: { value: TailscaleWhoisIdentity | null };
getReplyFromConfig: Mock<GetReplyFromConfigFn>;
sendWhatsAppMock: Mock<SendWhatsAppFn>;
testState: {
agentConfig: Record<string, unknown> | undefined;
agentsConfig: Record<string, unknown> | undefined;
bindingsConfig: AgentBinding[] | undefined;
channelsConfig: Record<string, unknown> | undefined;
sessionStorePath: string | undefined;
sessionConfig: Record<string, unknown> | undefined;
allowFrom: string[] | undefined;
cronStorePath: string | undefined;
cronEnabled: boolean | undefined;
gatewayBind: "auto" | "lan" | "tailnet" | "loopback" | undefined;
gatewayAuth: Record<string, unknown> | undefined;
gatewayControlUi: Record<string, unknown> | undefined;
hooksConfig: HooksConfig | undefined;
canvasHostPort: number | undefined;
legacyIssues: Array<{ path: string; message: string }>;
legacyParsed: Record<string, unknown>;
migrationConfig: Record<string, unknown> | null;
migrationChanges: string[];
};
};
const gatewayTestHoisted = vi.hoisted(() => {
const key = Symbol.for("openclaw.gatewayTestHelpers.hoisted");
const store = globalThis as Record<PropertyKey, unknown>;
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<string>(),
abortCalls: [],
waitCalls: [],
waitResults: new Map<string, boolean>(),
},
testTailscaleWhois: { value: null },
getReplyFromConfig: vi.fn<GetReplyFromConfigFn>().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");
}

View File

@ -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 }
: {}),
},
];
});
}

View File

@ -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<string, string> {
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<T>(moduleExport: unknown, pattern: RegExp): (() => T) | undefined {
if (!moduleExport || typeof moduleExport !== "object") {
return undefined;
}
for (const [key, value] of Object.entries(moduleExport as Record<string, unknown>)) {
if (pattern.test(key) && typeof value === "function") {
return value as () => T;
}
}
return undefined;
}
function resolveNamedBuilders<T>(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<string, unknown>)) {
if (pattern.test(key) && typeof value === "function") {
matches.push(value as () => T);
}
}
return matches;
}
function resolveNamedValues<T>(
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<string, unknown>)) {
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<T>(params: {
contract: ManifestContractKey;
pickEntries: (registry: ReturnType<typeof loadBundledCapabilityRuntimeRegistry>) => 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<SpeechProviderPlugin>(
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<MediaUnderstandingProviderPlugin>(
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<RealtimeVoiceProviderPlugin>(
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<RealtimeTranscriptionProviderPlugin>(
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<ImageGenerationProviderPlugin>(
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<VideoGenerationProviderPlugin>(
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;
}