From 6f04eee2a12e665fb4b0e3b0021dbbf90efdf2a2 Mon Sep 17 00:00:00 2001 From: Shakker Date: Fri, 24 Apr 2026 04:58:03 +0100 Subject: [PATCH] fix: keep static provider entries out of live discovery --- .../provider-discovery.runtime.test.ts | 100 ++++++++++++++++++ src/plugins/provider-discovery.runtime.ts | 13 ++- 2 files changed, 110 insertions(+), 3 deletions(-) create mode 100644 src/plugins/provider-discovery.runtime.test.ts diff --git a/src/plugins/provider-discovery.runtime.test.ts b/src/plugins/provider-discovery.runtime.test.ts new file mode 100644 index 00000000000..4b34bb241cd --- /dev/null +++ b/src/plugins/provider-discovery.runtime.test.ts @@ -0,0 +1,100 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { PluginManifestRecord } from "./manifest-registry.js"; +import type { ProviderPlugin } from "./types.js"; + +const mocks = vi.hoisted(() => ({ + loadPluginManifestRegistry: vi.fn(), + resolveDiscoveredProviderPluginIds: vi.fn(), + resolvePluginProviders: vi.fn(), + loadSource: vi.fn(), +})); + +vi.mock("./manifest-registry.js", () => ({ + loadPluginManifestRegistry: mocks.loadPluginManifestRegistry, +})); + +vi.mock("./providers.js", () => ({ + resolveDiscoveredProviderPluginIds: mocks.resolveDiscoveredProviderPluginIds, +})); + +vi.mock("./providers.runtime.js", () => ({ + resolvePluginProviders: mocks.resolvePluginProviders, +})); + +vi.mock("./source-loader.js", () => ({ + createPluginSourceLoader: () => mocks.loadSource, +})); + +import { resolvePluginDiscoveryProvidersRuntime } from "./provider-discovery.runtime.js"; + +function createManifestPlugin(id: string): PluginManifestRecord { + return { + id, + enabledByDefault: true, + channels: [], + providers: [id], + cliBackends: [], + skills: [], + hooks: [], + origin: "bundled", + rootDir: `/tmp/${id}`, + source: "bundled", + manifestPath: `/tmp/${id}/openclaw.plugin.json`, + providerDiscoverySource: `/tmp/${id}/provider-discovery.ts`, + }; +} + +function createProvider(params: { id: string; mode: "static" | "catalog" }): ProviderPlugin { + const hook = { + run: async () => ({ + provider: { + baseUrl: "https://example.test/v1", + models: [], + }, + }), + }; + return { + id: params.id, + label: params.id, + auth: [], + ...(params.mode === "static" ? { staticCatalog: hook } : { catalog: hook }), + }; +} + +describe("resolvePluginDiscoveryProvidersRuntime", () => { + beforeEach(() => { + vi.clearAllMocks(); + mocks.resolveDiscoveredProviderPluginIds.mockReturnValue(["deepseek"]); + mocks.loadPluginManifestRegistry.mockReturnValue({ + plugins: [createManifestPlugin("deepseek")], + diagnostics: [], + }); + }); + + it("falls back to full provider plugins when discovery entries only expose static catalogs", () => { + const fullProvider = createProvider({ id: "deepseek", mode: "catalog" }); + mocks.loadSource.mockReturnValue(createProvider({ id: "deepseek", mode: "static" })); + mocks.resolvePluginProviders.mockReturnValue([fullProvider]); + + expect(resolvePluginDiscoveryProvidersRuntime({})).toEqual([fullProvider]); + expect(mocks.resolvePluginProviders).toHaveBeenCalledWith( + expect.objectContaining({ + bundledProviderAllowlistCompat: true, + }), + ); + }); + + it("returns static-only discovery entries for callers that explicitly request them", () => { + const staticProvider = createProvider({ id: "deepseek", mode: "static" }); + mocks.loadSource.mockReturnValue(staticProvider); + + expect(resolvePluginDiscoveryProvidersRuntime({ discoveryEntriesOnly: true })).toEqual([ + expect.objectContaining({ + id: "deepseek", + pluginId: "deepseek", + staticCatalog: staticProvider.staticCatalog, + }), + ]); + expect(mocks.resolvePluginProviders).not.toHaveBeenCalled(); + }); +}); diff --git a/src/plugins/provider-discovery.runtime.ts b/src/plugins/provider-discovery.runtime.ts index 537fd468649..3325c333638 100644 --- a/src/plugins/provider-discovery.runtime.ts +++ b/src/plugins/provider-discovery.runtime.ts @@ -37,6 +37,12 @@ function normalizeDiscoveryModule(value: ProviderDiscoveryModule): ProviderPlugi return []; } +function hasLiveProviderDiscoveryHook(provider: ProviderPlugin): boolean { + return ( + typeof provider.catalog?.run === "function" || typeof provider.discovery?.run === "function" + ); +} + function resolveProviderDiscoveryEntryPlugins(params: { config?: OpenClawConfig; workspaceDir?: string; @@ -86,11 +92,12 @@ export function resolvePluginDiscoveryProvidersRuntime(params: { discoveryEntriesOnly?: boolean; }): ProviderPlugin[] { const entryProviders = resolveProviderDiscoveryEntryPlugins(params); - if (entryProviders.length > 0) { + if (params.discoveryEntriesOnly === true) { return entryProviders; } - if (params.discoveryEntriesOnly === true) { - return []; + const liveEntryProviders = entryProviders.filter(hasLiveProviderDiscoveryHook); + if (liveEntryProviders.length > 0) { + return liveEntryProviders; } return resolvePluginProviders({ ...params,