diff --git a/src/gateway/gateway-misc.test.ts b/src/gateway/gateway-misc.test.ts index 954aa65c623..1b10906790f 100644 --- a/src/gateway/gateway-misc.test.ts +++ b/src/gateway/gateway-misc.test.ts @@ -2,7 +2,7 @@ import * as fs from "node:fs/promises"; import type { IncomingMessage, ServerResponse } from "node:http"; import * as os from "node:os"; import * as path from "node:path"; -import { beforeEach, describe, expect, it, test, vi } from "vitest"; +import { beforeAll, beforeEach, describe, expect, it, test, vi } from "vitest"; import { defaultVoiceWakeTriggers } from "../infra/voicewake.js"; import { handleControlUiHttpRequest } from "./control-ui.js"; import { @@ -46,28 +46,15 @@ vi.mock("ws", () => ({ let GatewayClient: typeof import("./client.js").GatewayClient; -async function loadFreshGatewayClientModuleForTest() { - vi.resetModules(); - vi.doMock("ws", () => ({ - WebSocket: class MockWebSocket { - on = vi.fn(); - close = vi.fn(); - send = vi.fn(); - - constructor(url: unknown, opts: unknown) { - wsMockState.last = { url, opts }; - } - }, - })); - ({ GatewayClient } = await import("./client.js")); -} - -beforeEach(async () => { - wsMockState.last = null; - await loadFreshGatewayClientModuleForTest(); -}); - describe("GatewayClient", () => { + beforeAll(async () => { + ({ GatewayClient } = await import("./client.js")); + }); + + beforeEach(() => { + wsMockState.last = null; + }); + async function withControlUiRoot( params: { faviconSvg?: string; indexHtml?: string }, run: (tmp: string) => Promise, diff --git a/src/plugins/activation-context.ts b/src/plugins/activation-context.ts index 9f46233b815..252a5b158d3 100644 --- a/src/plugins/activation-context.ts +++ b/src/plugins/activation-context.ts @@ -47,6 +47,11 @@ export type BundledPluginCompatibleActivationInputs = PluginActivationInputs & { compatPluginIds: string[]; }; +export type BundledPluginCompatibleLoadValues = Pick< + BundledPluginCompatibleActivationInputs, + "rawConfig" | "config" | "activationSourceConfig" | "autoEnabledReasons" | "compatPluginIds" +>; + export function withActivatedPluginIds(params: { config?: OpenClawConfig; pluginIds: readonly string[]; @@ -235,3 +240,70 @@ export function resolveBundledPluginCompatibleActivationInputs(params: { compatPluginIds, }; } + +export function resolveBundledPluginCompatibleLoadValues(params: { + rawConfig?: OpenClawConfig; + resolvedConfig?: OpenClawConfig; + autoEnabledReasons?: Record; + env?: NodeJS.ProcessEnv; + workspaceDir?: string; + onlyPluginIds?: readonly string[]; + applyAutoEnable?: boolean; + compatMode: PluginActivationBundledCompatMode; + resolveCompatPluginIds: (params: { + config?: OpenClawConfig; + workspaceDir?: string; + env?: NodeJS.ProcessEnv; + onlyPluginIds?: readonly string[]; + }) => string[]; +}): BundledPluginCompatibleLoadValues { + const env = params.env ?? process.env; + const rawConfig = params.rawConfig ?? params.resolvedConfig; + let resolvedConfig = params.resolvedConfig ?? params.rawConfig; + let autoEnabledReasons = params.autoEnabledReasons ?? {}; + + if (params.applyAutoEnable && rawConfig !== undefined) { + const autoEnabled = applyPluginAutoEnable({ + config: rawConfig, + env, + }); + resolvedConfig = autoEnabled.config; + autoEnabledReasons = autoEnabled.autoEnabledReasons; + } + + const allowlistCompatEnabled = params.compatMode.allowlist === true; + const shouldResolveCompatPluginIds = + allowlistCompatEnabled || + params.compatMode.enablement === "always" || + (params.compatMode.enablement === "allowlist" && allowlistCompatEnabled) || + params.compatMode.vitest === true; + const compatPluginIds = shouldResolveCompatPluginIds + ? params.resolveCompatPluginIds({ + config: resolvedConfig, + workspaceDir: params.workspaceDir, + env, + onlyPluginIds: params.onlyPluginIds, + }) + : []; + const config = applyPluginCompatibilityOverrides({ + config: resolvedConfig, + compat: { + allowlistPluginIds: allowlistCompatEnabled ? compatPluginIds : undefined, + enablementPluginIds: + params.compatMode.enablement === "always" || + (params.compatMode.enablement === "allowlist" && allowlistCompatEnabled) + ? compatPluginIds + : undefined, + vitestPluginIds: params.compatMode.vitest ? compatPluginIds : undefined, + }, + env, + }); + + return { + rawConfig, + config, + activationSourceConfig: rawConfig, + autoEnabledReasons, + compatPluginIds, + }; +} diff --git a/src/plugins/loader.test.ts b/src/plugins/loader.test.ts index 8361bd4dc59..d6e761934d5 100644 --- a/src/plugins/loader.test.ts +++ b/src/plugins/loader.test.ts @@ -1679,8 +1679,6 @@ module.exports = { id: "throws-after-import", register() {} };`, }); it("can scope bundled provider loads to deepseek without hanging", () => { - resetPluginLoaderTestStateForTest(); - const scoped = loadOpenClawPlugins({ cache: false, activate: false, diff --git a/src/plugins/web-fetch-providers.shared.ts b/src/plugins/web-fetch-providers.shared.ts index 7becab78236..ed860dadc33 100644 --- a/src/plugins/web-fetch-providers.shared.ts +++ b/src/plugins/web-fetch-providers.shared.ts @@ -1,4 +1,3 @@ -import { type NormalizedPluginsConfig } from "./config-state.js"; import type { PluginLoadOptions } from "./loader.js"; import type { PluginWebFetchProviderEntry } from "./types.js"; import { @@ -26,7 +25,6 @@ export function resolveBundledWebFetchResolutionConfig(params: { bundledAllowlistCompat?: boolean; }): { config: PluginLoadOptions["config"]; - normalized: NormalizedPluginsConfig; activationSourceConfig?: PluginLoadOptions["config"]; autoEnabledReasons: Record; } { diff --git a/src/plugins/web-provider-resolution-candidates.test.ts b/src/plugins/web-provider-resolution-candidates.test.ts index f7d38c737a5..cff5516bd1b 100644 --- a/src/plugins/web-provider-resolution-candidates.test.ts +++ b/src/plugins/web-provider-resolution-candidates.test.ts @@ -2,13 +2,11 @@ import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const mocks = vi.hoisted(() => ({ loadPluginManifestRegistry: vi.fn(), - resolveManifestContractPluginIds: vi.fn(), })); vi.mock("./manifest-registry.js", () => ({ loadPluginManifestRegistry: (...args: unknown[]) => mocks.loadPluginManifestRegistry(...args), - resolveManifestContractPluginIds: (...args: unknown[]) => - mocks.resolveManifestContractPluginIds(...args), + resolveManifestContractPluginIds: vi.fn(), })); let resolveManifestDeclaredWebProviderCandidatePluginIds: typeof import("./web-provider-resolution-shared.js").resolveManifestDeclaredWebProviderCandidatePluginIds; @@ -20,8 +18,6 @@ describe("resolveManifestDeclaredWebProviderCandidatePluginIds", () => { }); beforeEach(() => { - mocks.resolveManifestContractPluginIds.mockReset(); - mocks.resolveManifestContractPluginIds.mockReturnValue(["alpha"]); mocks.loadPluginManifestRegistry.mockReset(); mocks.loadPluginManifestRegistry.mockReturnValue({ plugins: [ @@ -65,4 +61,14 @@ describe("resolveManifestDeclaredWebProviderCandidatePluginIds", () => { }), ).toBeUndefined(); }); + + it("derives provider candidates from a single manifest-registry read", () => { + expect( + resolveManifestDeclaredWebProviderCandidatePluginIds({ + contract: "webSearchProviders", + configKey: "webSearch", + }), + ).toEqual(["alpha", "beta"]); + expect(mocks.loadPluginManifestRegistry).toHaveBeenCalledTimes(1); + }); }); diff --git a/src/plugins/web-provider-resolution-shared.ts b/src/plugins/web-provider-resolution-shared.ts index f295a79e2fc..f1643eb3531 100644 --- a/src/plugins/web-provider-resolution-shared.ts +++ b/src/plugins/web-provider-resolution-shared.ts @@ -1,5 +1,4 @@ -import { resolveBundledPluginCompatibleActivationInputs } from "./activation-context.js"; -import type { NormalizedPluginsConfig } from "./config-state.js"; +import { resolveBundledPluginCompatibleLoadValues } from "./activation-context.js"; import type { PluginLoadOptions } from "./loader.js"; import { loadPluginManifestRegistry, @@ -72,16 +71,6 @@ export function resolveManifestDeclaredWebProviderCandidatePluginIds(params: { onlyPluginIds?: readonly string[]; origin?: PluginManifestRecord["origin"]; }): string[] | undefined { - const contractIds = new Set( - resolveManifestContractPluginIds({ - contract: params.contract, - origin: params.origin, - config: params.config, - workspaceDir: params.workspaceDir, - env: params.env, - onlyPluginIds: params.onlyPluginIds, - }), - ); const scopedPluginIds = normalizePluginIdScope(params.onlyPluginIds); const onlyPluginIdSet = createPluginIdScopeSet(scopedPluginIds); const ids = loadPluginManifestRegistry({ @@ -93,8 +82,7 @@ export function resolveManifestDeclaredWebProviderCandidatePluginIds(params: { (plugin) => (!params.origin || plugin.origin === params.origin) && (!onlyPluginIdSet || onlyPluginIdSet.has(plugin.id)) && - (contractIds.has(plugin.id) || - pluginManifestDeclaresProviderConfig(plugin, params.configKey, params.contract)), + pluginManifestDeclaresProviderConfig(plugin, params.configKey, params.contract), ) .map((plugin) => plugin.id) .toSorted((left, right) => left.localeCompare(right)); @@ -127,11 +115,10 @@ export function resolveBundledWebProviderResolutionConfig(params: { bundledAllowlistCompat?: boolean; }): { config: PluginLoadOptions["config"]; - normalized: NormalizedPluginsConfig; activationSourceConfig?: PluginLoadOptions["config"]; autoEnabledReasons: Record; } { - const activation = resolveBundledPluginCompatibleActivationInputs({ + const activation = resolveBundledPluginCompatibleLoadValues({ rawConfig: params.config, env: params.env, workspaceDir: params.workspaceDir, @@ -150,7 +137,6 @@ export function resolveBundledWebProviderResolutionConfig(params: { return { config: activation.config, - normalized: activation.normalized, activationSourceConfig: activation.activationSourceConfig, autoEnabledReasons: activation.autoEnabledReasons, }; diff --git a/src/plugins/web-search-providers.shared.ts b/src/plugins/web-search-providers.shared.ts index 366fa8e24d1..fa836792eab 100644 --- a/src/plugins/web-search-providers.shared.ts +++ b/src/plugins/web-search-providers.shared.ts @@ -1,4 +1,3 @@ -import { type NormalizedPluginsConfig } from "./config-state.js"; import type { PluginLoadOptions } from "./loader.js"; import type { PluginWebSearchProviderEntry } from "./types.js"; import { @@ -26,7 +25,6 @@ export function resolveBundledWebSearchResolutionConfig(params: { bundledAllowlistCompat?: boolean; }): { config: PluginLoadOptions["config"]; - normalized: NormalizedPluginsConfig; activationSourceConfig?: PluginLoadOptions["config"]; autoEnabledReasons: Record; } {