mirror of
https://github.com/moltbot/moltbot.git
synced 2026-05-07 07:58:36 +00:00
Reuse the startup runtime plugin registry across provider/tool helper paths while preserving standalone CLI/MCP fallback loading. Includes follow-up fixes for migration/provider/tool registry bootstrap and regression coverage for compatible registry reuse. Co-authored-by: DmitryPogodaev <pogodaev.dm@gmail.com>
1506 lines
48 KiB
TypeScript
1506 lines
48 KiB
TypeScript
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import type { OpenClawConfig } from "../config/config.js";
|
|
import { createEmptyPluginRegistry } from "./registry.js";
|
|
|
|
type MockManifestRegistry = {
|
|
plugins: Array<Record<string, unknown>>;
|
|
diagnostics: unknown[];
|
|
};
|
|
|
|
function createEmptyMockManifestRegistry(): MockManifestRegistry {
|
|
return { plugins: [], diagnostics: [] };
|
|
}
|
|
|
|
const mocks = vi.hoisted(() => ({
|
|
createMockRegistry: () => ({
|
|
plugins: [],
|
|
diagnostics: [],
|
|
memoryEmbeddingProviders: [],
|
|
speechProviders: [],
|
|
realtimeTranscriptionProviders: [],
|
|
realtimeVoiceProviders: [],
|
|
mediaUnderstandingProviders: [],
|
|
imageGenerationProviders: [],
|
|
videoGenerationProviders: [],
|
|
musicGenerationProviders: [],
|
|
}),
|
|
resolveRuntimePluginRegistry: vi.fn<
|
|
(params?: unknown) => ReturnType<typeof createEmptyPluginRegistry> | undefined
|
|
>(() => undefined),
|
|
resolvePluginRegistryLoadCacheKey: vi.fn((options: unknown) => JSON.stringify(options)),
|
|
loadPluginManifestRegistry: vi.fn<(params?: Record<string, unknown>) => MockManifestRegistry>(
|
|
() => createEmptyMockManifestRegistry(),
|
|
),
|
|
resolveInstalledManifestRegistryIndexFingerprint: vi.fn(() => "test-installed-index"),
|
|
loadBundledCapabilityRuntimeRegistry: vi.fn(),
|
|
loadPluginRegistrySnapshot: vi.fn<
|
|
(_params?: unknown) => { plugins: Array<Record<string, unknown>> }
|
|
>(() => ({
|
|
plugins: [],
|
|
})),
|
|
withBundledPluginAllowlistCompat: vi.fn(
|
|
({ config, pluginIds }: { config?: OpenClawConfig; pluginIds: string[] }) =>
|
|
({
|
|
...config,
|
|
plugins: {
|
|
...config?.plugins,
|
|
allow: Array.from(new Set([...(config?.plugins?.allow ?? []), ...pluginIds])),
|
|
},
|
|
}) as OpenClawConfig,
|
|
),
|
|
withBundledPluginEnablementCompat: vi.fn(({ config }) => config),
|
|
withBundledPluginVitestCompat: vi.fn(({ config }) => config),
|
|
}));
|
|
|
|
vi.mock("./loader.js", () => ({
|
|
resolveRuntimePluginRegistry: mocks.resolveRuntimePluginRegistry,
|
|
resolvePluginRegistryLoadCacheKey: mocks.resolvePluginRegistryLoadCacheKey,
|
|
}));
|
|
|
|
vi.mock("./active-runtime-registry.js", () => ({
|
|
getLoadedRuntimePluginRegistry: (params?: { requiredPluginIds?: string[] }) => {
|
|
if (params === undefined) {
|
|
return mocks.resolveRuntimePluginRegistry();
|
|
}
|
|
return mocks.resolveRuntimePluginRegistry({
|
|
onlyPluginIds: params.requiredPluginIds,
|
|
});
|
|
},
|
|
}));
|
|
|
|
vi.mock("./bundled-capability-runtime.js", () => ({
|
|
loadBundledCapabilityRuntimeRegistry: mocks.loadBundledCapabilityRuntimeRegistry,
|
|
}));
|
|
|
|
vi.mock("./manifest-registry-installed.js", () => ({
|
|
loadPluginManifestRegistryForInstalledIndex: mocks.loadPluginManifestRegistry,
|
|
resolveInstalledManifestRegistryIndexFingerprint:
|
|
mocks.resolveInstalledManifestRegistryIndexFingerprint,
|
|
}));
|
|
|
|
vi.mock("./manifest-registry.js", async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import("./manifest-registry.js")>();
|
|
return {
|
|
...actual,
|
|
loadPluginManifestRegistry: mocks.loadPluginManifestRegistry,
|
|
};
|
|
});
|
|
|
|
vi.mock("./plugin-registry.js", async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import("./plugin-registry.js")>();
|
|
return {
|
|
...actual,
|
|
loadPluginRegistrySnapshot: mocks.loadPluginRegistrySnapshot,
|
|
loadPluginRegistrySnapshotWithMetadata: (params?: { index?: unknown }) => {
|
|
const snapshot = (params?.index ?? mocks.loadPluginRegistrySnapshot(params)) as {
|
|
plugins?: Array<Record<string, unknown>>;
|
|
};
|
|
return {
|
|
snapshot: {
|
|
...snapshot,
|
|
plugins:
|
|
snapshot.plugins && snapshot.plugins.length > 0
|
|
? snapshot.plugins
|
|
: [
|
|
{
|
|
pluginId: "__test_manifest_registry_fixture__",
|
|
origin: "bundled",
|
|
enabled: true,
|
|
},
|
|
],
|
|
},
|
|
source: params?.index ? "provided" : "derived",
|
|
diagnostics: [],
|
|
};
|
|
},
|
|
loadPluginManifestRegistryForPluginRegistry: (
|
|
...args: Parameters<typeof mocks.loadPluginManifestRegistry>
|
|
) => {
|
|
const [{ includeDisabled: _includeDisabled, ...params } = {}] = args as [
|
|
Record<string, unknown>?,
|
|
];
|
|
return mocks.loadPluginManifestRegistry(params);
|
|
},
|
|
};
|
|
});
|
|
|
|
vi.mock("./bundled-compat.js", () => ({
|
|
withBundledPluginAllowlistCompat: mocks.withBundledPluginAllowlistCompat,
|
|
withBundledPluginEnablementCompat: mocks.withBundledPluginEnablementCompat,
|
|
withBundledPluginVitestCompat: mocks.withBundledPluginVitestCompat,
|
|
}));
|
|
|
|
let resolvePluginCapabilityProviders: typeof import("./capability-provider-runtime.js").resolvePluginCapabilityProviders;
|
|
let resolvePluginCapabilityProvider: typeof import("./capability-provider-runtime.js").resolvePluginCapabilityProvider;
|
|
let resolveBundledCapabilityProviderIds: typeof import("./capability-provider-runtime.js").resolveBundledCapabilityProviderIds;
|
|
let resolveManifestCapabilityProviderIds: typeof import("./capability-provider-runtime.js").resolveManifestCapabilityProviderIds;
|
|
let clearCurrentPluginMetadataSnapshot: typeof import("./current-plugin-metadata-snapshot.js").clearCurrentPluginMetadataSnapshot;
|
|
let setCurrentPluginMetadataSnapshot: typeof import("./current-plugin-metadata-snapshot.js").setCurrentPluginMetadataSnapshot;
|
|
|
|
function expectResolvedCapabilityProviderIds(providers: Array<{ id: string }>, expected: string[]) {
|
|
expect(providers.map((provider) => provider.id)).toEqual(expected);
|
|
}
|
|
|
|
function expectNoResolvedCapabilityProviders(providers: Array<{ id: string }>) {
|
|
expectResolvedCapabilityProviderIds(providers, []);
|
|
}
|
|
|
|
function expectActiveRegistryLookup(pluginIds: string[]) {
|
|
expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith({ onlyPluginIds: pluginIds });
|
|
}
|
|
|
|
function collectActiveRegistryLookups() {
|
|
return mocks.resolveRuntimePluginRegistry.mock.calls
|
|
.map(([options]) => options)
|
|
.filter((options): options is { onlyPluginIds?: string[] } =>
|
|
Boolean(
|
|
options &&
|
|
typeof options === "object" &&
|
|
Object.hasOwn(options as Record<string, unknown>, "onlyPluginIds"),
|
|
),
|
|
);
|
|
}
|
|
|
|
function expectBundledCompatLoadPath(params: {
|
|
cfg: OpenClawConfig;
|
|
allowlistCompat: OpenClawConfig;
|
|
enablementCompat: {
|
|
plugins: {
|
|
allow?: string[];
|
|
entries: { openai: { enabled: boolean } };
|
|
};
|
|
};
|
|
}) {
|
|
expect(mocks.loadPluginManifestRegistry).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
config: params.cfg,
|
|
env: process.env,
|
|
}),
|
|
);
|
|
expect(mocks.withBundledPluginEnablementCompat).toHaveBeenCalledWith({
|
|
config: params.allowlistCompat,
|
|
pluginIds: ["openai"],
|
|
});
|
|
expect(mocks.withBundledPluginVitestCompat).toHaveBeenCalledWith({
|
|
config: params.enablementCompat,
|
|
pluginIds: ["openai"],
|
|
env: process.env,
|
|
});
|
|
expectActiveRegistryLookup(["openai"]);
|
|
}
|
|
|
|
function createCompatChainConfig() {
|
|
const cfg = { plugins: { allow: ["custom-plugin"] } } as OpenClawConfig;
|
|
const allowlistCompat = {
|
|
plugins: {
|
|
allow: ["custom-plugin", "openai"],
|
|
},
|
|
} as OpenClawConfig;
|
|
const enablementCompat = {
|
|
plugins: {
|
|
allow: ["custom-plugin", "openai"],
|
|
entries: { openai: { enabled: true } },
|
|
},
|
|
};
|
|
return { cfg, allowlistCompat, enablementCompat };
|
|
}
|
|
|
|
function setBundledCapabilityFixture(
|
|
contractKey: string,
|
|
pluginId = "openai",
|
|
providerId = pluginId,
|
|
) {
|
|
mocks.loadPluginManifestRegistry.mockReturnValue({
|
|
plugins: [
|
|
{
|
|
id: pluginId,
|
|
origin: "bundled",
|
|
contracts: { [contractKey]: [providerId] },
|
|
},
|
|
{
|
|
id: "custom-plugin",
|
|
origin: "workspace",
|
|
contracts: {},
|
|
},
|
|
] as never,
|
|
diagnostics: [],
|
|
});
|
|
}
|
|
|
|
function expectCompatChainApplied(params: {
|
|
key:
|
|
| "memoryEmbeddingProviders"
|
|
| "speechProviders"
|
|
| "realtimeTranscriptionProviders"
|
|
| "realtimeVoiceProviders"
|
|
| "mediaUnderstandingProviders"
|
|
| "imageGenerationProviders"
|
|
| "videoGenerationProviders"
|
|
| "musicGenerationProviders";
|
|
contractKey: string;
|
|
cfg: OpenClawConfig;
|
|
allowlistCompat: OpenClawConfig;
|
|
enablementCompat: {
|
|
plugins: {
|
|
allow?: string[];
|
|
entries: { openai: { enabled: boolean } };
|
|
};
|
|
};
|
|
}) {
|
|
setBundledCapabilityFixture(params.contractKey);
|
|
mocks.withBundledPluginEnablementCompat.mockReturnValue(params.enablementCompat);
|
|
mocks.withBundledPluginVitestCompat.mockReturnValue(params.enablementCompat);
|
|
expectNoResolvedCapabilityProviders(
|
|
resolvePluginCapabilityProviders({ key: params.key, cfg: params.cfg }),
|
|
);
|
|
expectBundledCompatLoadPath(params);
|
|
}
|
|
|
|
describe("resolvePluginCapabilityProviders", () => {
|
|
beforeAll(async () => {
|
|
({
|
|
resolveBundledCapabilityProviderIds,
|
|
resolveManifestCapabilityProviderIds,
|
|
resolvePluginCapabilityProvider,
|
|
resolvePluginCapabilityProviders,
|
|
} = await import("./capability-provider-runtime.js"));
|
|
({ clearCurrentPluginMetadataSnapshot, setCurrentPluginMetadataSnapshot } =
|
|
await import("./current-plugin-metadata-snapshot.js"));
|
|
});
|
|
|
|
beforeEach(() => {
|
|
clearCurrentPluginMetadataSnapshot();
|
|
mocks.resolveRuntimePluginRegistry.mockReset();
|
|
mocks.resolveRuntimePluginRegistry.mockReturnValue(undefined);
|
|
mocks.resolvePluginRegistryLoadCacheKey.mockReset();
|
|
mocks.resolvePluginRegistryLoadCacheKey.mockImplementation((options: unknown) =>
|
|
JSON.stringify(options),
|
|
);
|
|
mocks.loadPluginRegistrySnapshot.mockReset();
|
|
mocks.loadPluginRegistrySnapshot.mockReturnValue({ plugins: [] });
|
|
mocks.loadPluginManifestRegistry.mockReset();
|
|
mocks.loadPluginManifestRegistry.mockReturnValue(createEmptyMockManifestRegistry());
|
|
mocks.loadBundledCapabilityRuntimeRegistry.mockReset();
|
|
mocks.loadBundledCapabilityRuntimeRegistry.mockImplementation(() => mocks.createMockRegistry());
|
|
mocks.withBundledPluginAllowlistCompat.mockClear();
|
|
mocks.withBundledPluginAllowlistCompat.mockImplementation(
|
|
({ config, pluginIds }: { config?: OpenClawConfig; pluginIds: string[] }) =>
|
|
({
|
|
...config,
|
|
plugins: {
|
|
...config?.plugins,
|
|
allow: Array.from(new Set([...(config?.plugins?.allow ?? []), ...pluginIds])),
|
|
},
|
|
}) as OpenClawConfig,
|
|
);
|
|
mocks.withBundledPluginEnablementCompat.mockReset();
|
|
mocks.withBundledPluginEnablementCompat.mockImplementation(({ config }) => config);
|
|
mocks.withBundledPluginVitestCompat.mockReset();
|
|
mocks.withBundledPluginVitestCompat.mockImplementation(({ config }) => config);
|
|
});
|
|
|
|
it("resolves bundled capability ids from the current metadata snapshot", () => {
|
|
setCurrentPluginMetadataSnapshot({
|
|
policyHash: "policy",
|
|
workspaceDir: "/workspace",
|
|
index: { plugins: [] },
|
|
registryDiagnostics: [],
|
|
manifestRegistry: { plugins: [], diagnostics: [] },
|
|
plugins: [
|
|
{
|
|
id: "fal",
|
|
origin: "bundled",
|
|
contracts: { imageGenerationProviders: ["fal"] },
|
|
},
|
|
],
|
|
diagnostics: [],
|
|
byPluginId: new Map(),
|
|
normalizePluginId: (id: string) => id,
|
|
owners: {
|
|
channels: new Map(),
|
|
channelConfigs: new Map(),
|
|
providers: new Map(),
|
|
modelCatalogProviders: new Map(),
|
|
cliBackends: new Map(),
|
|
setupProviders: new Map(),
|
|
commandAliases: new Map(),
|
|
contracts: new Map(),
|
|
},
|
|
metrics: {
|
|
registrySnapshotMs: 0,
|
|
manifestRegistryMs: 0,
|
|
ownerMapsMs: 0,
|
|
totalMs: 0,
|
|
indexPluginCount: 0,
|
|
manifestPluginCount: 1,
|
|
},
|
|
} as never);
|
|
|
|
expect(
|
|
resolveBundledCapabilityProviderIds({
|
|
key: "imageGenerationProviders",
|
|
workspaceDir: "/workspace",
|
|
}),
|
|
).toEqual(["fal"]);
|
|
expect(mocks.loadPluginManifestRegistry).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("resolves enabled external capability ids from the current metadata snapshot", () => {
|
|
setCurrentPluginMetadataSnapshot({
|
|
policyHash: "policy",
|
|
workspaceDir: "/workspace",
|
|
index: {
|
|
plugins: [
|
|
{ pluginId: "external-image", origin: "global", enabled: true },
|
|
{ pluginId: "external-disabled", origin: "global", enabled: false },
|
|
],
|
|
},
|
|
registryDiagnostics: [],
|
|
manifestRegistry: { plugins: [], diagnostics: [] },
|
|
plugins: [
|
|
{
|
|
id: "external-image",
|
|
origin: "global",
|
|
contracts: { imageGenerationProviders: ["external-image"] },
|
|
},
|
|
{
|
|
id: "external-disabled",
|
|
origin: "global",
|
|
contracts: { imageGenerationProviders: ["external-disabled"] },
|
|
},
|
|
],
|
|
diagnostics: [],
|
|
byPluginId: new Map(),
|
|
normalizePluginId: (id: string) => id,
|
|
owners: {
|
|
channels: new Map(),
|
|
channelConfigs: new Map(),
|
|
providers: new Map(),
|
|
modelCatalogProviders: new Map(),
|
|
cliBackends: new Map(),
|
|
setupProviders: new Map(),
|
|
commandAliases: new Map(),
|
|
contracts: new Map(),
|
|
},
|
|
metrics: {
|
|
registrySnapshotMs: 0,
|
|
manifestRegistryMs: 0,
|
|
ownerMapsMs: 0,
|
|
totalMs: 0,
|
|
indexPluginCount: 2,
|
|
manifestPluginCount: 2,
|
|
},
|
|
} as never);
|
|
|
|
expect(
|
|
resolveManifestCapabilityProviderIds({
|
|
key: "imageGenerationProviders",
|
|
workspaceDir: "/workspace",
|
|
}),
|
|
).toEqual(["external-image"]);
|
|
expect(mocks.loadPluginManifestRegistry).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("uses the active registry when capability providers are already loaded", () => {
|
|
const active = createEmptyPluginRegistry();
|
|
active.speechProviders.push({
|
|
pluginId: "openai",
|
|
pluginName: "openai",
|
|
source: "test",
|
|
provider: {
|
|
id: "openai",
|
|
label: "openai",
|
|
isConfigured: () => true,
|
|
synthesize: async () => ({
|
|
audioBuffer: Buffer.from("x"),
|
|
outputFormat: "mp3",
|
|
voiceCompatible: false,
|
|
fileExtension: ".mp3",
|
|
}),
|
|
},
|
|
} as never);
|
|
mocks.resolveRuntimePluginRegistry.mockReturnValue(active);
|
|
|
|
const providers = resolvePluginCapabilityProviders({ key: "speechProviders" });
|
|
|
|
expectResolvedCapabilityProviderIds(providers, ["openai"]);
|
|
expect(mocks.loadPluginManifestRegistry).not.toHaveBeenCalled();
|
|
expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith();
|
|
});
|
|
|
|
it("targets enabled external capability plugins without bundled fallback capture", () => {
|
|
const loaded = createEmptyPluginRegistry();
|
|
loaded.imageGenerationProviders.push({
|
|
pluginId: "external-image",
|
|
pluginName: "external-image",
|
|
source: "test",
|
|
provider: {
|
|
id: "external-image",
|
|
label: "External Image",
|
|
isConfigured: () => true,
|
|
generate: async () => ({
|
|
kind: "image",
|
|
images: [],
|
|
}),
|
|
},
|
|
} as never);
|
|
mocks.loadPluginRegistrySnapshot.mockReturnValue({
|
|
plugins: [{ pluginId: "external-image", origin: "global", enabled: true }],
|
|
});
|
|
mocks.loadPluginManifestRegistry.mockReturnValue({
|
|
plugins: [
|
|
{
|
|
id: "external-image",
|
|
origin: "global",
|
|
contracts: { imageGenerationProviders: ["external-image"] },
|
|
},
|
|
],
|
|
diagnostics: [],
|
|
});
|
|
mocks.resolveRuntimePluginRegistry.mockImplementation((options?: unknown) =>
|
|
options ? loaded : undefined,
|
|
);
|
|
|
|
expectResolvedCapabilityProviderIds(
|
|
resolvePluginCapabilityProviders({ key: "imageGenerationProviders" }),
|
|
["external-image"],
|
|
);
|
|
expect(mocks.resolveRuntimePluginRegistry).toHaveBeenLastCalledWith({
|
|
onlyPluginIds: ["external-image"],
|
|
});
|
|
expect(mocks.loadBundledCapabilityRuntimeRegistry).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("uses active non-speech capability providers even when cfg has explicit plugin entries", () => {
|
|
const active = createEmptyPluginRegistry();
|
|
active.mediaUnderstandingProviders.push({
|
|
pluginId: "deepgram",
|
|
pluginName: "Deepgram",
|
|
source: "test",
|
|
provider: {
|
|
id: "deepgram",
|
|
capabilities: ["audio"],
|
|
},
|
|
} as never);
|
|
mocks.resolveRuntimePluginRegistry.mockReturnValue(active);
|
|
|
|
const providers = resolvePluginCapabilityProviders({
|
|
key: "mediaUnderstandingProviders",
|
|
cfg: {
|
|
plugins: { entries: { deepgram: { enabled: true } } },
|
|
tools: {
|
|
media: {
|
|
models: [{ provider: "deepgram" }],
|
|
},
|
|
},
|
|
} as OpenClawConfig,
|
|
});
|
|
|
|
expectResolvedCapabilityProviderIds(providers, ["deepgram"]);
|
|
expect(mocks.loadPluginManifestRegistry).not.toHaveBeenCalled();
|
|
expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith();
|
|
});
|
|
|
|
it("merges configured media-understanding providers missing from the active registry", () => {
|
|
const active = createEmptyPluginRegistry();
|
|
active.mediaUnderstandingProviders.push({
|
|
pluginId: "openai",
|
|
pluginName: "OpenAI",
|
|
source: "test",
|
|
provider: {
|
|
id: "openai",
|
|
capabilities: ["image"],
|
|
},
|
|
} as never);
|
|
const loaded = createEmptyPluginRegistry();
|
|
loaded.mediaUnderstandingProviders.push(
|
|
{
|
|
pluginId: "deepgram",
|
|
pluginName: "Deepgram",
|
|
source: "test",
|
|
provider: {
|
|
id: "deepgram",
|
|
capabilities: ["audio"],
|
|
},
|
|
} as never,
|
|
{
|
|
pluginId: "google",
|
|
pluginName: "Google",
|
|
source: "test",
|
|
provider: {
|
|
id: "google",
|
|
capabilities: ["image", "audio", "video"],
|
|
},
|
|
} as never,
|
|
);
|
|
mocks.loadPluginManifestRegistry.mockReturnValue({
|
|
plugins: [
|
|
{
|
|
id: "deepgram",
|
|
origin: "bundled",
|
|
contracts: { mediaUnderstandingProviders: ["deepgram"] },
|
|
},
|
|
{
|
|
id: "google",
|
|
origin: "bundled",
|
|
contracts: { mediaUnderstandingProviders: ["google"] },
|
|
},
|
|
] as never,
|
|
diagnostics: [],
|
|
});
|
|
mocks.resolveRuntimePluginRegistry.mockImplementation((params?: unknown) =>
|
|
params === undefined ? active : loaded,
|
|
);
|
|
|
|
const providers = resolvePluginCapabilityProviders({
|
|
key: "mediaUnderstandingProviders",
|
|
cfg: {
|
|
plugins: { allow: ["openai", "deepgram", "google"] },
|
|
tools: {
|
|
media: {
|
|
audio: { enabled: true, models: [{ provider: "deepgram", model: "nova-3" }] },
|
|
},
|
|
},
|
|
} as OpenClawConfig,
|
|
});
|
|
|
|
expectResolvedCapabilityProviderIds(providers, ["openai", "deepgram"]);
|
|
expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith();
|
|
expectActiveRegistryLookup(["deepgram", "google"]);
|
|
});
|
|
|
|
it("keeps active speech providers when cfg requests an active provider alias", () => {
|
|
const active = createEmptyPluginRegistry();
|
|
active.speechProviders.push({
|
|
pluginId: "microsoft",
|
|
pluginName: "microsoft",
|
|
source: "test",
|
|
provider: {
|
|
id: "microsoft",
|
|
label: "microsoft",
|
|
aliases: ["edge"],
|
|
isConfigured: () => true,
|
|
synthesize: async () => ({
|
|
audioBuffer: Buffer.from("x"),
|
|
outputFormat: "mp3",
|
|
voiceCompatible: false,
|
|
fileExtension: ".mp3",
|
|
}),
|
|
},
|
|
} as never);
|
|
mocks.resolveRuntimePluginRegistry.mockReturnValue(active);
|
|
|
|
const providers = resolvePluginCapabilityProviders({
|
|
key: "speechProviders",
|
|
cfg: {
|
|
plugins: { entries: { microsoft: { enabled: true } } },
|
|
messages: { tts: { provider: "edge" } },
|
|
} as OpenClawConfig,
|
|
});
|
|
|
|
expectResolvedCapabilityProviderIds(providers, ["microsoft"]);
|
|
expect(mocks.loadPluginManifestRegistry).not.toHaveBeenCalled();
|
|
expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith();
|
|
});
|
|
|
|
it("keeps active capability providers when cfg has no explicit plugin config", () => {
|
|
const active = createEmptyPluginRegistry();
|
|
active.speechProviders.push({
|
|
pluginId: "acme",
|
|
pluginName: "acme",
|
|
source: "test",
|
|
provider: {
|
|
id: "acme",
|
|
label: "acme",
|
|
isConfigured: () => true,
|
|
synthesize: async () => ({
|
|
audioBuffer: Buffer.from("x"),
|
|
outputFormat: "mp3",
|
|
voiceCompatible: false,
|
|
fileExtension: ".mp3",
|
|
}),
|
|
},
|
|
} as never);
|
|
mocks.loadPluginManifestRegistry.mockReturnValue({
|
|
plugins: [
|
|
{
|
|
id: "microsoft",
|
|
origin: "bundled",
|
|
contracts: { speechProviders: ["microsoft"] },
|
|
},
|
|
] as never,
|
|
diagnostics: [],
|
|
});
|
|
mocks.resolveRuntimePluginRegistry.mockReturnValue(active);
|
|
|
|
const providers = resolvePluginCapabilityProviders({
|
|
key: "speechProviders",
|
|
cfg: { messages: { tts: { provider: "acme" } } } as OpenClawConfig,
|
|
});
|
|
|
|
expectResolvedCapabilityProviderIds(providers, ["acme"]);
|
|
expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith();
|
|
expect(mocks.resolveRuntimePluginRegistry).not.toHaveBeenCalledWith({
|
|
config: expect.anything(),
|
|
});
|
|
});
|
|
|
|
it("merges active and allowlisted bundled capability providers when cfg is passed", () => {
|
|
const active = createEmptyPluginRegistry();
|
|
active.speechProviders.push({
|
|
pluginId: "openai",
|
|
pluginName: "openai",
|
|
source: "test",
|
|
provider: {
|
|
id: "openai",
|
|
label: "openai",
|
|
isConfigured: () => true,
|
|
synthesize: async () => ({
|
|
audioBuffer: Buffer.from("x"),
|
|
outputFormat: "mp3",
|
|
voiceCompatible: false,
|
|
fileExtension: ".mp3",
|
|
}),
|
|
},
|
|
} as never);
|
|
const loaded = createEmptyPluginRegistry();
|
|
loaded.speechProviders.push({
|
|
pluginId: "microsoft",
|
|
pluginName: "microsoft",
|
|
source: "test",
|
|
provider: {
|
|
id: "microsoft",
|
|
label: "microsoft",
|
|
aliases: ["edge"],
|
|
isConfigured: () => true,
|
|
synthesize: async () => ({
|
|
audioBuffer: Buffer.from("x"),
|
|
outputFormat: "mp3",
|
|
voiceCompatible: false,
|
|
fileExtension: ".mp3",
|
|
}),
|
|
},
|
|
} as never);
|
|
mocks.loadPluginManifestRegistry.mockReturnValue({
|
|
plugins: [
|
|
{
|
|
id: "microsoft",
|
|
origin: "bundled",
|
|
contracts: { speechProviders: ["microsoft"] },
|
|
},
|
|
] as never,
|
|
diagnostics: [],
|
|
});
|
|
mocks.resolveRuntimePluginRegistry.mockImplementation((params?: unknown) =>
|
|
params === undefined ? active : loaded,
|
|
);
|
|
|
|
const providers = resolvePluginCapabilityProviders({
|
|
key: "speechProviders",
|
|
cfg: {
|
|
plugins: { allow: ["openai", "microsoft"] },
|
|
messages: { tts: { provider: "edge" } },
|
|
} as OpenClawConfig,
|
|
});
|
|
|
|
expectResolvedCapabilityProviderIds(providers, ["openai", "microsoft"]);
|
|
expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith();
|
|
expectActiveRegistryLookup(["microsoft"]);
|
|
});
|
|
|
|
it("uses bundled capability capture when runtime snapshot is empty for a requested speech provider", () => {
|
|
const active = createEmptyPluginRegistry();
|
|
active.speechProviders.push({
|
|
pluginId: "openai",
|
|
pluginName: "openai",
|
|
source: "test",
|
|
provider: {
|
|
id: "openai",
|
|
label: "openai",
|
|
isConfigured: () => true,
|
|
synthesize: async () => ({
|
|
audioBuffer: Buffer.from("x"),
|
|
outputFormat: "mp3",
|
|
voiceCompatible: false,
|
|
fileExtension: ".mp3",
|
|
}),
|
|
},
|
|
} as never);
|
|
const captured = createEmptyPluginRegistry();
|
|
captured.speechProviders.push({
|
|
pluginId: "google",
|
|
pluginName: "google",
|
|
source: "test",
|
|
provider: {
|
|
id: "google",
|
|
label: "google",
|
|
isConfigured: () => true,
|
|
synthesize: async () => ({
|
|
audioBuffer: Buffer.from("x"),
|
|
outputFormat: "mp3",
|
|
voiceCompatible: false,
|
|
fileExtension: ".mp3",
|
|
}),
|
|
},
|
|
} as never);
|
|
mocks.loadPluginManifestRegistry.mockReturnValue({
|
|
plugins: [
|
|
{
|
|
id: "google",
|
|
origin: "bundled",
|
|
contracts: { speechProviders: ["google"] },
|
|
},
|
|
{
|
|
id: "microsoft",
|
|
origin: "bundled",
|
|
contracts: { speechProviders: ["microsoft"] },
|
|
},
|
|
] as never,
|
|
diagnostics: [],
|
|
});
|
|
mocks.resolveRuntimePluginRegistry.mockImplementation((params?: unknown) =>
|
|
params === undefined ? active : createEmptyPluginRegistry(),
|
|
);
|
|
mocks.loadBundledCapabilityRuntimeRegistry.mockReturnValue(captured);
|
|
|
|
const providers = resolvePluginCapabilityProviders({
|
|
key: "speechProviders",
|
|
cfg: {
|
|
messages: { tts: { provider: "google" } },
|
|
} as OpenClawConfig,
|
|
});
|
|
|
|
expectResolvedCapabilityProviderIds(providers, ["openai", "google"]);
|
|
expectActiveRegistryLookup(["google"]);
|
|
expect(mocks.loadBundledCapabilityRuntimeRegistry).toHaveBeenCalledWith({
|
|
pluginIds: ["google"],
|
|
env: process.env,
|
|
pluginSdkResolution: undefined,
|
|
});
|
|
});
|
|
|
|
it("uses bundled capability capture when runtime snapshot misses a requested speech provider", () => {
|
|
const active = createEmptyPluginRegistry();
|
|
active.speechProviders.push({
|
|
pluginId: "openai",
|
|
pluginName: "openai",
|
|
source: "test",
|
|
provider: {
|
|
id: "openai",
|
|
label: "openai",
|
|
isConfigured: () => true,
|
|
synthesize: async () => ({
|
|
audioBuffer: Buffer.from("x"),
|
|
outputFormat: "mp3",
|
|
voiceCompatible: false,
|
|
fileExtension: ".mp3",
|
|
}),
|
|
},
|
|
} as never);
|
|
const loaded = createEmptyPluginRegistry();
|
|
loaded.speechProviders.push({
|
|
pluginId: "azure-speech",
|
|
pluginName: "azure-speech",
|
|
source: "test",
|
|
provider: {
|
|
id: "azure-speech",
|
|
label: "Azure Speech",
|
|
isConfigured: () => true,
|
|
synthesize: async () => ({
|
|
audioBuffer: Buffer.from("x"),
|
|
outputFormat: "mp3",
|
|
voiceCompatible: false,
|
|
fileExtension: ".mp3",
|
|
}),
|
|
},
|
|
} as never);
|
|
const captured = createEmptyPluginRegistry();
|
|
captured.speechProviders.push({
|
|
pluginId: "google",
|
|
pluginName: "google",
|
|
source: "test",
|
|
provider: {
|
|
id: "google",
|
|
label: "google",
|
|
isConfigured: () => true,
|
|
synthesize: async () => ({
|
|
audioBuffer: Buffer.from("x"),
|
|
outputFormat: "mp3",
|
|
voiceCompatible: false,
|
|
fileExtension: ".mp3",
|
|
}),
|
|
},
|
|
} as never);
|
|
mocks.loadPluginManifestRegistry.mockReturnValue({
|
|
plugins: [
|
|
{
|
|
id: "azure-speech",
|
|
origin: "bundled",
|
|
contracts: { speechProviders: ["azure-speech"] },
|
|
},
|
|
{
|
|
id: "google",
|
|
origin: "bundled",
|
|
contracts: { speechProviders: ["google"] },
|
|
},
|
|
] as never,
|
|
diagnostics: [],
|
|
});
|
|
mocks.resolveRuntimePluginRegistry.mockImplementation((params?: unknown) =>
|
|
params === undefined ? active : loaded,
|
|
);
|
|
mocks.loadBundledCapabilityRuntimeRegistry.mockReturnValue(captured);
|
|
|
|
const providers = resolvePluginCapabilityProviders({
|
|
key: "speechProviders",
|
|
cfg: {
|
|
messages: { tts: { provider: "google" } },
|
|
} as OpenClawConfig,
|
|
});
|
|
|
|
expectResolvedCapabilityProviderIds(providers, ["openai", "google"]);
|
|
expect(mocks.loadBundledCapabilityRuntimeRegistry).toHaveBeenCalledWith({
|
|
pluginIds: ["google"],
|
|
env: process.env,
|
|
pluginSdkResolution: undefined,
|
|
});
|
|
});
|
|
|
|
it("does not merge unrelated bundled capability providers when cfg requests one provider", () => {
|
|
const active = createEmptyPluginRegistry();
|
|
active.speechProviders.push({
|
|
pluginId: "openai",
|
|
pluginName: "openai",
|
|
source: "test",
|
|
provider: {
|
|
id: "openai",
|
|
label: "openai",
|
|
isConfigured: () => true,
|
|
synthesize: async () => ({
|
|
audioBuffer: Buffer.from("x"),
|
|
outputFormat: "mp3",
|
|
voiceCompatible: false,
|
|
fileExtension: ".mp3",
|
|
}),
|
|
},
|
|
} as never);
|
|
const loaded = createEmptyPluginRegistry();
|
|
loaded.speechProviders.push(
|
|
{
|
|
pluginId: "microsoft",
|
|
pluginName: "microsoft",
|
|
source: "test",
|
|
provider: {
|
|
id: "microsoft",
|
|
label: "microsoft",
|
|
aliases: ["edge"],
|
|
isConfigured: () => true,
|
|
synthesize: async () => ({
|
|
audioBuffer: Buffer.from("x"),
|
|
outputFormat: "mp3",
|
|
voiceCompatible: false,
|
|
fileExtension: ".mp3",
|
|
}),
|
|
},
|
|
} as never,
|
|
{
|
|
pluginId: "elevenlabs",
|
|
pluginName: "elevenlabs",
|
|
source: "test",
|
|
provider: {
|
|
id: "elevenlabs",
|
|
label: "elevenlabs",
|
|
isConfigured: () => true,
|
|
synthesize: async () => ({
|
|
audioBuffer: Buffer.from("x"),
|
|
outputFormat: "mp3",
|
|
voiceCompatible: false,
|
|
fileExtension: ".mp3",
|
|
}),
|
|
},
|
|
} as never,
|
|
);
|
|
mocks.loadPluginManifestRegistry.mockReturnValue({
|
|
plugins: [
|
|
{
|
|
id: "microsoft",
|
|
origin: "bundled",
|
|
contracts: { speechProviders: ["microsoft"] },
|
|
},
|
|
{
|
|
id: "elevenlabs",
|
|
origin: "bundled",
|
|
contracts: { speechProviders: ["elevenlabs"] },
|
|
},
|
|
] as never,
|
|
diagnostics: [],
|
|
});
|
|
mocks.resolveRuntimePluginRegistry.mockImplementation((params?: unknown) =>
|
|
params === undefined ? active : loaded,
|
|
);
|
|
|
|
const providers = resolvePluginCapabilityProviders({
|
|
key: "speechProviders",
|
|
cfg: {
|
|
plugins: { allow: ["openai", "microsoft", "elevenlabs"] },
|
|
messages: { tts: { provider: "edge" } },
|
|
} as OpenClawConfig,
|
|
});
|
|
|
|
expectResolvedCapabilityProviderIds(providers, ["openai", "microsoft"]);
|
|
});
|
|
|
|
it.each([
|
|
["memoryEmbeddingProviders", "memoryEmbeddingProviders"],
|
|
["speechProviders", "speechProviders"],
|
|
["realtimeTranscriptionProviders", "realtimeTranscriptionProviders"],
|
|
["realtimeVoiceProviders", "realtimeVoiceProviders"],
|
|
["mediaUnderstandingProviders", "mediaUnderstandingProviders"],
|
|
["imageGenerationProviders", "imageGenerationProviders"],
|
|
["videoGenerationProviders", "videoGenerationProviders"],
|
|
["musicGenerationProviders", "musicGenerationProviders"],
|
|
] as const)("applies bundled compat before fallback loading for %s", (key, contractKey) => {
|
|
const { cfg, allowlistCompat, enablementCompat } = createCompatChainConfig();
|
|
expectCompatChainApplied({
|
|
key,
|
|
contractKey,
|
|
cfg,
|
|
allowlistCompat,
|
|
enablementCompat,
|
|
});
|
|
});
|
|
|
|
it("reads manifest-derived capability plugin ids for each config snapshot", () => {
|
|
const { cfg, enablementCompat } = createCompatChainConfig();
|
|
setBundledCapabilityFixture("mediaUnderstandingProviders");
|
|
mocks.withBundledPluginEnablementCompat.mockReturnValue(enablementCompat);
|
|
mocks.withBundledPluginVitestCompat.mockReturnValue(enablementCompat);
|
|
|
|
expectNoResolvedCapabilityProviders(
|
|
resolvePluginCapabilityProviders({ key: "mediaUnderstandingProviders", cfg }),
|
|
);
|
|
expectNoResolvedCapabilityProviders(
|
|
resolvePluginCapabilityProviders({ key: "mediaUnderstandingProviders", cfg }),
|
|
);
|
|
|
|
expect(mocks.loadPluginManifestRegistry).toHaveBeenCalledTimes(2);
|
|
expect(mocks.withBundledPluginAllowlistCompat).toHaveBeenCalledTimes(2);
|
|
expect(mocks.withBundledPluginAllowlistCompat).toHaveBeenCalledWith({
|
|
config: cfg,
|
|
pluginIds: ["openai"],
|
|
});
|
|
});
|
|
|
|
it("reuses capability snapshot loads for the same config object", () => {
|
|
const { cfg, enablementCompat } = createCompatChainConfig();
|
|
const loaded = createEmptyPluginRegistry();
|
|
loaded.mediaUnderstandingProviders.push({
|
|
pluginId: "openai",
|
|
pluginName: "openai",
|
|
source: "test",
|
|
provider: {
|
|
id: "openai",
|
|
capabilities: ["image"],
|
|
},
|
|
} as never);
|
|
setBundledCapabilityFixture("mediaUnderstandingProviders");
|
|
mocks.withBundledPluginEnablementCompat.mockReturnValue(enablementCompat);
|
|
mocks.withBundledPluginVitestCompat.mockReturnValue(enablementCompat);
|
|
mocks.resolveRuntimePluginRegistry.mockImplementation((params?: unknown) =>
|
|
params === undefined ? undefined : loaded,
|
|
);
|
|
|
|
expectResolvedCapabilityProviderIds(
|
|
resolvePluginCapabilityProviders({ key: "mediaUnderstandingProviders", cfg }),
|
|
["openai"],
|
|
);
|
|
expectResolvedCapabilityProviderIds(
|
|
resolvePluginCapabilityProviders({ key: "mediaUnderstandingProviders", cfg }),
|
|
["openai"],
|
|
);
|
|
|
|
const snapshotLoads = mocks.resolveRuntimePluginRegistry.mock.calls.filter(
|
|
([options]) => options !== undefined,
|
|
);
|
|
expect(snapshotLoads).toHaveLength(1);
|
|
});
|
|
|
|
it("resolves manifest-derived capability plugin ids for equivalent config snapshots independently", () => {
|
|
const first = createCompatChainConfig();
|
|
const second = createCompatChainConfig();
|
|
setBundledCapabilityFixture("mediaUnderstandingProviders");
|
|
mocks.withBundledPluginEnablementCompat.mockReturnValue(first.enablementCompat);
|
|
mocks.withBundledPluginVitestCompat.mockReturnValue(first.enablementCompat);
|
|
|
|
expectNoResolvedCapabilityProviders(
|
|
resolvePluginCapabilityProviders({
|
|
key: "mediaUnderstandingProviders",
|
|
cfg: first.cfg,
|
|
}),
|
|
);
|
|
expectNoResolvedCapabilityProviders(
|
|
resolvePluginCapabilityProviders({
|
|
key: "mediaUnderstandingProviders",
|
|
cfg: second.cfg,
|
|
}),
|
|
);
|
|
|
|
expect(mocks.loadPluginManifestRegistry).toHaveBeenCalledTimes(2);
|
|
expect(mocks.withBundledPluginAllowlistCompat).toHaveBeenCalledTimes(2);
|
|
expect(mocks.withBundledPluginAllowlistCompat).toHaveBeenNthCalledWith(1, {
|
|
config: first.cfg,
|
|
pluginIds: ["openai"],
|
|
});
|
|
expect(mocks.withBundledPluginAllowlistCompat).toHaveBeenNthCalledWith(2, {
|
|
config: second.cfg,
|
|
pluginIds: ["openai"],
|
|
});
|
|
});
|
|
|
|
it("reuses a compatible active registry even when the capability list is empty", () => {
|
|
const active = createEmptyPluginRegistry();
|
|
mocks.resolveRuntimePluginRegistry.mockReturnValue(active);
|
|
|
|
const providers = resolvePluginCapabilityProviders({
|
|
key: "mediaUnderstandingProviders",
|
|
cfg: {} as OpenClawConfig,
|
|
});
|
|
|
|
expectNoResolvedCapabilityProviders(providers);
|
|
expectActiveRegistryLookup([]);
|
|
});
|
|
|
|
it("loads bundled capability providers even without an explicit cfg", () => {
|
|
const compatConfig = {
|
|
plugins: {
|
|
enabled: true,
|
|
allow: ["google"],
|
|
entries: { google: { enabled: true } },
|
|
},
|
|
} as OpenClawConfig;
|
|
const loaded = createEmptyPluginRegistry();
|
|
loaded.mediaUnderstandingProviders.push({
|
|
pluginId: "google",
|
|
pluginName: "google",
|
|
source: "test",
|
|
provider: {
|
|
id: "google",
|
|
capabilities: ["image", "audio", "video"],
|
|
describeImage: vi.fn(),
|
|
transcribeAudio: vi.fn(),
|
|
describeVideo: vi.fn(),
|
|
autoPriority: { image: 30, audio: 40, video: 10 },
|
|
nativeDocumentInputs: ["pdf"],
|
|
},
|
|
} as never);
|
|
setBundledCapabilityFixture("mediaUnderstandingProviders", "google", "google");
|
|
mocks.withBundledPluginEnablementCompat.mockReturnValue(compatConfig);
|
|
mocks.withBundledPluginVitestCompat.mockReturnValue(compatConfig);
|
|
mocks.resolveRuntimePluginRegistry.mockImplementation((params?: unknown) =>
|
|
params === undefined ? undefined : loaded,
|
|
);
|
|
|
|
const providers = resolvePluginCapabilityProviders({ key: "mediaUnderstandingProviders" });
|
|
|
|
expectResolvedCapabilityProviderIds(providers, ["google"]);
|
|
expect(mocks.loadPluginManifestRegistry).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
config: {},
|
|
env: process.env,
|
|
}),
|
|
);
|
|
expectActiveRegistryLookup(["google"]);
|
|
});
|
|
|
|
it("loads fallback snapshots without startup dependency repair", () => {
|
|
const cfg = { plugins: { allow: ["custom-plugin"] } } as OpenClawConfig;
|
|
const enablementCompat = {
|
|
plugins: {
|
|
allow: ["custom-plugin", "openai"],
|
|
entries: { openai: { enabled: true } },
|
|
},
|
|
};
|
|
setBundledCapabilityFixture("mediaUnderstandingProviders");
|
|
mocks.withBundledPluginEnablementCompat.mockReturnValue(enablementCompat);
|
|
mocks.withBundledPluginVitestCompat.mockReturnValue(enablementCompat);
|
|
|
|
expectNoResolvedCapabilityProviders(
|
|
resolvePluginCapabilityProviders({
|
|
key: "mediaUnderstandingProviders",
|
|
cfg,
|
|
}),
|
|
);
|
|
|
|
expectActiveRegistryLookup(["openai"]);
|
|
});
|
|
|
|
it("does not resolve non-speech capability providers when plugins are globally disabled", () => {
|
|
const cfg = { plugins: { enabled: false, allow: ["custom-plugin"] } } as OpenClawConfig;
|
|
const active = createEmptyPluginRegistry();
|
|
active.mediaUnderstandingProviders.push({
|
|
pluginId: "openai",
|
|
pluginName: "openai",
|
|
source: "test",
|
|
provider: {
|
|
id: "openai",
|
|
capabilities: ["image"],
|
|
},
|
|
} as never);
|
|
mocks.resolveRuntimePluginRegistry.mockReturnValue(active);
|
|
|
|
const providers = resolvePluginCapabilityProviders({
|
|
key: "mediaUnderstandingProviders",
|
|
cfg,
|
|
});
|
|
|
|
expectNoResolvedCapabilityProviders(providers);
|
|
expect(mocks.loadPluginManifestRegistry).not.toHaveBeenCalled();
|
|
expect(mocks.withBundledPluginAllowlistCompat).not.toHaveBeenCalled();
|
|
expect(mocks.withBundledPluginEnablementCompat).not.toHaveBeenCalled();
|
|
expect(mocks.withBundledPluginVitestCompat).not.toHaveBeenCalled();
|
|
expect(mocks.resolveRuntimePluginRegistry).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("loads bundled speech providers through compat when plugins are globally disabled", () => {
|
|
const cfg = {
|
|
plugins: { enabled: false },
|
|
messages: { tts: { provider: "mistral" } },
|
|
} as OpenClawConfig;
|
|
const allowlistCompat = {
|
|
...cfg,
|
|
plugins: {
|
|
enabled: false,
|
|
allow: ["microsoft"],
|
|
},
|
|
} as OpenClawConfig;
|
|
const compatConfig = {
|
|
...cfg,
|
|
plugins: {
|
|
enabled: true,
|
|
allow: ["microsoft"],
|
|
entries: { microsoft: { enabled: true } },
|
|
},
|
|
} as OpenClawConfig;
|
|
const loaded = createEmptyPluginRegistry();
|
|
loaded.speechProviders.push({
|
|
pluginId: "microsoft",
|
|
pluginName: "microsoft",
|
|
source: "test",
|
|
provider: {
|
|
id: "microsoft",
|
|
label: "microsoft",
|
|
aliases: ["edge"],
|
|
isConfigured: () => true,
|
|
synthesize: async () => ({
|
|
audioBuffer: Buffer.from("x"),
|
|
outputFormat: "mp3",
|
|
voiceCompatible: false,
|
|
fileExtension: ".mp3",
|
|
}),
|
|
},
|
|
} as never);
|
|
mocks.loadPluginManifestRegistry.mockReturnValue({
|
|
plugins: [
|
|
{
|
|
id: "microsoft",
|
|
origin: "bundled",
|
|
contracts: { speechProviders: ["microsoft"] },
|
|
},
|
|
] as never,
|
|
diagnostics: [],
|
|
});
|
|
mocks.withBundledPluginEnablementCompat.mockReturnValue(compatConfig);
|
|
mocks.withBundledPluginVitestCompat.mockReturnValue(compatConfig);
|
|
mocks.resolveRuntimePluginRegistry.mockImplementation((params?: unknown) =>
|
|
params === undefined ? undefined : loaded,
|
|
);
|
|
|
|
const providers = resolvePluginCapabilityProviders({
|
|
key: "speechProviders",
|
|
cfg,
|
|
});
|
|
|
|
expectResolvedCapabilityProviderIds(providers, ["microsoft"]);
|
|
expect(mocks.loadPluginManifestRegistry).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
config: cfg,
|
|
env: process.env,
|
|
}),
|
|
);
|
|
expect(mocks.withBundledPluginAllowlistCompat).toHaveBeenCalledWith({
|
|
config: cfg,
|
|
pluginIds: ["microsoft"],
|
|
});
|
|
expect(mocks.withBundledPluginEnablementCompat).toHaveBeenCalledWith({
|
|
config: allowlistCompat,
|
|
pluginIds: ["microsoft"],
|
|
});
|
|
expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith();
|
|
expectActiveRegistryLookup(["microsoft"]);
|
|
});
|
|
|
|
it.each([
|
|
"imageGenerationProviders",
|
|
"videoGenerationProviders",
|
|
"musicGenerationProviders",
|
|
] as const)("uses an explicit empty plugin scope for %s when no bundled owner exists", (key) => {
|
|
const providers = resolvePluginCapabilityProviders({
|
|
key,
|
|
cfg: {} as OpenClawConfig,
|
|
});
|
|
|
|
expectNoResolvedCapabilityProviders(providers as Array<{ id: string }>);
|
|
expect(mocks.loadPluginManifestRegistry).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
config: {},
|
|
env: process.env,
|
|
}),
|
|
);
|
|
expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith();
|
|
expectActiveRegistryLookup([]);
|
|
});
|
|
|
|
it("scopes media capability snapshot loads to manifest-derived bundled owners", () => {
|
|
const cfg = { plugins: { allow: ["openai", "minimax"] } } as OpenClawConfig;
|
|
mocks.loadPluginManifestRegistry.mockReturnValue({
|
|
plugins: [
|
|
{
|
|
id: "openai",
|
|
origin: "bundled",
|
|
contracts: {
|
|
imageGenerationProviders: ["openai"],
|
|
videoGenerationProviders: ["openai"],
|
|
},
|
|
},
|
|
{
|
|
id: "minimax",
|
|
origin: "bundled",
|
|
contracts: {
|
|
imageGenerationProviders: ["minimax"],
|
|
videoGenerationProviders: ["minimax"],
|
|
musicGenerationProviders: ["minimax"],
|
|
},
|
|
},
|
|
] as never,
|
|
diagnostics: [],
|
|
});
|
|
|
|
resolvePluginCapabilityProviders({ key: "imageGenerationProviders", cfg });
|
|
resolvePluginCapabilityProviders({ key: "videoGenerationProviders", cfg });
|
|
resolvePluginCapabilityProviders({ key: "musicGenerationProviders", cfg });
|
|
|
|
const snapshotLoadOptions = collectActiveRegistryLookups();
|
|
expect(snapshotLoadOptions.map((options) => options.onlyPluginIds)).toEqual([
|
|
["minimax", "openai"],
|
|
["minimax", "openai"],
|
|
["minimax"],
|
|
]);
|
|
});
|
|
|
|
it("does not unscoped-load media generation capabilities without bundled owners", () => {
|
|
const cfg = { plugins: { allow: ["openai"] } } as OpenClawConfig;
|
|
mocks.loadPluginManifestRegistry.mockReturnValue({
|
|
plugins: [
|
|
{
|
|
id: "openai",
|
|
origin: "bundled",
|
|
contracts: {
|
|
imageGenerationProviders: ["openai"],
|
|
},
|
|
},
|
|
] as never,
|
|
diagnostics: [],
|
|
});
|
|
|
|
expectNoResolvedCapabilityProviders(
|
|
resolvePluginCapabilityProviders({ key: "imageGenerationProviders", cfg }),
|
|
);
|
|
expectNoResolvedCapabilityProviders(
|
|
resolvePluginCapabilityProviders({ key: "musicGenerationProviders", cfg }),
|
|
);
|
|
|
|
const snapshotLoadOptions = collectActiveRegistryLookups();
|
|
expect(snapshotLoadOptions.map((options) => options.onlyPluginIds)).toEqual([["openai"], []]);
|
|
});
|
|
|
|
it("loads only the bundled owner plugin for a targeted provider lookup", () => {
|
|
const cfg = { plugins: { allow: ["custom-plugin"] } } as OpenClawConfig;
|
|
const allowlistCompat = {
|
|
plugins: {
|
|
allow: ["custom-plugin", "google"],
|
|
},
|
|
} as OpenClawConfig;
|
|
const enablementCompat = {
|
|
plugins: {
|
|
allow: ["custom-plugin", "google"],
|
|
entries: { google: { enabled: true } },
|
|
},
|
|
};
|
|
const loaded = createEmptyPluginRegistry();
|
|
loaded.memoryEmbeddingProviders.push({
|
|
pluginId: "google",
|
|
pluginName: "google",
|
|
source: "test",
|
|
provider: {
|
|
id: "gemini",
|
|
create: async () => ({ provider: null }),
|
|
},
|
|
} as never);
|
|
mocks.loadPluginManifestRegistry.mockReturnValue({
|
|
plugins: [
|
|
{
|
|
id: "google",
|
|
origin: "bundled",
|
|
contracts: { memoryEmbeddingProviders: ["gemini"] },
|
|
},
|
|
{
|
|
id: "openai",
|
|
origin: "bundled",
|
|
contracts: { memoryEmbeddingProviders: ["openai"] },
|
|
},
|
|
] as never,
|
|
diagnostics: [],
|
|
});
|
|
mocks.withBundledPluginEnablementCompat.mockReturnValue(enablementCompat);
|
|
mocks.withBundledPluginVitestCompat.mockReturnValue(enablementCompat);
|
|
mocks.resolveRuntimePluginRegistry.mockImplementation((params?: unknown) =>
|
|
params === undefined ? undefined : loaded,
|
|
);
|
|
|
|
const provider = resolvePluginCapabilityProvider({
|
|
key: "memoryEmbeddingProviders",
|
|
providerId: "gemini",
|
|
cfg,
|
|
});
|
|
|
|
expect(provider?.id).toBe("gemini");
|
|
expect(mocks.withBundledPluginAllowlistCompat).toHaveBeenCalledWith({
|
|
config: cfg,
|
|
pluginIds: ["google"],
|
|
});
|
|
expect(mocks.withBundledPluginEnablementCompat).toHaveBeenCalledWith({
|
|
config: allowlistCompat,
|
|
pluginIds: ["google"],
|
|
});
|
|
expectActiveRegistryLookup(["google"]);
|
|
});
|
|
|
|
it("does not load targeted non-speech capability providers when plugins are globally disabled", () => {
|
|
const cfg = { plugins: { enabled: false, allow: ["custom-plugin"] } } as OpenClawConfig;
|
|
const loaded = createEmptyPluginRegistry();
|
|
loaded.memoryEmbeddingProviders.push({
|
|
pluginId: "google",
|
|
pluginName: "google",
|
|
source: "test",
|
|
provider: {
|
|
id: "gemini",
|
|
create: async () => ({ provider: null }),
|
|
},
|
|
} as never);
|
|
mocks.loadPluginManifestRegistry.mockReturnValue({
|
|
plugins: [
|
|
{
|
|
id: "google",
|
|
origin: "bundled",
|
|
contracts: { memoryEmbeddingProviders: ["gemini"] },
|
|
},
|
|
{
|
|
id: "openai",
|
|
origin: "bundled",
|
|
contracts: { memoryEmbeddingProviders: ["openai"] },
|
|
},
|
|
] as never,
|
|
diagnostics: [],
|
|
});
|
|
mocks.resolveRuntimePluginRegistry.mockImplementation((params?: unknown) =>
|
|
params === undefined ? undefined : loaded,
|
|
);
|
|
|
|
const provider = resolvePluginCapabilityProvider({
|
|
key: "memoryEmbeddingProviders",
|
|
providerId: "gemini",
|
|
cfg,
|
|
});
|
|
|
|
expect(provider).toBeUndefined();
|
|
expect(mocks.loadPluginManifestRegistry).not.toHaveBeenCalled();
|
|
expect(mocks.withBundledPluginAllowlistCompat).not.toHaveBeenCalled();
|
|
expect(mocks.withBundledPluginEnablementCompat).not.toHaveBeenCalled();
|
|
expect(mocks.withBundledPluginVitestCompat).not.toHaveBeenCalled();
|
|
expect(mocks.resolveRuntimePluginRegistry).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("loads targeted bundled speech providers through compat when plugins are globally disabled", () => {
|
|
const cfg = { plugins: { enabled: false, allow: ["custom-plugin"] } } as OpenClawConfig;
|
|
const allowlistCompat = {
|
|
plugins: {
|
|
enabled: false,
|
|
allow: ["custom-plugin", "microsoft"],
|
|
},
|
|
} as OpenClawConfig;
|
|
const enablementCompat = {
|
|
plugins: {
|
|
enabled: true,
|
|
allow: ["custom-plugin", "microsoft"],
|
|
entries: { microsoft: { enabled: true } },
|
|
},
|
|
};
|
|
const loaded = createEmptyPluginRegistry();
|
|
loaded.speechProviders.push({
|
|
pluginId: "microsoft",
|
|
pluginName: "microsoft",
|
|
source: "test",
|
|
provider: {
|
|
id: "microsoft",
|
|
label: "microsoft",
|
|
aliases: ["edge"],
|
|
isConfigured: () => true,
|
|
synthesize: async () => ({
|
|
audioBuffer: Buffer.from("x"),
|
|
outputFormat: "mp3",
|
|
voiceCompatible: false,
|
|
fileExtension: ".mp3",
|
|
}),
|
|
},
|
|
} as never);
|
|
mocks.loadPluginManifestRegistry.mockReturnValue({
|
|
plugins: [
|
|
{
|
|
id: "microsoft",
|
|
origin: "bundled",
|
|
contracts: { speechProviders: ["microsoft"] },
|
|
},
|
|
{
|
|
id: "openai",
|
|
origin: "bundled",
|
|
contracts: { speechProviders: ["openai"] },
|
|
},
|
|
] as never,
|
|
diagnostics: [],
|
|
});
|
|
mocks.withBundledPluginEnablementCompat.mockReturnValue(enablementCompat);
|
|
mocks.withBundledPluginVitestCompat.mockReturnValue(enablementCompat);
|
|
mocks.resolveRuntimePluginRegistry.mockImplementation((params?: unknown) =>
|
|
params === undefined ? undefined : loaded,
|
|
);
|
|
|
|
const provider = resolvePluginCapabilityProvider({
|
|
key: "speechProviders",
|
|
providerId: "microsoft",
|
|
cfg,
|
|
});
|
|
|
|
expect(provider?.id).toBe("microsoft");
|
|
expect(mocks.withBundledPluginAllowlistCompat).toHaveBeenCalledWith({
|
|
config: cfg,
|
|
pluginIds: ["microsoft"],
|
|
});
|
|
expect(mocks.withBundledPluginEnablementCompat).toHaveBeenCalledWith({
|
|
config: allowlistCompat,
|
|
pluginIds: ["microsoft"],
|
|
});
|
|
expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith();
|
|
expectActiveRegistryLookup(["microsoft"]);
|
|
});
|
|
});
|