// Runtime registry loader tests cover the surviving process-root load scopes. import { beforeEach, describe, expect, it, vi } from "vitest"; const mocks = vi.hoisted(() => ({ loadOpenClawPlugins: vi.fn(), resolveConfiguredChannelPluginIds: vi.fn(), resolveChannelPluginIds: vi.fn(), resolveEffectivePluginIds: vi.fn(), collectConfiguredMemoryEmbeddingProviderIds: vi.fn< typeof import("../gateway-startup-plugin-ids.js").collectConfiguredMemoryEmbeddingProviderIds >(), applyPluginAutoEnable: vi.fn(), resolvePluginMetadataSnapshot: vi.fn(), isPluginMetadataSnapshotCompatible: vi.fn(), resolveAgentWorkspaceDir: vi.fn< typeof import("../../agents/agent-scope.js").resolveAgentWorkspaceDir >(() => "/resolved-workspace"), resolveDefaultAgentId: vi.fn( () => "default", ), })); vi.mock("../loader.js", () => ({ loadOpenClawPlugins: (...args: Parameters) => mocks.loadOpenClawPlugins(...args), })); vi.mock("../channel-plugin-ids.js", () => ({ resolveConfiguredChannelPluginIds: ( ...args: Parameters ) => mocks.resolveConfiguredChannelPluginIds(...args), resolveChannelPluginIds: (...args: Parameters) => mocks.resolveChannelPluginIds(...args), })); vi.mock("../effective-plugin-ids.js", () => ({ resolveEffectivePluginIds: (...args: Parameters) => mocks.resolveEffectivePluginIds(...args), })); vi.mock("../gateway-startup-plugin-ids.js", () => ({ collectConfiguredMemoryEmbeddingProviderIds: ( ...args: Parameters ) => mocks.collectConfiguredMemoryEmbeddingProviderIds(...args), })); vi.mock("../../config/plugin-auto-enable.js", () => ({ applyPluginAutoEnable: (...args: Parameters) => mocks.applyPluginAutoEnable(...args), })); vi.mock("../plugin-metadata-snapshot.js", () => ({ resolvePluginMetadataSnapshot: ( ...args: Parameters ) => mocks.resolvePluginMetadataSnapshot(...args), isPluginMetadataSnapshotCompatible: ( ...args: Parameters ) => mocks.isPluginMetadataSnapshotCompatible(...args), })); vi.mock("../../agents/agent-scope.js", () => ({ resolveAgentWorkspaceDir: (...args: Parameters) => mocks.resolveAgentWorkspaceDir(...args), resolveDefaultAgentId: (...args: Parameters) => mocks.resolveDefaultAgentId(...args), })); import { ensurePluginRegistryLoaded } from "./runtime-registry-loader.js"; function useMemoryProviderOwner(params: { adapterId: string; contract: "embeddingProviders" | "memoryEmbeddingProviders"; pluginId: string; }): void { mocks.resolvePluginMetadataSnapshot.mockReturnValue({ policyHash: "test", index: { installRecords: {}, plugins: [ { pluginId: params.pluginId, contributions: { contracts: { [params.contract]: [params.adapterId] }, }, }, ], }, manifestRegistry: { plugins: [], diagnostics: [] }, } as never); } function requireLoadOptions(): Record { const options = mocks.loadOpenClawPlugins.mock.calls[0]?.[0]; if (!options) { throw new Error("expected plugin load options"); } return options as Record; } describe("ensurePluginRegistryLoaded", () => { beforeEach(() => { vi.clearAllMocks(); mocks.resolvePluginMetadataSnapshot.mockReset(); mocks.isPluginMetadataSnapshotCompatible.mockReturnValue(true); mocks.applyPluginAutoEnable.mockImplementation((params) => ({ config: params.config ?? {}, changes: [], autoEnabledReasons: {}, })); }); it("loads configured channel owners through the canonical root loader", () => { const config = { channels: { demo: { enabled: true } } }; mocks.resolveConfiguredChannelPluginIds.mockReturnValue(["demo-channel"]); ensurePluginRegistryLoaded({ scope: "configured-channels", config: config as never }); expect(mocks.resolveConfiguredChannelPluginIds).toHaveBeenCalledWith( expect.objectContaining({ config, workspaceDir: "/resolved-workspace" }), ); expect(requireLoadOptions()).toEqual( expect.objectContaining({ onlyPluginIds: ["demo-channel"], throwOnLoadError: true, workspaceDir: "/resolved-workspace", }), ); }); it("keeps an empty configured-channel scope empty", () => { mocks.resolveConfiguredChannelPluginIds.mockReturnValue([]); ensurePluginRegistryLoaded({ scope: "configured-channels", config: {} }); expect(requireLoadOptions().onlyPluginIds).toEqual([]); }); it("loads effective plugin ids for the all scope", () => { const config = { plugins: { enabled: true } }; mocks.resolveEffectivePluginIds.mockReturnValue(["demo", "memory-core"]); ensurePluginRegistryLoaded({ scope: "all", config }); expect(mocks.resolveEffectivePluginIds).toHaveBeenCalledWith({ config, env: process.env, workspaceDir: "/resolved-workspace", }); expect(requireLoadOptions()).toEqual( expect.objectContaining({ onlyPluginIds: ["demo", "memory-core"], throwOnLoadError: true, }), ); }); it("loads only the selected memory backend and embedding provider owners", () => { const config = { memory: { search: { provider: "openai" } }, plugins: { allow: ["acpx", "memory-core"], slots: { memory: "memory-core" }, entries: { unrelated: { enabled: true } }, }, }; mocks.collectConfiguredMemoryEmbeddingProviderIds.mockReturnValue(new Set(["openai"])); ensurePluginRegistryLoaded({ scope: "memory", config }); expect(mocks.collectConfiguredMemoryEmbeddingProviderIds).toHaveBeenCalledWith(config); expect(requireLoadOptions()).toEqual( expect.objectContaining({ config, activationSourceConfig: config, onlyPluginIds: ["memory-core", "openai"], throwOnLoadError: true, }), ); }); it.each([ { adapterId: "gemini", contract: "memoryEmbeddingProviders" as const, pluginId: "google", }, { adapterId: "local", contract: "embeddingProviders" as const, pluginId: "llama-cpp", }, ])("loads the $pluginId owner for the $adapterId memory adapter", (provider) => { const config = { memory: { search: { provider: provider.adapterId } }, plugins: { slots: { memory: "memory-core" } }, }; mocks.collectConfiguredMemoryEmbeddingProviderIds.mockReturnValue( new Set([provider.adapterId]), ); useMemoryProviderOwner(provider); ensurePluginRegistryLoaded({ scope: "memory", config }); expect(requireLoadOptions().onlyPluginIds).toEqual( [provider.pluginId, "memory-core"].toSorted(), ); }); it("keeps a denied memory provider owner denied", () => { const config = { memory: { search: { provider: "gemini" } }, plugins: { allow: ["memory-core"], deny: ["google"], slots: { memory: "memory-core" }, }, }; mocks.collectConfiguredMemoryEmbeddingProviderIds.mockReturnValue(new Set(["gemini"])); useMemoryProviderOwner({ adapterId: "gemini", contract: "memoryEmbeddingProviders", pluginId: "google", }); ensurePluginRegistryLoaded({ scope: "memory", config }); const options = requireLoadOptions(); expect(options.onlyPluginIds).toEqual(["google", "memory-core"]); expect(options.config).toEqual(config); expect(options.activationSourceConfig).toEqual(config); }); it("keeps an explicitly disabled memory provider owner disabled", () => { const config = { memory: { search: { provider: "local" } }, plugins: { entries: { "llama-cpp": { enabled: false } }, slots: { memory: "memory-core" }, }, }; mocks.collectConfiguredMemoryEmbeddingProviderIds.mockReturnValue(new Set(["local"])); useMemoryProviderOwner({ adapterId: "local", contract: "embeddingProviders", pluginId: "llama-cpp", }); ensurePluginRegistryLoaded({ scope: "memory", config }); const options = requireLoadOptions(); expect(options.onlyPluginIds).toEqual(["llama-cpp", "memory-core"]); expect(options.config).toEqual(config); expect(options.activationSourceConfig).toEqual(config); }); it("keeps an empty memory scope empty when no backend is selected", () => { mocks.collectConfiguredMemoryEmbeddingProviderIds.mockReturnValue(new Set()); ensurePluginRegistryLoaded({ scope: "memory", config: { plugins: { slots: { memory: "none" } } }, }); expect(requireLoadOptions().onlyPluginIds).toEqual([]); }); });