mirror of https://github.com/openclaw/openclaw.git
refactor: remove bundled channel discovery leaks
This commit is contained in:
parent
604e16c765
commit
181a50e146
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 ?? [])
|
||||
|
|
|
|||
|
|
@ -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[] {
|
||||
|
|
|
|||
|
|
@ -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}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
|
|
@ -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 }
|
||||
: {}),
|
||||
},
|
||||
];
|
||||
});
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue