import { beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; import { createEmptyPluginRegistry } from "./registry.js"; type MockManifestRegistry = { plugins: Array>; diagnostics: unknown[]; }; function createEmptyMockManifestRegistry(): MockManifestRegistry { return { plugins: [], diagnostics: [] }; } const mocks = vi.hoisted(() => ({ resolveRuntimePluginRegistry: vi.fn< (params?: unknown) => ReturnType | undefined >(() => undefined), loadPluginManifestRegistry: vi.fn<() => MockManifestRegistry>(() => createEmptyMockManifestRegistry(), ), withBundledPluginAllowlistCompat: vi.fn(({ config }) => config), withBundledPluginEnablementCompat: vi.fn(({ config }) => config), withBundledPluginVitestCompat: vi.fn(({ config }) => config), })); vi.mock("./loader.js", () => ({ resolveRuntimePluginRegistry: mocks.resolveRuntimePluginRegistry, })); 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; function expectResolvedCapabilityProviderIds(providers: Array<{ id: string }>, expected: string[]) { expect(providers.map((provider) => provider.id)).toEqual(expected); } function expectBundledCompatLoadPath(params: { cfg: OpenClawConfig; allowlistCompat: { plugins: { allow: string[] } }; enablementCompat: { plugins: { allow: string[]; entries: { openai: { enabled: boolean } }; }; }; }) { expect(mocks.loadPluginManifestRegistry).toHaveBeenCalledWith({ config: params.cfg, env: process.env, }); expect(mocks.withBundledPluginAllowlistCompat).toHaveBeenCalledWith({ config: params.cfg, pluginIds: ["openai"], }); expect(mocks.withBundledPluginEnablementCompat).toHaveBeenCalledWith({ config: params.allowlistCompat, pluginIds: ["openai"], }); expect(mocks.withBundledPluginVitestCompat).toHaveBeenCalledWith({ config: params.enablementCompat, pluginIds: ["openai"], env: process.env, }); expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith({ config: params.enablementCompat, }); } function createCompatChainConfig() { 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 } }, }, }; return { cfg, allowlistCompat, enablementCompat }; } function setBundledCapabilityFixture(contractKey: string) { mocks.loadPluginManifestRegistry.mockReturnValue({ plugins: [ { id: "openai", origin: "bundled", contracts: { [contractKey]: ["openai"] }, }, { id: "custom-plugin", origin: "workspace", contracts: {}, }, ] as never, diagnostics: [], }); } function expectCompatChainApplied(params: { key: "speechProviders" | "mediaUnderstandingProviders" | "imageGenerationProviders"; contractKey: string; cfg: OpenClawConfig; allowlistCompat: { plugins: { allow: string[] } }; enablementCompat: { plugins: { allow: string[]; entries: { openai: { enabled: boolean } }; }; }; }) { setBundledCapabilityFixture(params.contractKey); mocks.withBundledPluginAllowlistCompat.mockReturnValue(params.allowlistCompat); mocks.withBundledPluginEnablementCompat.mockReturnValue(params.enablementCompat); mocks.withBundledPluginVitestCompat.mockReturnValue(params.enablementCompat); resolvePluginCapabilityProviders({ key: params.key, cfg: params.cfg }); expectBundledCompatLoadPath(params); } describe("resolvePluginCapabilityProviders", () => { beforeEach(async () => { vi.resetModules(); mocks.resolveRuntimePluginRegistry.mockReset(); mocks.resolveRuntimePluginRegistry.mockReturnValue(undefined); mocks.loadPluginManifestRegistry.mockReset(); mocks.loadPluginManifestRegistry.mockReturnValue(createEmptyMockManifestRegistry()); 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", }), }, }); mocks.resolveRuntimePluginRegistry.mockReturnValue(active); const providers = resolvePluginCapabilityProviders({ key: "speechProviders" }); expectResolvedCapabilityProviderIds(providers, ["openai"]); expect(mocks.loadPluginManifestRegistry).not.toHaveBeenCalled(); expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith(undefined); }); it.each([ ["speechProviders", "speechProviders"], ["mediaUnderstandingProviders", "mediaUnderstandingProviders"], ["imageGenerationProviders", "imageGenerationProviders"], ] as const)("applies bundled compat before fallback loading for %s", (key, contractKey) => { const { cfg, allowlistCompat, enablementCompat } = createCompatChainConfig(); expectCompatChainApplied({ key, contractKey, cfg, allowlistCompat, enablementCompat, }); }); 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, }); expect(providers).toEqual([]); expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith({ config: expect.anything(), }); }); });