diff --git a/src/agents/runtime-plugins.test.ts b/src/agents/runtime-plugins.test.ts index 802085a00ed..8ef6dd4479b 100644 --- a/src/agents/runtime-plugins.test.ts +++ b/src/agents/runtime-plugins.test.ts @@ -1,26 +1,23 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; const hoisted = vi.hoisted(() => ({ - loadOpenClawPlugins: vi.fn(), - getCompatibleActivePluginRegistry: vi.fn(), + resolveRuntimePluginRegistry: vi.fn(), })); vi.mock("../plugins/loader.js", () => ({ - loadOpenClawPlugins: hoisted.loadOpenClawPlugins, - getCompatibleActivePluginRegistry: hoisted.getCompatibleActivePluginRegistry, + resolveRuntimePluginRegistry: hoisted.resolveRuntimePluginRegistry, })); describe("ensureRuntimePluginsLoaded", () => { beforeEach(() => { - hoisted.loadOpenClawPlugins.mockReset(); - hoisted.getCompatibleActivePluginRegistry.mockReset(); - hoisted.getCompatibleActivePluginRegistry.mockReturnValue(undefined); + hoisted.resolveRuntimePluginRegistry.mockReset(); + hoisted.resolveRuntimePluginRegistry.mockReturnValue(undefined); vi.resetModules(); }); it("does not reactivate plugins when a process already has an active registry", async () => { const { ensureRuntimePluginsLoaded } = await import("./runtime-plugins.js"); - hoisted.getCompatibleActivePluginRegistry.mockReturnValue({}); + hoisted.resolveRuntimePluginRegistry.mockReturnValue({}); ensureRuntimePluginsLoaded({ config: {} as never, @@ -28,10 +25,10 @@ describe("ensureRuntimePluginsLoaded", () => { allowGatewaySubagentBinding: true, }); - expect(hoisted.loadOpenClawPlugins).not.toHaveBeenCalled(); + expect(hoisted.resolveRuntimePluginRegistry).toHaveBeenCalledTimes(1); }); - it("loads runtime plugins when no compatible active registry exists", async () => { + it("resolves runtime plugins through the shared runtime helper", async () => { const { ensureRuntimePluginsLoaded } = await import("./runtime-plugins.js"); ensureRuntimePluginsLoaded({ @@ -40,7 +37,7 @@ describe("ensureRuntimePluginsLoaded", () => { allowGatewaySubagentBinding: true, }); - expect(hoisted.loadOpenClawPlugins).toHaveBeenCalledWith({ + expect(hoisted.resolveRuntimePluginRegistry).toHaveBeenCalledWith({ config: {} as never, workspaceDir: "/tmp/workspace", runtimeOptions: { @@ -58,13 +55,13 @@ describe("ensureRuntimePluginsLoaded", () => { allowGatewaySubagentBinding: true, }); - expect(hoisted.getCompatibleActivePluginRegistry).toHaveBeenCalledWith({ + expect(hoisted.resolveRuntimePluginRegistry).toHaveBeenCalledWith({ config: {} as never, workspaceDir: "/tmp/workspace", runtimeOptions: { allowGatewaySubagentBinding: true, }, }); - expect(hoisted.loadOpenClawPlugins).toHaveBeenCalledTimes(1); + expect(hoisted.resolveRuntimePluginRegistry).toHaveBeenCalledTimes(1); }); }); diff --git a/src/agents/runtime-plugins.ts b/src/agents/runtime-plugins.ts index 474a7d6fdd5..09957d7fb30 100644 --- a/src/agents/runtime-plugins.ts +++ b/src/agents/runtime-plugins.ts @@ -1,5 +1,5 @@ import type { OpenClawConfig } from "../config/config.js"; -import { getCompatibleActivePluginRegistry, loadOpenClawPlugins } from "../plugins/loader.js"; +import { resolveRuntimePluginRegistry } from "../plugins/loader.js"; import { resolveUserPath } from "../utils.js"; export function ensureRuntimePluginsLoaded(params: { @@ -20,8 +20,5 @@ export function ensureRuntimePluginsLoaded(params: { } : undefined, }; - if (getCompatibleActivePluginRegistry(loadOptions)) { - return; - } - loadOpenClawPlugins(loadOptions); + resolveRuntimePluginRegistry(loadOptions); } diff --git a/src/image-generation/provider-registry.test.ts b/src/image-generation/provider-registry.test.ts index 94164b70ffc..ce67dcedfce 100644 --- a/src/image-generation/provider-registry.test.ts +++ b/src/image-generation/provider-registry.test.ts @@ -2,16 +2,14 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { createEmptyPluginRegistry } from "../plugins/registry.js"; import { resetPluginRuntimeStateForTest, setActivePluginRegistry } from "../plugins/runtime.js"; -const { loadOpenClawPluginsMock, getCompatibleActivePluginRegistryMock } = vi.hoisted(() => ({ - loadOpenClawPluginsMock: vi.fn(() => createEmptyPluginRegistry()), - getCompatibleActivePluginRegistryMock: vi.fn< +const { resolveRuntimePluginRegistryMock } = vi.hoisted(() => ({ + resolveRuntimePluginRegistryMock: vi.fn< (params?: unknown) => ReturnType | undefined >(() => undefined), })); vi.mock("../plugins/loader.js", () => ({ - loadOpenClawPlugins: loadOpenClawPluginsMock, - getCompatibleActivePluginRegistry: getCompatibleActivePluginRegistryMock, + resolveRuntimePluginRegistry: resolveRuntimePluginRegistryMock, })); let getImageGenerationProvider: typeof import("./provider-registry.js").getImageGenerationProvider; @@ -19,10 +17,8 @@ let listImageGenerationProviders: typeof import("./provider-registry.js").listIm describe("image-generation provider registry", () => { afterEach(() => { - loadOpenClawPluginsMock.mockReset(); - loadOpenClawPluginsMock.mockReturnValue(createEmptyPluginRegistry()); - getCompatibleActivePluginRegistryMock.mockReset(); - getCompatibleActivePluginRegistryMock.mockReturnValue(undefined); + resolveRuntimePluginRegistryMock.mockReset(); + resolveRuntimePluginRegistryMock.mockReturnValue(undefined); resetPluginRuntimeStateForTest(); }); @@ -34,7 +30,7 @@ describe("image-generation provider registry", () => { it("does not load plugins when listing without config", () => { expect(listImageGenerationProviders()).toEqual([]); - expect(loadOpenClawPluginsMock).not.toHaveBeenCalled(); + expect(resolveRuntimePluginRegistryMock).toHaveBeenCalledWith(undefined); }); it("uses active plugin providers without loading from disk", () => { @@ -56,12 +52,12 @@ describe("image-generation provider registry", () => { }, }); setActivePluginRegistry(registry); - getCompatibleActivePluginRegistryMock.mockReturnValue(registry); + resolveRuntimePluginRegistryMock.mockReturnValue(registry); const provider = getImageGenerationProvider("custom-image"); expect(provider?.id).toBe("custom-image"); - expect(loadOpenClawPluginsMock).not.toHaveBeenCalled(); + expect(resolveRuntimePluginRegistryMock).toHaveBeenCalledWith(undefined); }); it("ignores prototype-like provider ids and aliases", () => { @@ -101,7 +97,7 @@ describe("image-generation provider registry", () => { }, ); setActivePluginRegistry(registry); - getCompatibleActivePluginRegistryMock.mockReturnValue(registry); + resolveRuntimePluginRegistryMock.mockReturnValue(registry); expect(listImageGenerationProviders().map((provider) => provider.id)).toEqual(["safe-image"]); expect(getImageGenerationProvider("__proto__")).toBeUndefined(); diff --git a/src/plugins/capability-provider-runtime.test.ts b/src/plugins/capability-provider-runtime.test.ts index 609416511b0..bc4b5bfe47e 100644 --- a/src/plugins/capability-provider-runtime.test.ts +++ b/src/plugins/capability-provider-runtime.test.ts @@ -13,8 +13,7 @@ function createEmptyMockManifestRegistry(): MockManifestRegistry { } const mocks = vi.hoisted(() => ({ - loadOpenClawPlugins: vi.fn(() => createEmptyPluginRegistry()), - getCompatibleActivePluginRegistry: vi.fn< + resolveRuntimePluginRegistry: vi.fn< (params?: unknown) => ReturnType | undefined >(() => undefined), loadPluginManifestRegistry: vi.fn<() => MockManifestRegistry>(() => @@ -26,8 +25,7 @@ const mocks = vi.hoisted(() => ({ })); vi.mock("./loader.js", () => ({ - loadOpenClawPlugins: mocks.loadOpenClawPlugins, - getCompatibleActivePluginRegistry: mocks.getCompatibleActivePluginRegistry, + resolveRuntimePluginRegistry: mocks.resolveRuntimePluginRegistry, })); vi.mock("./manifest-registry.js", () => ({ @@ -73,7 +71,7 @@ function expectBundledCompatLoadPath(params: { pluginIds: ["openai"], env: process.env, }); - expect(mocks.loadOpenClawPlugins).toHaveBeenCalledWith({ + expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith({ config: params.enablementCompat, }); } @@ -112,10 +110,8 @@ describe("resolvePluginCapabilityProviders", () => { beforeEach(async () => { vi.resetModules(); resetPluginRuntimeStateForTest(); - mocks.loadOpenClawPlugins.mockReset(); - mocks.loadOpenClawPlugins.mockReturnValue(createEmptyPluginRegistry()); - mocks.getCompatibleActivePluginRegistry.mockReset(); - mocks.getCompatibleActivePluginRegistry.mockReturnValue(undefined); + mocks.resolveRuntimePluginRegistry.mockReset(); + mocks.resolveRuntimePluginRegistry.mockReturnValue(undefined); mocks.loadPluginManifestRegistry.mockReset(); mocks.loadPluginManifestRegistry.mockReturnValue(createEmptyMockManifestRegistry()); mocks.withBundledPluginAllowlistCompat.mockReset(); @@ -145,13 +141,13 @@ describe("resolvePluginCapabilityProviders", () => { }), }, }); - mocks.getCompatibleActivePluginRegistry.mockReturnValue(active); + mocks.resolveRuntimePluginRegistry.mockReturnValue(active); const providers = resolvePluginCapabilityProviders({ key: "speechProviders" }); expectResolvedCapabilityProviderIds(providers, ["openai"]); expect(mocks.loadPluginManifestRegistry).not.toHaveBeenCalled(); - expect(mocks.loadOpenClawPlugins).not.toHaveBeenCalled(); + expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith(undefined); }); it.each([ @@ -176,7 +172,7 @@ describe("resolvePluginCapabilityProviders", () => { it("reuses a compatible active registry even when the capability list is empty", () => { const active = createEmptyPluginRegistry(); - mocks.getCompatibleActivePluginRegistry.mockReturnValue(active); + mocks.resolveRuntimePluginRegistry.mockReturnValue(active); const providers = resolvePluginCapabilityProviders({ key: "mediaUnderstandingProviders", @@ -184,6 +180,8 @@ describe("resolvePluginCapabilityProviders", () => { }); expect(providers).toEqual([]); - expect(mocks.loadOpenClawPlugins).not.toHaveBeenCalled(); + expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith({ + config: expect.anything(), + }); }); }); diff --git a/src/plugins/capability-provider-runtime.ts b/src/plugins/capability-provider-runtime.ts index 4cd2586b1c1..628768d5431 100644 --- a/src/plugins/capability-provider-runtime.ts +++ b/src/plugins/capability-provider-runtime.ts @@ -4,7 +4,7 @@ import { withBundledPluginEnablementCompat, withBundledPluginVitestCompat, } from "./bundled-compat.js"; -import { getCompatibleActivePluginRegistry, loadOpenClawPlugins } from "./loader.js"; +import { resolveRuntimePluginRegistry } from "./loader.js"; import { loadPluginManifestRegistry } from "./manifest-registry.js"; import type { PluginRegistry } from "./registry.js"; @@ -73,10 +73,7 @@ export function resolvePluginCapabilityProviders entry.provider, ) as CapabilityProviderForKey[]; diff --git a/src/plugins/loader.test.ts b/src/plugins/loader.test.ts index b64480e32c4..1595a7a863b 100644 --- a/src/plugins/loader.test.ts +++ b/src/plugins/loader.test.ts @@ -13,6 +13,7 @@ import { clearPluginLoaderCache, getCompatibleActivePluginRegistry, loadOpenClawPlugins, + resolveRuntimePluginRegistry, resolvePluginLoadCacheContext, } from "./loader.js"; import { clearPluginManifestRegistryCache } from "./manifest-registry.js"; @@ -3594,6 +3595,31 @@ describe("getCompatibleActivePluginRegistry", () => { }); }); +describe("resolveRuntimePluginRegistry", () => { + it("reuses the compatible active registry before attempting a fresh load", () => { + const registry = createEmptyPluginRegistry(); + const loadOptions = { + config: { + plugins: { + allow: ["demo"], + }, + }, + workspaceDir: "/tmp/workspace-a", + }; + const { cacheKey } = resolvePluginLoadCacheContext(loadOptions); + setActivePluginRegistry(registry, cacheKey); + + expect(resolveRuntimePluginRegistry(loadOptions)).toBe(registry); + }); + + it("falls back to the current active runtime when no explicit load context is provided", () => { + const registry = createEmptyPluginRegistry(); + setActivePluginRegistry(registry, "startup-registry"); + + expect(resolveRuntimePluginRegistry()).toBe(registry); + }); +}); + describe("clearPluginLoaderCache", () => { it("resets registered memory plugin registries", () => { registerMemoryEmbeddingProvider({ diff --git a/src/plugins/loader.ts b/src/plugins/loader.ts index c6890fcf0e9..c8b2ba23170 100644 --- a/src/plugins/loader.ts +++ b/src/plugins/loader.ts @@ -317,6 +317,15 @@ export function getCompatibleActivePluginRegistry( : undefined; } +export function resolveRuntimePluginRegistry( + options?: PluginLoadOptions, +): PluginRegistry | undefined { + if (!options || !hasExplicitCompatibilityInputs(options)) { + return getCompatibleActivePluginRegistry(); + } + return getCompatibleActivePluginRegistry(options) ?? loadOpenClawPlugins(options); +} + function validatePluginConfig(params: { schema?: Record; cacheKey?: string; diff --git a/src/plugins/tools.optional.test.ts b/src/plugins/tools.optional.test.ts index 7b938ea04ec..a1a11f094d0 100644 --- a/src/plugins/tools.optional.test.ts +++ b/src/plugins/tools.optional.test.ts @@ -8,13 +8,11 @@ type MockRegistryToolEntry = { }; const loadOpenClawPluginsMock = vi.fn(); -const getCompatibleActivePluginRegistryMock = vi.fn(); +const resolveRuntimePluginRegistryMock = vi.fn(); const applyPluginAutoEnableMock = vi.fn(); vi.mock("./loader.js", () => ({ - loadOpenClawPlugins: (params: unknown) => loadOpenClawPluginsMock(params), - getCompatibleActivePluginRegistry: (params: unknown) => - getCompatibleActivePluginRegistryMock(params), + resolveRuntimePluginRegistry: (params: unknown) => resolveRuntimePluginRegistryMock(params), })); vi.mock("../config/plugin-auto-enable.js", () => ({ @@ -137,8 +135,10 @@ describe("resolvePluginTools optional tools", () => { beforeEach(async () => { vi.resetModules(); loadOpenClawPluginsMock.mockClear(); - getCompatibleActivePluginRegistryMock.mockReset(); - getCompatibleActivePluginRegistryMock.mockReturnValue(undefined); + resolveRuntimePluginRegistryMock.mockReset(); + resolveRuntimePluginRegistryMock.mockImplementation((params) => + loadOpenClawPluginsMock(params), + ); applyPluginAutoEnableMock.mockReset(); applyPluginAutoEnableMock.mockImplementation(({ config }: { config: unknown }) => ({ config, @@ -317,7 +317,7 @@ describe("resolvePluginTools optional tools", () => { ], diagnostics: [], }; - getCompatibleActivePluginRegistryMock.mockReturnValue(activeRegistry); + resolveRuntimePluginRegistryMock.mockReturnValue(activeRegistry); const tools = resolvePluginTools( createResolveToolsParams({ diff --git a/src/plugins/tools.ts b/src/plugins/tools.ts index 2e3bf0c0163..66f16f73b88 100644 --- a/src/plugins/tools.ts +++ b/src/plugins/tools.ts @@ -3,7 +3,7 @@ import type { AnyAgentTool } from "../agents/tools/common.js"; import { applyPluginAutoEnable } from "../config/plugin-auto-enable.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; import { applyTestPluginDefaults, normalizePluginsConfig } from "./config-state.js"; -import { getCompatibleActivePluginRegistry, loadOpenClawPlugins } from "./loader.js"; +import { resolveRuntimePluginRegistry } from "./loader.js"; import { createPluginLoaderLogger } from "./logger.js"; import type { OpenClawPluginToolContext } from "./types.js"; @@ -79,8 +79,10 @@ export function resolvePluginTools(params: { env, logger: createPluginLoaderLogger(log), }; - const registry = - getCompatibleActivePluginRegistry(loadOptions) ?? loadOpenClawPlugins(loadOptions); + const registry = resolveRuntimePluginRegistry(loadOptions); + if (!registry) { + return []; + } const tools: AnyAgentTool[] = []; const existing = params.existingToolNames ?? new Set(); diff --git a/src/plugins/web-search-providers.runtime.ts b/src/plugins/web-search-providers.runtime.ts index e0b116af123..48e4b2f1186 100644 --- a/src/plugins/web-search-providers.runtime.ts +++ b/src/plugins/web-search-providers.runtime.ts @@ -6,7 +6,7 @@ import { resolvePluginSnapshotCacheTtlMs, shouldUsePluginSnapshotCache, } from "./cache-controls.js"; -import { getCompatibleActivePluginRegistry, loadOpenClawPlugins } from "./loader.js"; +import { loadOpenClawPlugins, resolveRuntimePluginRegistry } from "./loader.js"; import type { PluginLoadOptions } from "./loader.js"; import { createPluginLoaderLogger } from "./logger.js"; import { loadPluginManifestRegistry, type PluginManifestRecord } from "./manifest-registry.js"; @@ -202,10 +202,9 @@ export function resolveRuntimeWebSearchProviders(params: { bundledAllowlistCompat?: boolean; onlyPluginIds?: readonly string[]; }): PluginWebSearchProviderEntry[] { - const runtimeRegistry = - params.config === undefined - ? getCompatibleActivePluginRegistry() - : getCompatibleActivePluginRegistry(resolveWebSearchLoadOptions(params)); + const runtimeRegistry = resolveRuntimePluginRegistry( + params.config === undefined ? undefined : resolveWebSearchLoadOptions(params), + ); if (runtimeRegistry) { return mapRegistryWebSearchProviders({ registry: runtimeRegistry, diff --git a/src/tts/provider-registry.test.ts b/src/tts/provider-registry.test.ts index 56e444bc642..7320cb2b1aa 100644 --- a/src/tts/provider-registry.test.ts +++ b/src/tts/provider-registry.test.ts @@ -4,15 +4,11 @@ import { createEmptyPluginRegistry } from "../plugins/registry-empty.js"; import { resetPluginRuntimeStateForTest, setActivePluginRegistry } from "../plugins/runtime.js"; import type { SpeechProviderPlugin } from "../plugins/types.js"; -const loadOpenClawPluginsMock = vi.fn(); -const getCompatibleActivePluginRegistryMock = vi.fn(); +const resolveRuntimePluginRegistryMock = vi.fn(); vi.mock("../plugins/loader.js", () => ({ - loadOpenClawPlugins: (...args: Parameters) => - loadOpenClawPluginsMock(...args), - getCompatibleActivePluginRegistry: ( - ...args: Parameters - ) => getCompatibleActivePluginRegistryMock(...args), + resolveRuntimePluginRegistry: (...args: Parameters) => + resolveRuntimePluginRegistryMock(...args), })); let getSpeechProvider: typeof import("./provider-registry.js").getSpeechProvider; @@ -39,10 +35,8 @@ describe("speech provider registry", () => { beforeEach(async () => { vi.resetModules(); resetPluginRuntimeStateForTest(); - loadOpenClawPluginsMock.mockReset(); - loadOpenClawPluginsMock.mockReturnValue(createEmptyPluginRegistry()); - getCompatibleActivePluginRegistryMock.mockReset(); - getCompatibleActivePluginRegistryMock.mockReturnValue(undefined); + resolveRuntimePluginRegistryMock.mockReset(); + resolveRuntimePluginRegistryMock.mockReturnValue(undefined); ({ getSpeechProvider, listSpeechProviders, @@ -66,7 +60,7 @@ describe("speech provider registry", () => { }, ], }); - getCompatibleActivePluginRegistryMock.mockReturnValue({ + resolveRuntimePluginRegistryMock.mockReturnValue({ ...createEmptyPluginRegistry(), speechProviders: [ { @@ -79,11 +73,11 @@ describe("speech provider registry", () => { const providers = listSpeechProviders(); expect(providers.map((provider) => provider.id)).toEqual(["demo-speech"]); - expect(loadOpenClawPluginsMock).not.toHaveBeenCalled(); + expect(resolveRuntimePluginRegistryMock).toHaveBeenCalledWith(undefined); }); it("loads speech providers from plugins when config is provided", () => { - loadOpenClawPluginsMock.mockReturnValue({ + resolveRuntimePluginRegistryMock.mockReturnValue({ ...createEmptyPluginRegistry(), speechProviders: [ { @@ -98,7 +92,7 @@ describe("speech provider registry", () => { expect(listSpeechProviders(cfg).map((provider) => provider.id)).toEqual(["microsoft"]); expect(getSpeechProvider("edge", cfg)?.id).toBe("microsoft"); - expect(loadOpenClawPluginsMock).toHaveBeenCalledWith({ + expect(resolveRuntimePluginRegistryMock).toHaveBeenCalledWith({ config: { plugins: { entries: { @@ -114,6 +108,7 @@ describe("speech provider registry", () => { it("returns no providers when neither plugins nor active registry provide speech support", () => { expect(listSpeechProviders()).toEqual([]); expect(getSpeechProvider("demo-speech")).toBeUndefined(); + expect(resolveRuntimePluginRegistryMock).toHaveBeenCalledWith(undefined); }); it("canonicalizes the legacy edge alias to microsoft", () => { @@ -127,7 +122,7 @@ describe("speech provider registry", () => { }, ], }); - getCompatibleActivePluginRegistryMock.mockReturnValue({ + resolveRuntimePluginRegistryMock.mockReturnValue({ ...createEmptyPluginRegistry(), speechProviders: [ {