diff --git a/src/plugins/capability-provider-runtime.test.ts b/src/plugins/capability-provider-runtime.test.ts index fccd7f8bbf8..dae3de47ae6 100644 --- a/src/plugins/capability-provider-runtime.test.ts +++ b/src/plugins/capability-provider-runtime.test.ts @@ -105,6 +105,46 @@ function setBundledCapabilityFixture(contractKey: string) { }); } +function setActiveSpeechCapabilityRegistry(providerId: string) { + const active = createEmptyPluginRegistry(); + active.speechProviders.push({ + pluginId: providerId, + pluginName: "OpenAI", + source: "test", + provider: { + id: providerId, + label: "OpenAI", + isConfigured: () => true, + synthesize: async () => ({ + audioBuffer: Buffer.from("x"), + outputFormat: "mp3", + voiceCompatible: false, + fileExtension: ".mp3", + }), + }, + }); + setActivePluginRegistry(active); +} + +function expectCompatChainApplied(params: { + key: "speechProviders" | "mediaUnderstandingProviders" | "imageGenerationProviders"; + contractKey: string; + cfg: OpenClawConfig; + allowlistCompat: { plugins: { allow: string[] } }; + enablementCompat: { + plugins: { + allow: string[]; + entries: { openai: { enabled: boolean } }; + }; + }; +}) { + setBundledCapabilityFixture(params.contractKey); + mocks.withBundledPluginAllowlistCompat.mockReturnValue(params.allowlistCompat); + mocks.withBundledPluginEnablementCompat.mockReturnValue(params.enablementCompat); + mocks.withBundledPluginVitestCompat.mockReturnValue(params.enablementCompat); + resolvePluginCapabilityProviders({ key: params.key, cfg: params.cfg }); + expectBundledCompatLoadPath(params); +} describe("resolvePluginCapabilityProviders", () => { beforeEach(async () => { vi.resetModules(); @@ -154,14 +194,9 @@ describe("resolvePluginCapabilityProviders", () => { ["imageGenerationProviders", "imageGenerationProviders"], ] as const)("applies bundled compat before fallback loading for %s", (key, contractKey) => { const { cfg, allowlistCompat, enablementCompat } = createCompatChainConfig(); - setBundledCapabilityFixture(contractKey); - mocks.withBundledPluginAllowlistCompat.mockReturnValue(allowlistCompat); - mocks.withBundledPluginEnablementCompat.mockReturnValue(enablementCompat); - mocks.withBundledPluginVitestCompat.mockReturnValue(enablementCompat); - - resolvePluginCapabilityProviders({ key, cfg }); - - expectBundledCompatLoadPath({ + expectCompatChainApplied({ + key, + contractKey, cfg, allowlistCompat, enablementCompat, diff --git a/src/plugins/memory-embedding-providers.test.ts b/src/plugins/memory-embedding-providers.test.ts index e9bcca86c0b..5917a76f4bd 100644 --- a/src/plugins/memory-embedding-providers.test.ts +++ b/src/plugins/memory-embedding-providers.test.ts @@ -37,6 +37,22 @@ function createOwnedAdapterEntry(id: string) { }; } +function expectRegisteredProviderState(params: { + entry: { + adapter: MemoryEmbeddingProviderAdapter; + ownerPluginId?: string; + }; + expectedList?: Array<{ + adapter: MemoryEmbeddingProviderAdapter; + ownerPluginId?: string; + }>; +}) { + expectRegisteredProviderEntry(params.entry.adapter.id, params.entry); + if (params.expectedList) { + expect(listRegisteredMemoryEmbeddingProviders()).toEqual(params.expectedList); + } +} + function expectMemoryEmbeddingProviderIds(expectedIds: readonly string[]) { expect(listMemoryEmbeddingProviders().map((adapter) => adapter.id)).toEqual([...expectedIds]); } @@ -81,14 +97,11 @@ describe("memory embedding provider registry", () => { expectList: false, }, ] as const)("$name", ({ entry, setup, expectList }) => { - const expectedEntry = entry; - setup(entry); - - expectRegisteredProviderEntry(entry.adapter.id, expectedEntry); - if (expectList) { - expect(listRegisteredMemoryEmbeddingProviders()).toEqual([expectedEntry]); - } + expectRegisteredProviderState({ + entry, + ...(expectList ? { expectedList: [entry] } : {}), + }); }); it("clears the registry", () => { diff --git a/src/plugins/provider-auth-choices.test.ts b/src/plugins/provider-auth-choices.test.ts index 8412ba96606..76254996c69 100644 --- a/src/plugins/provider-auth-choices.test.ts +++ b/src/plugins/provider-auth-choices.test.ts @@ -20,6 +20,10 @@ function createManifestPlugin(id: string, providerAuthChoices: Array) { + return overrides; +} + function setManifestPlugins(plugins: Array>) { loadPluginManifestRegistry.mockReturnValue({ plugins, @@ -36,21 +40,26 @@ function expectDeprecatedAuthChoice(choiceIds: string[], expectedChoiceId?: stri } } +function setSingleManifestProviderAuthChoices( + pluginId: string, + providerAuthChoices: Array>, +) { + setManifestPlugins([createManifestPlugin(pluginId, providerAuthChoices)]); +} + describe("provider auth choice manifest helpers", () => { it("flattens manifest auth choices", () => { - setManifestPlugins([ - createManifestPlugin("openai", [ - { - provider: "openai", - method: "api-key", - choiceId: "openai-api-key", - choiceLabel: "OpenAI API key", - onboardingScopes: ["text-inference"], - optionKey: "openaiApiKey", - cliFlag: "--openai-api-key", - cliOption: "--openai-api-key ", - }, - ]), + setSingleManifestProviderAuthChoices("openai", [ + createProviderAuthChoice({ + provider: "openai", + method: "api-key", + choiceId: "openai-api-key", + choiceLabel: "OpenAI API key", + onboardingScopes: ["text-inference"], + optionKey: "openaiApiKey", + cliFlag: "--openai-api-key", + cliOption: "--openai-api-key ", + }), ]); expect(resolveManifestProviderAuthChoices()).toEqual([ @@ -74,7 +83,7 @@ describe("provider auth choice manifest helpers", () => { name: "deduplicates flag metadata by option key + flag", plugins: [ createManifestPlugin("moonshot", [ - { + createProviderAuthChoice({ provider: "moonshot", method: "api-key", choiceId: "moonshot-api-key", @@ -83,8 +92,8 @@ describe("provider auth choice manifest helpers", () => { cliFlag: "--moonshot-api-key", cliOption: "--moonshot-api-key ", cliDescription: "Moonshot API key", - }, - { + }), + createProviderAuthChoice({ provider: "moonshot", method: "api-key-cn", choiceId: "moonshot-api-key-cn", @@ -93,7 +102,7 @@ describe("provider auth choice manifest helpers", () => { cliFlag: "--moonshot-api-key", cliOption: "--moonshot-api-key ", cliDescription: "Moonshot API key", - }, + }), ]), ], run: () => @@ -111,12 +120,12 @@ describe("provider auth choice manifest helpers", () => { name: "resolves deprecated auth-choice aliases through manifest metadata", plugins: [ createManifestPlugin("minimax", [ - { + createProviderAuthChoice({ provider: "minimax", method: "api-global", choiceId: "minimax-global-api", deprecatedChoiceIds: ["minimax", "minimax-api"], - }, + }), ]), ], run: () => { diff --git a/src/plugins/provider-discovery.test.ts b/src/plugins/provider-discovery.test.ts index 36f839f5217..d9598de1961 100644 --- a/src/plugins/provider-discovery.test.ts +++ b/src/plugins/provider-discovery.test.ts @@ -61,6 +61,19 @@ function createCatalogRuntimeContext() { }; } +function expectNormalizedDiscoveryResult(params: { + provider: ProviderPlugin; + result: Parameters[0]["result"]; + expected: Record; +}) { + expect( + normalizePluginDiscoveryResult({ + provider: params.provider, + result: params.result, + }), + ).toEqual(params.expected); +} + describe("groupPluginDiscoveryProvidersByOrder", () => { it.each([ { @@ -132,8 +145,7 @@ describe("normalizePluginDiscoveryResult", () => { }, }, ] as const)("$name", ({ provider, result, expected }) => { - const normalized = normalizePluginDiscoveryResult({ provider, result }); - expect(normalized).toEqual(expected); + expectNormalizedDiscoveryResult({ provider, result, expected }); }); }); diff --git a/src/plugins/provider-onboarding-config.test.ts b/src/plugins/provider-onboarding-config.test.ts index ff08433650a..afcd462fe72 100644 --- a/src/plugins/provider-onboarding-config.test.ts +++ b/src/plugins/provider-onboarding-config.test.ts @@ -45,6 +45,12 @@ function expectProviderModels( expect(providers?.[providerId]).toMatchObject(expected); } +function expectDefaultPrimaryModel(cfg: OpenClawConfig, modelRef: string) { + expect(cfg.agents?.defaults?.model).toEqual({ + primary: modelRef, + }); +} + function createDemoProviderParams(params?: { providerId?: string; baseUrl?: string; @@ -135,9 +141,7 @@ describe("provider onboarding preset appliers", () => { { id: "b", name: "Model B" }, ], }); - expect(cfg.agents?.defaults?.model).toEqual({ - primary: "demo/a", - }); + expectDefaultPrimaryModel(cfg, "demo/a"); return; } diff --git a/src/plugins/provider-validation.test.ts b/src/plugins/provider-validation.test.ts index 39e213c941b..d294b6dcf38 100644 --- a/src/plugins/provider-validation.test.ts +++ b/src/plugins/provider-validation.test.ts @@ -67,6 +67,20 @@ function expectNormalizedProviderFixture(params: { return result; } +function expectProviderNormalizationResult(params: { + provider: ProviderPlugin; + expectedProvider?: Record; + expectedDiagnostics?: ReadonlyArray<{ level: PluginDiagnostic["level"]; message: string }>; + expectedDiagnosticText?: readonly string[]; + assert?: ( + provider: ReturnType, + diagnostics: PluginDiagnostic[], + ) => void; +}) { + const { diagnostics, provider } = expectNormalizedProviderFixture(params); + params.assert?.(provider, diagnostics); +} + describe("normalizeRegisteredProvider", () => { it.each([ { @@ -187,16 +201,12 @@ describe("normalizeRegisteredProvider", () => { ] as const)( "$name", ({ provider: inputProvider, expectedProvider, expectedDiagnostics, assert }) => { - const { diagnostics, provider } = expectNormalizedProviderFixture({ + expectProviderNormalizationResult({ provider: inputProvider, ...(expectedProvider ? { expectedProvider } : {}), ...(expectedDiagnostics ? { expectedDiagnostics } : {}), + ...(assert ? { assert } : {}), }); - - if (assert) { - assert(provider, diagnostics); - return; - } }, ); diff --git a/src/plugins/provider-wizard.test.ts b/src/plugins/provider-wizard.test.ts index efecb535d33..2dca8296569 100644 --- a/src/plugins/provider-wizard.test.ts +++ b/src/plugins/provider-wizard.test.ts @@ -94,6 +94,32 @@ function createWizardRuntimeParams(params?: { }; } +function expectWizardResolutionCount(params: { + provider: ProviderPlugin; + config?: object; + env?: NodeJS.ProcessEnv; + expectedCount: number; +}) { + setResolvedProviders(params.provider); + resolveProviderWizardOptions( + createWizardRuntimeParams({ + config: params.config, + env: params.env, + }), + ); + resolveProviderWizardOptions( + createWizardRuntimeParams({ + config: params.config, + env: params.env, + }), + ); + expectProviderResolutionCall({ + config: params.config, + env: params.env, + count: params.expectedCount, + }); +} + function expectProviderResolutionCall(params?: { config?: object; env?: NodeJS.ProcessEnv; @@ -112,21 +138,6 @@ function setResolvedProviders(...providers: ProviderPlugin[]) { resolvePluginProviders.mockReturnValue(providers); } -function resolveWizardOptionsTwice(params: { - config?: object; - env: NodeJS.ProcessEnv; - workspaceDir?: string; -}) { - const runtimeParams = createWizardRuntimeParams(params); - resolveProviderWizardOptions(runtimeParams); - resolveProviderWizardOptions(runtimeParams); -} - -function expectWizardProviderCacheMiss(params: { config?: object; env: NodeJS.ProcessEnv }) { - resolveWizardOptionsTwice(params); - expectProviderResolutionCall({ ...params, count: 2 }); -} - function expectSingleWizardChoice(params: { provider: ProviderPlugin; choice: string; @@ -363,11 +374,12 @@ describe("provider wizard boundaries", () => { }), }, ] as const)("$name", ({ env }) => { - const provider = createSglangSetupProvider(); - const config = createSglangConfig(); - setResolvedProviders(provider); - - expectWizardProviderCacheMiss({ config, env }); + expectWizardResolutionCount({ + provider: createSglangSetupProvider(), + config: createSglangConfig(), + env, + expectedCount: 2, + }); }); it("expires provider-wizard memoization after the shortest plugin cache ttl", () => { diff --git a/src/plugins/runtime/gateway-request-scope.test.ts b/src/plugins/runtime/gateway-request-scope.test.ts index 331684db758..295add1382d 100644 --- a/src/plugins/runtime/gateway-request-scope.test.ts +++ b/src/plugins/runtime/gateway-request-scope.test.ts @@ -31,6 +31,17 @@ describe("gateway request scope", () => { expect(runtimeScope.getPluginRuntimeGatewayRequestScope()).toEqual(expected); } + async function expectGatewayScopeWithPluginId(pluginId: string) { + await withTestGatewayScope(async (runtimeScope) => { + await runtimeScope.withPluginRuntimePluginIdScope(pluginId, async () => { + expectGatewayScope(runtimeScope, { + ...TEST_SCOPE, + pluginId, + }); + }); + }); + } + it("reuses AsyncLocalStorage across reloaded module instances", async () => { const first = await importGatewayRequestScopeModule(); @@ -42,13 +53,6 @@ describe("gateway request scope", () => { }); it("attaches plugin id to the active scope", async () => { - await withTestGatewayScope(async (runtimeScope) => { - await runtimeScope.withPluginRuntimePluginIdScope("voice-call", async () => { - expectGatewayScope(runtimeScope, { - ...TEST_SCOPE, - pluginId: "voice-call", - }); - }); - }); + await expectGatewayScopeWithPluginId("voice-call"); }); }); diff --git a/src/plugins/runtime/index.test.ts b/src/plugins/runtime/index.test.ts index f97c17147df..4886bf8bf81 100644 --- a/src/plugins/runtime/index.test.ts +++ b/src/plugins/runtime/index.test.ts @@ -56,6 +56,20 @@ function expectFunctionKeys(value: Record, keys: readonly strin }); } +function expectRunCommandOutcome(params: { + runtime: ReturnType; + expected: "resolve" | "reject"; + commandResult: ReturnType; +}) { + const command = params.runtime.system.runCommandWithTimeout(["echo", "hello"], { + timeoutMs: 1000, + }); + if (params.expected === "resolve") { + return expect(command).resolves.toEqual(params.commandResult); + } + return expect(command).rejects.toThrow("boom"); +} + describe("plugin runtime command execution", () => { beforeEach(() => { vi.restoreAllMocks(); @@ -83,12 +97,7 @@ describe("plugin runtime command execution", () => { } const runtime = createPluginRuntime(); - const command = runtime.system.runCommandWithTimeout(["echo", "hello"], { timeoutMs: 1000 }); - if (expected === "resolve") { - await expect(command).resolves.toEqual(commandResult); - } else { - await expect(command).rejects.toThrow("boom"); - } + await expectRunCommandOutcome({ runtime, expected, commandResult }); expect(runCommandWithTimeoutMock).toHaveBeenCalledWith(["echo", "hello"], { timeoutMs: 1000 }); }); diff --git a/src/plugins/web-search-providers.runtime.test.ts b/src/plugins/web-search-providers.runtime.test.ts index 9cf18e3617e..80ae30974cd 100644 --- a/src/plugins/web-search-providers.runtime.test.ts +++ b/src/plugins/web-search-providers.runtime.test.ts @@ -202,6 +202,47 @@ function expectSnapshotMemoization(params: { expectLoaderCallCount(params.expectedLoaderCalls); } +function expectAutoEnabledWebSearchLoad(params: { + rawConfig: { plugins?: Record }; + expectedAllow: readonly string[]; +}) { + expect(applyPluginAutoEnableSpy).toHaveBeenCalledWith({ + config: params.rawConfig, + env: createWebSearchEnv(), + }); + expect(loadOpenClawPluginsMock).toHaveBeenCalledWith( + expect.objectContaining({ + config: expect.objectContaining({ + plugins: expect.objectContaining({ + allow: expect.arrayContaining([...params.expectedAllow]), + }), + }), + }), + ); +} + +function expectSnapshotLoaderCalls(params: { + config: { plugins?: Record }; + env: NodeJS.ProcessEnv; + mutate: () => void; + expectedLoaderCalls: number; +}) { + resolvePluginWebSearchProviders( + createSnapshotParams({ + config: params.config, + env: params.env, + }), + ); + params.mutate(); + resolvePluginWebSearchProviders( + createSnapshotParams({ + config: params.config, + env: params.env, + }), + ); + expectLoaderCallCount(params.expectedLoaderCalls); +} + describe("resolvePluginWebSearchProviders", () => { beforeAll(async () => { ({ createEmptyPluginRegistry } = await import("./registry.js")); @@ -272,19 +313,10 @@ describe("resolvePluginWebSearchProviders", () => { resolvePluginWebSearchProviders(createSnapshotParams({ config: rawConfig })); - expect(applyPluginAutoEnableSpy).toHaveBeenCalledWith({ - config: rawConfig, - env: createWebSearchEnv(), + expectAutoEnabledWebSearchLoad({ + rawConfig, + expectedAllow: ["brave", "perplexity"], }); - expect(loadOpenClawPluginsMock).toHaveBeenCalledWith( - expect.objectContaining({ - config: expect.objectContaining({ - plugins: expect.objectContaining({ - allow: expect.arrayContaining(["brave", "perplexity"]), - }), - }), - }), - ); }); it("scopes plugin loading to manifest-declared web-search candidates", () => { @@ -301,16 +333,29 @@ describe("resolvePluginWebSearchProviders", () => { }); }); - it("invalidates the snapshot cache when config or env contents change in place", () => { + it.each([ + { + name: "invalidates the snapshot cache when config contents change in place", + mutate: (config: { plugins?: Record }, _env: NodeJS.ProcessEnv) => { + config.plugins = { allow: ["perplexity"] }; + }, + }, + { + name: "invalidates the snapshot cache when env contents change in place", + mutate: (_config: { plugins?: Record }, env: NodeJS.ProcessEnv) => { + env.OPENCLAW_HOME = "/tmp/openclaw-home-b"; + }, + }, + ] as const)("$name", ({ mutate }) => { const config = createBraveAllowConfig(); const env = createWebSearchEnv({ OPENCLAW_HOME: "/tmp/openclaw-home-a" }); - resolvePluginWebSearchProviders(createSnapshotParams({ config, env })); - config.plugins.allow = ["perplexity"]; - env.OPENCLAW_HOME = "/tmp/openclaw-home-b"; - resolvePluginWebSearchProviders(createSnapshotParams({ config, env })); - - expectLoaderCallCount(2); + expectSnapshotLoaderCalls({ + config, + env, + mutate: () => mutate(config, env), + expectedLoaderCalls: 2, + }); }); it.each([ @@ -380,13 +425,14 @@ describe("resolvePluginWebSearchProviders", () => { OPENCLAW_PLUGIN_DISCOVERY_CACHE_MS: "1000", }); - resolvePluginWebSearchProviders(createSnapshotParams({ config, env })); - - env.OPENCLAW_PLUGIN_DISCOVERY_CACHE_MS = "5"; - - resolvePluginWebSearchProviders(createSnapshotParams({ config, env })); - - expectLoaderCallCount(2); + expectSnapshotLoaderCalls({ + config, + env, + mutate: () => { + env.OPENCLAW_PLUGIN_DISCOVERY_CACHE_MS = "5"; + }, + expectedLoaderCalls: 2, + }); }); it("prefers the active plugin registry for runtime resolution", () => {