From b4c38c78f31a2d2cc924e983807682aafb970cc8 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 28 Mar 2026 03:29:41 +0000 Subject: [PATCH] test: dedupe plugin provider runtime suites --- .../capability-provider-runtime.test.ts | 21 ++-- src/plugins/provider-validation.test.ts | 8 +- src/plugins/status.test.ts | 15 ++- src/plugins/tools.optional.test.ts | 14 ++- .../web-search-providers.runtime.test.ts | 102 ++++++++++-------- src/plugins/web-search-providers.test.ts | 9 +- 6 files changed, 105 insertions(+), 64 deletions(-) diff --git a/src/plugins/capability-provider-runtime.test.ts b/src/plugins/capability-provider-runtime.test.ts index 715bdf9d798..d1454488dce 100644 --- a/src/plugins/capability-provider-runtime.test.ts +++ b/src/plugins/capability-provider-runtime.test.ts @@ -70,6 +70,18 @@ function expectBundledCompatLoadPath(params: { }); } +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: [ @@ -137,14 +149,7 @@ describe("resolvePluginCapabilityProviders", () => { ["mediaUnderstandingProviders", "mediaUnderstandingProviders"], ["imageGenerationProviders", "imageGenerationProviders"], ] as const)("applies bundled compat before fallback loading for %s", (key, contractKey) => { - 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 } }, - }, - }; + const { cfg, allowlistCompat, enablementCompat } = createCompatChainConfig(); setBundledCapabilityFixture(contractKey); mocks.withBundledPluginAllowlistCompat.mockReturnValue(allowlistCompat); mocks.withBundledPluginEnablementCompat.mockReturnValue(enablementCompat); diff --git a/src/plugins/provider-validation.test.ts b/src/plugins/provider-validation.test.ts index eb809e75b66..b7ea714fccb 100644 --- a/src/plugins/provider-validation.test.ts +++ b/src/plugins/provider-validation.test.ts @@ -30,6 +30,10 @@ function expectDiagnosticMessages( ); } +function expectDiagnosticText(diagnostics: PluginDiagnostic[], messages: readonly string[]) { + expect(diagnostics.map((diag) => diag.message)).toEqual([...messages]); +} + function normalizeProviderFixture(provider: ProviderPlugin) { const { diagnostics, pushDiagnostic } = collectDiagnostics(); const normalizedProvider = normalizeRegisteredProvider({ @@ -155,7 +159,7 @@ describe("normalizeRegisteredProvider", () => { diagnostics: PluginDiagnostic[], ) => { expect(provider?.wizard).toBeUndefined(); - expect(diagnostics.map((diag) => diag.message)).toEqual([ + expectDiagnosticText(diagnostics, [ 'provider "demo" setup metadata ignored because it has no auth methods', 'provider "demo" model-picker metadata ignored because it has no auth methods', ]); @@ -195,7 +199,7 @@ describe("normalizeRegisteredProvider", () => { expect(provider?.catalog).toBeDefined(); expect(provider?.discovery).toBeUndefined(); - expect(diagnostics.map((diag) => diag.message)).toEqual([ + expectDiagnosticText(diagnostics, [ 'provider "demo" registered both catalog and discovery; using catalog', ]); }); diff --git a/src/plugins/status.test.ts b/src/plugins/status.test.ts index 223556d0f48..def7d07e3a0 100644 --- a/src/plugins/status.test.ts +++ b/src/plugins/status.test.ts @@ -95,6 +95,13 @@ function expectNoCompatibilityWarnings() { expect(buildPluginCompatibilityWarnings()).toEqual([]); } +function expectCapabilityKinds( + inspect: NonNullable>, + kinds: readonly string[], +) { + expect(inspect.capabilities.map((entry) => entry.kind)).toEqual(kinds); +} + describe("buildPluginStatusReport", () => { beforeEach(async () => { vi.resetModules(); @@ -230,7 +237,7 @@ describe("buildPluginStatusReport", () => { expect(inspect).not.toBeNull(); expect(inspect?.shape).toBe("hybrid-capability"); expect(inspect?.capabilityMode).toBe("hybrid"); - expect(inspect?.capabilities.map((entry) => entry.kind)).toEqual([ + expectCapabilityKinds(inspect!, [ "cli-backend", "text-inference", "media-understanding", @@ -279,10 +286,7 @@ describe("buildPluginStatusReport", () => { expect(inspect.map((entry) => entry.plugin.id)).toEqual(["lca", "microsoft"]); expect(inspect.map((entry) => entry.shape)).toEqual(["hook-only", "hybrid-capability"]); expect(inspect[0]?.usesLegacyBeforeAgentStart).toBe(true); - expect(inspect[1]?.capabilities.map((entry) => entry.kind)).toEqual([ - "text-inference", - "web-search", - ]); + expectCapabilityKinds(inspect[1], ["text-inference", "web-search"]); }); it("treats a CLI-backend-only plugin as a plain capability", () => { @@ -298,6 +302,7 @@ describe("buildPluginStatusReport", () => { expect(inspect?.shape).toBe("plain-capability"); expect(inspect?.capabilityMode).toBe("plain"); + expectCapabilityKinds(inspect!, ["cli-backend"]); expect(inspect?.capabilities).toEqual([{ kind: "cli-backend", ids: ["claude-cli"] }]); }); diff --git a/src/plugins/tools.optional.test.ts b/src/plugins/tools.optional.test.ts index 5acf9ba2a54..edafeb97a5d 100644 --- a/src/plugins/tools.optional.test.ts +++ b/src/plugins/tools.optional.test.ts @@ -117,6 +117,14 @@ function expectLoaderCall(overrides: Record) { expect(loadOpenClawPluginsMock).toHaveBeenCalledWith(expect.objectContaining(overrides)); } +function expectSingleDiagnosticMessage( + diagnostics: Array<{ message: string }>, + messageFragment: string, +) { + expect(diagnostics).toHaveLength(1); + expect(diagnostics[0]?.message).toContain(messageFragment); +} + describe("resolvePluginTools optional tools", () => { beforeEach(async () => { vi.resetModules(); @@ -170,8 +178,7 @@ describe("resolvePluginTools optional tools", () => { ); expect(tools).toHaveLength(0); - expect(registry.diagnostics).toHaveLength(1); - expect(registry.diagnostics[0]?.message).toContain("plugin id conflicts with core tool name"); + expectSingleDiagnosticMessage(registry.diagnostics, "plugin id conflicts with core tool name"); }); it("skips conflicting tool names but keeps other tools", () => { @@ -179,8 +186,7 @@ describe("resolvePluginTools optional tools", () => { const tools = resolveWithConflictingCoreName(); expectResolvedToolNames(tools, ["other_tool"]); - expect(registry.diagnostics).toHaveLength(1); - expect(registry.diagnostics[0]?.message).toContain("plugin tool name conflict"); + expectSingleDiagnosticMessage(registry.diagnostics, "plugin tool name conflict"); }); it("suppresses conflict diagnostics when requested", () => { diff --git a/src/plugins/web-search-providers.runtime.test.ts b/src/plugins/web-search-providers.runtime.test.ts index 0490ff30c59..63d5099a67b 100644 --- a/src/plugins/web-search-providers.runtime.test.ts +++ b/src/plugins/web-search-providers.runtime.test.ts @@ -131,6 +131,51 @@ function expectBundledRuntimeProviderKeys( ); } +function createManifestRegistryFixture() { + return { + plugins: [ + { + id: "brave", + origin: "bundled", + rootDir: "/tmp/brave", + source: "/tmp/brave/index.js", + manifestPath: "/tmp/brave/openclaw.plugin.json", + channels: [], + providers: [], + skills: [], + hooks: [], + configUiHints: { "webSearch.apiKey": { label: "key" } }, + }, + { + id: "noise", + origin: "bundled", + rootDir: "/tmp/noise", + source: "/tmp/noise/index.js", + manifestPath: "/tmp/noise/openclaw.plugin.json", + channels: [], + providers: [], + skills: [], + hooks: [], + configUiHints: { unrelated: { label: "nope" } }, + }, + ], + diagnostics: [], + }; +} + +function expectLoaderCallCount(count: number) { + expect(loadOpenClawPluginsMock).toHaveBeenCalledTimes(count); +} + +function expectScopedWebSearchCandidates(pluginIds: readonly string[]) { + expect(loadPluginManifestRegistryMock).toHaveBeenCalled(); + expect(loadOpenClawPluginsMock).toHaveBeenCalledWith( + expect.objectContaining({ + onlyPluginIds: [...pluginIds], + }), + ); +} + describe("resolvePluginWebSearchProviders", () => { beforeAll(async () => { ({ createEmptyPluginRegistry } = await import("./registry.js")); @@ -148,39 +193,13 @@ describe("resolvePluginWebSearchProviders", () => { resetWebSearchProviderSnapshotCacheForTests(); loadPluginManifestRegistryMock = vi .spyOn(manifestRegistryModule, "loadPluginManifestRegistry") - .mockReturnValue({ - plugins: [ - { - id: "brave", - origin: "bundled", - rootDir: "/tmp/brave", - source: "/tmp/brave/index.js", - manifestPath: "/tmp/brave/openclaw.plugin.json", - channels: [], - providers: [], - skills: [], - hooks: [], - configUiHints: { "webSearch.apiKey": { label: "key" } }, - }, - { - id: "noise", - origin: "bundled", - rootDir: "/tmp/noise", - source: "/tmp/noise/index.js", - manifestPath: "/tmp/noise/openclaw.plugin.json", - channels: [], - providers: [], - skills: [], - hooks: [], - configUiHints: { unrelated: { label: "nope" } }, - }, - ], - diagnostics: [], - } as ManifestRegistryModule["loadPluginManifestRegistry"] extends ( - ...args: unknown[] - ) => infer R - ? R - : never); + .mockReturnValue( + createManifestRegistryFixture() as ManifestRegistryModule["loadPluginManifestRegistry"] extends ( + ...args: unknown[] + ) => infer R + ? R + : never, + ); loadOpenClawPluginsMock = vi .spyOn(loaderModule, "loadOpenClawPlugins") .mockImplementation((params) => { @@ -201,18 +220,13 @@ describe("resolvePluginWebSearchProviders", () => { const providers = resolvePluginWebSearchProviders({}); expectBundledRuntimeProviderKeys(providers); - expect(loadOpenClawPluginsMock).toHaveBeenCalledTimes(1); + expectLoaderCallCount(1); }); it("scopes plugin loading to manifest-declared web-search candidates", () => { resolvePluginWebSearchProviders({}); - expect(loadPluginManifestRegistryMock).toHaveBeenCalled(); - expect(loadOpenClawPluginsMock).toHaveBeenCalledWith( - expect.objectContaining({ - onlyPluginIds: ["brave"], - }), - ); + expectScopedWebSearchCandidates(["brave"]); }); it("memoizes snapshot provider resolution for the same config and env", () => { @@ -224,7 +238,7 @@ describe("resolvePluginWebSearchProviders", () => { const second = resolvePluginWebSearchProviders(runtimeParams); expect(second).toBe(first); - expect(loadOpenClawPluginsMock).toHaveBeenCalledTimes(1); + expectLoaderCallCount(1); }); it("invalidates the snapshot cache when config or env contents change in place", () => { @@ -236,7 +250,7 @@ describe("resolvePluginWebSearchProviders", () => { env.OPENCLAW_HOME = "/tmp/openclaw-home-b"; resolvePluginWebSearchProviders(createSnapshotParams({ config, env })); - expect(loadOpenClawPluginsMock).toHaveBeenCalledTimes(2); + expectLoaderCallCount(2); }); it.each([ @@ -258,7 +272,7 @@ describe("resolvePluginWebSearchProviders", () => { resolvePluginWebSearchProviders(createSnapshotParams({ config, env: createWebSearchEnv(env) })); resolvePluginWebSearchProviders(createSnapshotParams({ config, env: createWebSearchEnv(env) })); - expect(loadOpenClawPluginsMock).toHaveBeenCalledTimes(2); + expectLoaderCallCount(2); }); it("does not leak host Vitest env into an explicit non-Vitest cache key", () => { @@ -313,7 +327,7 @@ describe("resolvePluginWebSearchProviders", () => { resolvePluginWebSearchProviders(createSnapshotParams({ config, env })); - expect(loadOpenClawPluginsMock).toHaveBeenCalledTimes(2); + expectLoaderCallCount(2); }); it("prefers the active plugin registry for runtime resolution", () => { diff --git a/src/plugins/web-search-providers.test.ts b/src/plugins/web-search-providers.test.ts index 6f7651e3528..dc72c67c418 100644 --- a/src/plugins/web-search-providers.test.ts +++ b/src/plugins/web-search-providers.test.ts @@ -51,6 +51,13 @@ function expectBundledWebSearchProviders( ); } +function expectResolvedPluginIds( + providers: ReturnType, + expectedPluginIds: readonly string[], +) { + expect(providers.map((provider) => provider.pluginId)).toEqual(expectedPluginIds); +} + describe("resolveBundledPluginWebSearchProviders", () => { it( "returns bundled providers in alphabetical order", @@ -123,7 +130,7 @@ describe("resolveBundledPluginWebSearchProviders", () => { ])("$title", ({ params, expectedPluginIds }) => { const providers = resolveBundledPluginWebSearchProviders(params); - expect(providers.map((provider) => provider.pluginId)).toEqual(expectedPluginIds); + expectResolvedPluginIds(providers, expectedPluginIds); }); it("preserves explicit bundled provider entry state", () => {