diff --git a/src/image-generation/provider-registry.allowlist.test.ts b/src/image-generation/provider-registry.allowlist.test.ts deleted file mode 100644 index d444cb8d07c..00000000000 --- a/src/image-generation/provider-registry.allowlist.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { beforeAll, describe, expect, it } from "vitest"; -import type { OpenClawConfig } from "../config/config.js"; -import { - getProviderRegistryAllowlistMocks, - installProviderRegistryAllowlistMockDefaults, - primeBundledProviderAllowlistFallback, -} from "../test-utils/provider-registry-allowlist.test-helpers.js"; - -let getImageGenerationProvider: typeof import("./provider-registry.js").getImageGenerationProvider; -let listImageGenerationProviders: typeof import("./provider-registry.js").listImageGenerationProviders; -const mocks = getProviderRegistryAllowlistMocks(); -installProviderRegistryAllowlistMockDefaults(); - -describe("image-generation provider registry allowlist fallback", () => { - beforeAll(async () => { - ({ getImageGenerationProvider, listImageGenerationProviders } = - await import("./provider-registry.js")); - }); - - it("adds bundled capability plugin ids to plugins.allow before fallback registry load", () => { - const { cfg, compatConfig } = primeBundledProviderAllowlistFallback({ - contractKey: "imageGenerationProviders", - }); - - expect(listImageGenerationProviders(cfg as OpenClawConfig)).toEqual([]); - expect(getImageGenerationProvider("openai", cfg as OpenClawConfig)).toBeUndefined(); - expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith({ - config: compatConfig, - activate: false, - }); - }); -}); diff --git a/src/image-generation/provider-registry.test.ts b/src/image-generation/provider-registry.test.ts index d8f06a86fc7..4a6bbc1237c 100644 --- a/src/image-generation/provider-registry.test.ts +++ b/src/image-generation/provider-registry.test.ts @@ -1,4 +1,5 @@ import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import type { OpenClawConfig } from "../config/types.js"; import type { ImageGenerationProviderPlugin } from "../plugins/types.js"; const { resolvePluginCapabilityProvidersMock } = vi.hoisted(() => ({ @@ -40,10 +41,12 @@ describe("image-generation provider registry", () => { }); it("delegates provider resolution to the capability provider boundary", () => { - expect(listImageGenerationProviders()).toEqual([]); + const cfg = {} as OpenClawConfig; + + expect(listImageGenerationProviders(cfg)).toEqual([]); expect(resolvePluginCapabilityProvidersMock).toHaveBeenCalledWith({ key: "imageGenerationProviders", - cfg: undefined, + cfg, }); }); diff --git a/src/media-understanding/provider-registry.allowlist.test.ts b/src/media-understanding/provider-registry.allowlist.test.ts deleted file mode 100644 index 3238b572f88..00000000000 --- a/src/media-understanding/provider-registry.allowlist.test.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { beforeAll, describe, expect, it } from "vitest"; -import type { OpenClawConfig } from "../config/types.js"; -import { - getProviderRegistryAllowlistMocks, - installProviderRegistryAllowlistMockDefaults, - primeBundledProviderAllowlistFallback, -} from "../test-utils/provider-registry-allowlist.test-helpers.js"; - -let buildMediaUnderstandingRegistry: typeof import("./provider-registry.js").buildMediaUnderstandingRegistry; -let getMediaUnderstandingProvider: typeof import("./provider-registry.js").getMediaUnderstandingProvider; -const mocks = getProviderRegistryAllowlistMocks(); -installProviderRegistryAllowlistMockDefaults(); - -describe("media-understanding provider registry allowlist fallback", () => { - beforeAll(async () => { - ({ buildMediaUnderstandingRegistry, getMediaUnderstandingProvider } = - await import("./provider-registry.js")); - }); - - it("adds bundled capability plugin ids to plugins.allow before fallback registry load", () => { - const { cfg, compatConfig } = primeBundledProviderAllowlistFallback({ - contractKey: "mediaUnderstandingProviders", - }); - - const registry = buildMediaUnderstandingRegistry(undefined, cfg as OpenClawConfig); - - expect(getMediaUnderstandingProvider("openai", registry)).toBeUndefined(); - expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith({ - config: compatConfig, - activate: false, - }); - }); -}); diff --git a/src/media-understanding/provider-registry.test.ts b/src/media-understanding/provider-registry.test.ts index 644d53bb384..0550eef0af8 100644 --- a/src/media-understanding/provider-registry.test.ts +++ b/src/media-understanding/provider-registry.test.ts @@ -1,88 +1,53 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; -import { createEmptyPluginRegistry } from "../plugins/registry.js"; -import { setActivePluginRegistry } from "../plugins/runtime.js"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { buildMediaUnderstandingRegistry, getMediaUnderstandingProvider, } from "./provider-registry.js"; +import type { MediaUnderstandingProvider } from "./types.js"; -vi.mock("../plugins/capability-provider-runtime.js", async () => { - const actual = await vi.importActual( - "../plugins/capability-provider-runtime.js", - ); - const runtime = - await vi.importActual("../plugins/runtime.js"); - return { - ...actual, - resolvePluginCapabilityProviders: ({ key }: { key: string }) => - key !== "mediaUnderstandingProviders" - ? [] - : (() => { - const activeProviders = - runtime - .getActivePluginRegistry() - ?.mediaUnderstandingProviders.map((entry) => entry.provider) ?? []; - return activeProviders.length > 0 - ? activeProviders - : [ - { id: "groq", capabilities: ["image", "audio"] }, - { id: "deepgram", capabilities: ["audio"] }, - ]; - })(), - }; -}); +const resolvePluginCapabilityProvidersMock = vi.hoisted(() => vi.fn()); + +vi.mock("../plugins/capability-provider-runtime.js", () => ({ + resolvePluginCapabilityProviders: resolvePluginCapabilityProvidersMock, +})); + +function createMediaProvider( + params: Pick & + Partial, +): MediaUnderstandingProvider { + return params; +} describe("media-understanding provider registry", () => { - afterEach(() => { - setActivePluginRegistry(createEmptyPluginRegistry()); + beforeEach(() => { + resolvePluginCapabilityProvidersMock.mockReset(); + resolvePluginCapabilityProvidersMock.mockReturnValue([]); }); - it("loads bundled providers by default when no active registry is present", () => { + it("loads media providers from the capability runtime", () => { + resolvePluginCapabilityProvidersMock.mockReturnValue([ + createMediaProvider({ id: "groq", capabilities: ["image", "audio"] }), + createMediaProvider({ id: "deepgram", capabilities: ["audio"] }), + ]); + const registry = buildMediaUnderstandingRegistry(); + expect(getMediaUnderstandingProvider("groq", registry)?.id).toBe("groq"); expect(getMediaUnderstandingProvider("deepgram", registry)?.id).toBe("deepgram"); + expect(resolvePluginCapabilityProvidersMock).toHaveBeenCalledWith({ + key: "mediaUnderstandingProviders", + cfg: undefined, + }); }); - it("merges plugin-registered media providers into the active registry", async () => { - const pluginRegistry = createEmptyPluginRegistry(); - pluginRegistry.mediaUnderstandingProviders.push({ - pluginId: "google", - pluginName: "Google Plugin", - source: "test", - provider: { - id: "google", - capabilities: ["image", "audio", "video"], - describeImage: async () => ({ text: "plugin image" }), - transcribeAudio: async () => ({ text: "plugin audio" }), - describeVideo: async () => ({ text: "plugin video" }), - }, - }); - setActivePluginRegistry(pluginRegistry); + it("keeps provider id normalization behavior for capability providers", () => { + resolvePluginCapabilityProvidersMock.mockReturnValue([ + createMediaProvider({ id: "google", capabilities: ["image", "audio", "video"] }), + ]); const registry = buildMediaUnderstandingRegistry(); - const provider = getMediaUnderstandingProvider("gemini", registry); - expect(provider?.id).toBe("google"); - expect(await provider?.describeVideo?.({} as never)).toEqual({ text: "plugin video" }); - }); - - it("keeps provider id normalization behavior for plugin-owned providers", () => { - const pluginRegistry = createEmptyPluginRegistry(); - pluginRegistry.mediaUnderstandingProviders.push({ - pluginId: "google", - pluginName: "Google Plugin", - source: "test", - provider: { - id: "google", - capabilities: ["image", "audio", "video"], - }, - }); - setActivePluginRegistry(pluginRegistry); - - const registry = buildMediaUnderstandingRegistry(); - const provider = getMediaUnderstandingProvider("gemini", registry); - - expect(provider?.id).toBe("google"); + expect(getMediaUnderstandingProvider("gemini", registry)?.id).toBe("google"); }); it("auto-registers media-understanding for config providers with image-capable models (#51392)", () => { @@ -109,21 +74,15 @@ describe("media-understanding provider registry", () => { expect(textOnlyProvider).toBeUndefined(); }); - it("does not override plugin-registered providers when config also has image-capable models", async () => { - const pluginRegistry = createEmptyPluginRegistry(); - pluginRegistry.mediaUnderstandingProviders.push({ - pluginId: "google", - pluginName: "Google Plugin", - source: "test", - provider: { + it("does not override capability providers when config also has image-capable models", async () => { + resolvePluginCapabilityProvidersMock.mockReturnValue([ + createMediaProvider({ id: "google", capabilities: ["image", "audio", "video"], describeImage: async () => ({ text: "plugin image" }), transcribeAudio: async () => ({ text: "plugin audio" }), - }, - }); - setActivePluginRegistry(pluginRegistry); - + }), + ]); const cfg = { models: { providers: { @@ -140,6 +99,10 @@ describe("media-understanding provider registry", () => { expect(provider?.capabilities).toEqual(["image", "audio", "video"]); expect(await provider?.describeImage?.({} as never)).toEqual({ text: "plugin image" }); expect(await provider?.transcribeAudio?.({} as never)).toEqual({ text: "plugin audio" }); + expect(resolvePluginCapabilityProvidersMock).toHaveBeenCalledWith({ + key: "mediaUnderstandingProviders", + cfg, + }); }); it("does not auto-register providers with audio or video only inputs", () => { diff --git a/src/plugin-activation-boundary.test.ts b/src/plugin-activation-boundary.test.ts index 1aca9f2cc82..624eecebc26 100644 --- a/src/plugin-activation-boundary.test.ts +++ b/src/plugin-activation-boundary.test.ts @@ -68,6 +68,18 @@ vi.mock("./plugins/manifest-registry.js", () => ({ loadPluginManifestRegistry, })); +vi.mock("./secrets/channel-env-vars.js", () => ({ + getChannelEnvVars: (channelId: string) => { + const varsByChannel: Record = { + discord: ["DISCORD_BOT_TOKEN"], + irc: ["IRC_HOST", "IRC_NICK"], + slack: ["SLACK_BOT_TOKEN"], + telegram: ["TELEGRAM_BOT_TOKEN"], + }; + return varsByChannel[channelId] ?? []; + }, +})); + vi.mock("./plugin-sdk/facade-loader.js", () => ({ ...facadeMockHelpers, listImportedBundledPluginFacadeIds: () => [], @@ -114,7 +126,6 @@ describe("plugin activation boundary", () => { }); expect(loadBundledPluginPublicSurfaceModuleSync).not.toHaveBeenCalled(); - expect(loadBundledPluginPublicSurfaceModuleSync).not.toHaveBeenCalled(); expect(parseBrowserMajorVersion("Google Chrome 144.0.7534.0")).toBe(144); expect( loadBundledPluginPublicSurfaceModuleSync.mock.calls.map( diff --git a/src/security/audit-plugins-trust.test.ts b/src/security/audit-plugins-trust.test.ts index ff3d9cc236b..8a0d6651edc 100644 --- a/src/security/audit-plugins-trust.test.ts +++ b/src/security/audit-plugins-trust.test.ts @@ -83,6 +83,24 @@ vi.mock("../plugins/config-state.js", () => ({ }), })); +vi.mock("../plugins/plugin-registry.js", () => ({ + createPluginRegistryIdNormalizer: () => (id: string) => id, + loadPluginRegistrySnapshot: () => ({ + diagnostics: [], + plugins: [{ pluginId: "discord" }], + }), +})); + +vi.mock("../config/commands.js", () => ({ + resolveNativeSkillsEnabled: ({ + globalSetting, + providerSetting, + }: { + globalSetting?: boolean | "auto"; + providerSetting?: boolean | "auto"; + }) => providerSetting === true || (providerSetting === undefined && globalSetting === true), +})); + vi.mock("../channels/plugins/read-only.js", () => ({ listReadOnlyChannelPluginsForConfig: () => mockChannelPlugins, })); diff --git a/src/security/dangerous-config-flags.test.ts b/src/security/dangerous-config-flags.test.ts index 198059c4d64..ae1bdb96902 100644 --- a/src/security/dangerous-config-flags.test.ts +++ b/src/security/dangerous-config-flags.test.ts @@ -2,12 +2,19 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; import { collectEnabledInsecureOrDangerousFlags } from "./dangerous-config-flags.js"; -const { loadPluginManifestRegistryMock } = vi.hoisted(() => ({ - loadPluginManifestRegistryMock: vi.fn(), +const { resolvePluginConfigContractsByIdMock } = vi.hoisted(() => ({ + resolvePluginConfigContractsByIdMock: vi.fn(), })); -vi.mock("../plugins/manifest-registry.js", () => ({ - loadPluginManifestRegistry: loadPluginManifestRegistryMock, +vi.mock("../plugins/config-contracts.js", () => ({ + collectPluginConfigContractMatches: ({ + pathPattern, + root, + }: { + pathPattern: string; + root: Record; + }) => (Object.hasOwn(root, pathPattern) ? [{ path: pathPattern, value: root[pathPattern] }] : []), + resolvePluginConfigContractsById: resolvePluginConfigContractsByIdMock, })); function asConfig(value: unknown): OpenClawConfig { @@ -16,21 +23,23 @@ function asConfig(value: unknown): OpenClawConfig { describe("collectEnabledInsecureOrDangerousFlags", () => { beforeEach(() => { - loadPluginManifestRegistryMock.mockReset(); + resolvePluginConfigContractsByIdMock.mockReset(); + resolvePluginConfigContractsByIdMock.mockReturnValue(new Map()); }); it("collects manifest-declared dangerous plugin config values", () => { - loadPluginManifestRegistryMock.mockReturnValue({ - plugins: [ - { - id: "acpx", - configContracts: { - dangerousFlags: [{ path: "permissionMode", equals: "approve-all" }], + resolvePluginConfigContractsByIdMock.mockReturnValue( + new Map([ + [ + "acpx", + { + configContracts: { + dangerousFlags: [{ path: "permissionMode", equals: "approve-all" }], + }, }, - }, - ], - diagnostics: [], - }); + ], + ]), + ); expect( collectEnabledInsecureOrDangerousFlags( @@ -50,17 +59,18 @@ describe("collectEnabledInsecureOrDangerousFlags", () => { }); it("ignores plugin config values that are not declared as dangerous", () => { - loadPluginManifestRegistryMock.mockReturnValue({ - plugins: [ - { - id: "other", - configContracts: { - dangerousFlags: [{ path: "mode", equals: "danger" }], + resolvePluginConfigContractsByIdMock.mockReturnValue( + new Map([ + [ + "other", + { + configContracts: { + dangerousFlags: [{ path: "mode", equals: "danger" }], + }, }, - }, - ], - diagnostics: [], - }); + ], + ]), + ); expect( collectEnabledInsecureOrDangerousFlags( diff --git a/src/test-utils/provider-registry-allowlist.test-helpers.ts b/src/test-utils/provider-registry-allowlist.test-helpers.ts index 5e535d89e67..071d91d81ee 100644 --- a/src/test-utils/provider-registry-allowlist.test-helpers.ts +++ b/src/test-utils/provider-registry-allowlist.test-helpers.ts @@ -1,5 +1,5 @@ import { beforeEach, vi } from "vitest"; -import { createEmptyPluginRegistry } from "../plugins/registry.js"; +import { createEmptyPluginRegistry } from "../plugins/registry-empty.js"; const providerRegistryAllowlistMocks = vi.hoisted(() => ({ resolveRuntimePluginRegistry: vi.fn< diff --git a/src/trajectory/metadata.test.ts b/src/trajectory/metadata.test.ts index 5156a9140e4..ee1d7854516 100644 --- a/src/trajectory/metadata.test.ts +++ b/src/trajectory/metadata.test.ts @@ -18,8 +18,8 @@ vi.mock("../infra/os-summary.js", () => ({ }), })); -vi.mock("../plugins/manifest-registry.js", () => ({ - loadPluginManifestRegistry, +vi.mock("../plugins/plugin-registry.js", () => ({ + loadPluginManifestRegistryForPluginRegistry: loadPluginManifestRegistry, })); import { buildTrajectoryArtifacts, buildTrajectoryRunMetadata } from "./metadata.js"; diff --git a/src/tts/provider-registry.test.ts b/src/tts/provider-registry.test.ts index 02a0c982c66..357429c211b 100644 --- a/src/tts/provider-registry.test.ts +++ b/src/tts/provider-registry.test.ts @@ -1,26 +1,13 @@ import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/types.js"; -import { createEmptyPluginRegistry } from "../plugins/registry-empty.js"; import type { SpeechProviderPlugin } from "../plugins/types.js"; -const resolveRuntimePluginRegistryMock = vi.fn(); -const loadPluginManifestRegistryMock = vi.fn(() => ({ - plugins: [ - { id: "elevenlabs", origin: "bundled", contracts: { speechProviders: [{}] } }, - { id: "microsoft", origin: "bundled", contracts: { speechProviders: [{}] } }, - { id: "openai", origin: "bundled", contracts: { speechProviders: [{}] } }, - { id: "tts-local-cli", origin: "bundled", contracts: { speechProviders: [{}] } }, - ], -})); +const resolvePluginCapabilityProviderMock = vi.hoisted(() => vi.fn()); +const resolvePluginCapabilityProvidersMock = vi.hoisted(() => vi.fn()); -vi.mock("../plugins/loader.js", () => ({ - resolveRuntimePluginRegistry: (...args: Parameters) => - resolveRuntimePluginRegistryMock(...args), -})); - -vi.mock("../plugins/manifest-registry.js", () => ({ - loadPluginManifestRegistry: (...args: Parameters) => - loadPluginManifestRegistryMock(...args), +vi.mock("../plugins/capability-provider-runtime.js", () => ({ + resolvePluginCapabilityProvider: resolvePluginCapabilityProviderMock, + resolvePluginCapabilityProviders: resolvePluginCapabilityProvidersMock, })); let getSpeechProvider: typeof import("./provider-registry.js").getSpeechProvider; @@ -54,100 +41,48 @@ describe("speech provider registry", () => { }); beforeEach(() => { - resolveRuntimePluginRegistryMock.mockReset(); - resolveRuntimePluginRegistryMock.mockReturnValue(undefined); - loadPluginManifestRegistryMock.mockClear(); - }); - it("uses active plugin speech providers without reloading plugins", () => { - resolveRuntimePluginRegistryMock.mockReturnValue({ - ...createEmptyPluginRegistry(), - speechProviders: [ - { - pluginId: "test-demo-speech", - source: "test", - provider: createSpeechProvider("demo-speech"), - }, - ], - }); - const providers = listSpeechProviders(); - - expect(providers.map((provider) => provider.id)).toEqual(["demo-speech"]); - expect(resolveRuntimePluginRegistryMock).toHaveBeenCalledWith(); + resolvePluginCapabilityProviderMock.mockReset(); + resolvePluginCapabilityProviderMock.mockReturnValue(undefined); + resolvePluginCapabilityProvidersMock.mockReset(); + resolvePluginCapabilityProvidersMock.mockReturnValue([]); }); - it("uses active plugin speech providers even when config is provided", () => { - resolveRuntimePluginRegistryMock.mockReturnValue({ - ...createEmptyPluginRegistry(), - speechProviders: [ - { - pluginId: "test-microsoft", - source: "test", - provider: createSpeechProvider("microsoft", ["edge"]), - }, - ], - }); - + it("lists providers from the speech capability runtime", () => { const cfg = {} as OpenClawConfig; + resolvePluginCapabilityProvidersMock.mockReturnValue([createSpeechProvider("demo-speech")]); - expect(listSpeechProviders(cfg).map((provider) => provider.id)).toEqual(["microsoft"]); - expect(getSpeechProvider("edge", cfg)?.id).toBe("microsoft"); - expect(resolveRuntimePluginRegistryMock).toHaveBeenCalledWith(); + expect(listSpeechProviders(cfg).map((provider) => provider.id)).toEqual(["demo-speech"]); + expect(resolvePluginCapabilityProvidersMock).toHaveBeenCalledWith({ + key: "speechProviders", + cfg, + }); }); - it("loads speech providers from plugins when config is provided and no active providers exist", () => { - resolveRuntimePluginRegistryMock.mockImplementation((params?: unknown) => - params === undefined - ? createEmptyPluginRegistry() - : { - ...createEmptyPluginRegistry(), - speechProviders: [ - { - pluginId: "test-microsoft", - source: "test", - provider: createSpeechProvider("microsoft", ["edge"]), - }, - ], - }, - ); - + it("gets providers by normalized id through the capability runtime", () => { const cfg = {} as OpenClawConfig; + const provider = createSpeechProvider("microsoft", ["edge"]); + resolvePluginCapabilityProviderMock.mockReturnValue(provider); - expect(listSpeechProviders(cfg).map((provider) => provider.id)).toEqual(["microsoft"]); - expect(getSpeechProvider("edge", cfg)?.id).toBe("microsoft"); - expect(resolveRuntimePluginRegistryMock).toHaveBeenCalledWith({ - config: { - plugins: { - entries: { - elevenlabs: { enabled: true }, - microsoft: { enabled: true }, - openai: { enabled: true }, - "tts-local-cli": { enabled: true }, - }, - }, - }, - activate: false, + expect(getSpeechProvider(" MICROSOFT ", cfg)).toBe(provider); + expect(resolvePluginCapabilityProviderMock).toHaveBeenCalledWith({ + key: "speechProviders", + providerId: "microsoft", + cfg, }); }); - it("returns no providers when neither plugins nor active registry provide speech support", () => { - expect(listSpeechProviders()).toEqual([]); - expect(getSpeechProvider("demo-speech")).toBeUndefined(); - expect(resolveRuntimePluginRegistryMock).toHaveBeenCalledWith(); - }); - - it("canonicalizes the legacy edge alias to microsoft", () => { - resolveRuntimePluginRegistryMock.mockReturnValue({ - ...createEmptyPluginRegistry(), - speechProviders: [ - { - pluginId: "test-microsoft", - source: "test", - provider: createSpeechProvider("microsoft", ["edge"]), - }, - ], - }); + it("canonicalizes aliases from listed providers when direct lookup misses", () => { + resolvePluginCapabilityProvidersMock.mockReturnValue([ + createSpeechProvider("microsoft", ["edge"]), + ]); expect(normalizeSpeechProviderId("edge")).toBe("edge"); expect(canonicalizeSpeechProviderId("edge")).toBe("microsoft"); }); + + it("returns empty results when the capability runtime has no speech providers", () => { + expect(listSpeechProviders()).toEqual([]); + expect(getSpeechProvider("demo-speech")).toBeUndefined(); + expect(canonicalizeSpeechProviderId("demo-speech")).toBe("demo-speech"); + }); });