fix(regression): restore bundled capability provider compat

This commit is contained in:
Tak Hoffman 2026-03-27 19:05:47 -05:00
parent d0cd645b4a
commit 3dbd81e610
No known key found for this signature in database
3 changed files with 198 additions and 2 deletions

View File

@ -0,0 +1,129 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import { createEmptyPluginRegistry } from "./registry.js";
import { resetPluginRuntimeStateForTest, setActivePluginRegistry } from "./runtime.js";
const mocks = vi.hoisted(() => ({
loadOpenClawPlugins: vi.fn(() => createEmptyPluginRegistry()),
loadPluginManifestRegistry: vi.fn(() => ({ plugins: [], diagnostics: [] })),
withBundledPluginAllowlistCompat: vi.fn(({ config }) => config),
withBundledPluginEnablementCompat: vi.fn(({ config }) => config),
withBundledPluginVitestCompat: vi.fn(({ config }) => config),
}));
vi.mock("./loader.js", () => ({
loadOpenClawPlugins: mocks.loadOpenClawPlugins,
}));
vi.mock("./manifest-registry.js", () => ({
loadPluginManifestRegistry: mocks.loadPluginManifestRegistry,
}));
vi.mock("./bundled-compat.js", () => ({
withBundledPluginAllowlistCompat: mocks.withBundledPluginAllowlistCompat,
withBundledPluginEnablementCompat: mocks.withBundledPluginEnablementCompat,
withBundledPluginVitestCompat: mocks.withBundledPluginVitestCompat,
}));
let resolvePluginCapabilityProviders: typeof import("./capability-provider-runtime.js").resolvePluginCapabilityProviders;
describe("resolvePluginCapabilityProviders", () => {
beforeEach(async () => {
vi.resetModules();
resetPluginRuntimeStateForTest();
mocks.loadOpenClawPlugins.mockReset();
mocks.loadOpenClawPlugins.mockReturnValue(createEmptyPluginRegistry());
mocks.loadPluginManifestRegistry.mockReset();
mocks.loadPluginManifestRegistry.mockReturnValue({ plugins: [], diagnostics: [] });
mocks.withBundledPluginAllowlistCompat.mockReset();
mocks.withBundledPluginAllowlistCompat.mockImplementation(({ config }) => config);
mocks.withBundledPluginEnablementCompat.mockReset();
mocks.withBundledPluginEnablementCompat.mockImplementation(({ config }) => config);
mocks.withBundledPluginVitestCompat.mockReset();
mocks.withBundledPluginVitestCompat.mockImplementation(({ config }) => config);
({ resolvePluginCapabilityProviders } = await import("./capability-provider-runtime.js"));
});
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",
}),
},
});
setActivePluginRegistry(active);
const providers = resolvePluginCapabilityProviders({ key: "speechProviders" });
expect(providers.map((provider) => provider.id)).toEqual(["openai"]);
expect(mocks.loadPluginManifestRegistry).not.toHaveBeenCalled();
expect(mocks.loadOpenClawPlugins).not.toHaveBeenCalled();
});
it.each([
["speechProviders", "speechProviders"],
["mediaUnderstandingProviders", "mediaUnderstandingProviders"],
["imageGenerationProviders", "imageGenerationProviders"],
] as const)("applies bundled compat before fallback loading for %s", (key, contractKey) => {
const cfg = { plugins: { allow: ["custom-plugin"] } } as OpenClawConfig;
const allowlistCompat = { plugins: { allow: ["custom-plugin", "openai"] } };
const enablementCompat = {
plugins: {
allow: ["custom-plugin", "openai"],
entries: { openai: { enabled: true } },
},
};
mocks.loadPluginManifestRegistry.mockReturnValue({
plugins: [
{
id: "openai",
origin: "bundled",
contracts: { [contractKey]: ["openai"] },
},
{
id: "custom-plugin",
origin: "workspace",
contracts: {},
},
] as Array<Record<string, unknown>>,
diagnostics: [],
});
mocks.withBundledPluginAllowlistCompat.mockReturnValue(allowlistCompat);
mocks.withBundledPluginEnablementCompat.mockReturnValue(enablementCompat);
mocks.withBundledPluginVitestCompat.mockReturnValue(enablementCompat);
resolvePluginCapabilityProviders({ key, cfg });
expect(mocks.loadPluginManifestRegistry).toHaveBeenCalledWith({
config: cfg,
env: process.env,
});
expect(mocks.withBundledPluginAllowlistCompat).toHaveBeenCalledWith({
config: cfg,
pluginIds: ["openai"],
});
expect(mocks.withBundledPluginEnablementCompat).toHaveBeenCalledWith({
config: allowlistCompat,
pluginIds: ["openai"],
});
expect(mocks.withBundledPluginVitestCompat).toHaveBeenCalledWith({
config: enablementCompat,
pluginIds: ["openai"],
env: process.env,
});
expect(mocks.loadOpenClawPlugins).toHaveBeenCalledWith({
config: enablementCompat,
});
});
});

View File

@ -1,5 +1,11 @@
import type { OpenClawConfig } from "../config/config.js";
import {
withBundledPluginAllowlistCompat,
withBundledPluginEnablementCompat,
withBundledPluginVitestCompat,
} from "./bundled-compat.js";
import { loadOpenClawPlugins } from "./loader.js";
import { loadPluginManifestRegistry } from "./manifest-registry.js";
import type { PluginRegistry } from "./registry.js";
import { getActivePluginRegistry } from "./runtime.js";
@ -8,9 +14,56 @@ type CapabilityProviderRegistryKey =
| "mediaUnderstandingProviders"
| "imageGenerationProviders";
type CapabilityContractKey =
| "speechProviders"
| "mediaUnderstandingProviders"
| "imageGenerationProviders";
type CapabilityProviderForKey<K extends CapabilityProviderRegistryKey> =
PluginRegistry[K][number] extends { provider: infer T } ? T : never;
const CAPABILITY_CONTRACT_KEY: Record<CapabilityProviderRegistryKey, CapabilityContractKey> = {
speechProviders: "speechProviders",
mediaUnderstandingProviders: "mediaUnderstandingProviders",
imageGenerationProviders: "imageGenerationProviders",
};
function resolveBundledCapabilityCompatPluginIds(params: {
key: CapabilityProviderRegistryKey;
cfg?: OpenClawConfig;
}): string[] {
const contractKey = CAPABILITY_CONTRACT_KEY[params.key];
return loadPluginManifestRegistry({
config: params.cfg,
env: process.env,
})
.plugins.filter(
(plugin) => plugin.origin === "bundled" && (plugin.contracts?.[contractKey]?.length ?? 0) > 0,
)
.map((plugin) => plugin.id)
.toSorted((left, right) => left.localeCompare(right));
}
function resolveCapabilityProviderConfig(params: {
key: CapabilityProviderRegistryKey;
cfg?: OpenClawConfig;
}) {
const pluginIds = resolveBundledCapabilityCompatPluginIds(params);
const allowlistCompat = withBundledPluginAllowlistCompat({
config: params.cfg,
pluginIds,
});
const enablementCompat = withBundledPluginEnablementCompat({
config: allowlistCompat,
pluginIds,
});
return withBundledPluginVitestCompat({
config: enablementCompat,
pluginIds,
env: process.env,
});
}
export function resolvePluginCapabilityProviders<K extends CapabilityProviderRegistryKey>(params: {
key: K;
cfg?: OpenClawConfig;
@ -20,7 +73,11 @@ export function resolvePluginCapabilityProviders<K extends CapabilityProviderReg
const shouldUseActive =
params.useActiveRegistryWhen?.(active) ?? (active?.[params.key].length ?? 0) > 0;
const registry =
shouldUseActive || !params.cfg ? active : loadOpenClawPlugins({ config: params.cfg });
shouldUseActive || !params.cfg
? active
: loadOpenClawPlugins({
config: resolveCapabilityProviderConfig({ key: params.key, cfg: params.cfg }),
});
return (registry?.[params.key] ?? []).map(
(entry) => entry.provider,
) as CapabilityProviderForKey<K>[];

View File

@ -83,7 +83,17 @@ describe("speech provider registry", () => {
expect(listSpeechProviders(cfg).map((provider) => provider.id)).toEqual(["microsoft"]);
expect(getSpeechProvider("edge", cfg)?.id).toBe("microsoft");
expect(loadOpenClawPluginsMock).toHaveBeenCalledWith({ config: cfg });
expect(loadOpenClawPluginsMock).toHaveBeenCalledWith({
config: {
plugins: {
entries: {
elevenlabs: { enabled: true },
microsoft: { enabled: true },
openai: { enabled: true },
},
},
},
});
});
it("returns no providers when neither plugins nor active registry provide speech support", () => {