mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-03 20:40:23 +00:00
fix(plugins): reuse runtime registries for web provider snapshots (#59865)
* fix(plugins): reuse runtime registries for web providers * test(plugins): clarify runtime reuse intent * chore(changelog): note web provider runtime reuse
This commit is contained in:
@@ -442,6 +442,15 @@ export function resolveRuntimePluginRegistry(
|
||||
return getCompatibleActivePluginRegistry(options) ?? loadOpenClawPlugins(options);
|
||||
}
|
||||
|
||||
export function resolveCompatibleRuntimePluginRegistry(
|
||||
options?: PluginLoadOptions,
|
||||
): PluginRegistry | undefined {
|
||||
// Check whether the active runtime registry is already compatible with these
|
||||
// load options. Unlike resolveRuntimePluginRegistry, this never triggers a
|
||||
// fresh plugin load on cache miss.
|
||||
return getCompatibleActivePluginRegistry(options);
|
||||
}
|
||||
|
||||
function validatePluginConfig(params: {
|
||||
schema?: Record<string, unknown>;
|
||||
cacheKey?: string;
|
||||
|
||||
171
src/plugins/web-fetch-providers.runtime.test.ts
Normal file
171
src/plugins/web-fetch-providers.runtime.test.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createEmptyPluginRegistry } from "./registry.js";
|
||||
|
||||
type LoaderModule = typeof import("./loader.js");
|
||||
type ManifestRegistryModule = typeof import("./manifest-registry.js");
|
||||
type RuntimeModule = typeof import("./runtime.js");
|
||||
type WebFetchProvidersRuntimeModule = typeof import("./web-fetch-providers.runtime.js");
|
||||
type WebFetchProvidersSharedModule = typeof import("./web-fetch-providers.shared.js");
|
||||
|
||||
let loaderModule: LoaderModule;
|
||||
let manifestRegistryModule: ManifestRegistryModule;
|
||||
let webFetchProvidersSharedModule: WebFetchProvidersSharedModule;
|
||||
let loadOpenClawPluginsMock: ReturnType<typeof vi.fn>;
|
||||
let setActivePluginRegistry: RuntimeModule["setActivePluginRegistry"];
|
||||
let resolvePluginWebFetchProviders: WebFetchProvidersRuntimeModule["resolvePluginWebFetchProviders"];
|
||||
let resetWebFetchProviderSnapshotCacheForTests: WebFetchProvidersRuntimeModule["__testing"]["resetWebFetchProviderSnapshotCacheForTests"];
|
||||
|
||||
const DEFAULT_WORKSPACE = "/tmp/workspace";
|
||||
|
||||
function createWebFetchEnv(overrides?: Partial<NodeJS.ProcessEnv>) {
|
||||
return {
|
||||
OPENCLAW_HOME: "/tmp/openclaw-home",
|
||||
...overrides,
|
||||
} as NodeJS.ProcessEnv;
|
||||
}
|
||||
|
||||
function createFirecrawlAllowConfig() {
|
||||
return {
|
||||
plugins: {
|
||||
allow: ["firecrawl"],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createManifestRegistryFixture() {
|
||||
return {
|
||||
plugins: [
|
||||
{
|
||||
id: "firecrawl",
|
||||
origin: "bundled",
|
||||
rootDir: "/tmp/firecrawl",
|
||||
source: "/tmp/firecrawl/index.js",
|
||||
manifestPath: "/tmp/firecrawl/openclaw.plugin.json",
|
||||
channels: [],
|
||||
providers: [],
|
||||
skills: [],
|
||||
hooks: [],
|
||||
configUiHints: { "webFetch.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 createRuntimeWebFetchProvider() {
|
||||
return {
|
||||
pluginId: "firecrawl",
|
||||
pluginName: "Firecrawl",
|
||||
source: "test" as const,
|
||||
provider: {
|
||||
id: "firecrawl",
|
||||
label: "Firecrawl",
|
||||
hint: "Firecrawl runtime provider",
|
||||
envVars: ["FIRECRAWL_API_KEY"],
|
||||
placeholder: "firecrawl-...",
|
||||
signupUrl: "https://example.com/firecrawl",
|
||||
credentialPath: "plugins.entries.firecrawl.config.webFetch.apiKey",
|
||||
getCredentialValue: () => "configured",
|
||||
setCredentialValue: () => {},
|
||||
createTool: () => ({
|
||||
description: "firecrawl",
|
||||
parameters: {},
|
||||
execute: async () => ({}),
|
||||
}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("resolvePluginWebFetchProviders", () => {
|
||||
beforeAll(async () => {
|
||||
loaderModule = await import("./loader.js");
|
||||
manifestRegistryModule = await import("./manifest-registry.js");
|
||||
webFetchProvidersSharedModule = await import("./web-fetch-providers.shared.js");
|
||||
({ setActivePluginRegistry } = await import("./runtime.js"));
|
||||
({
|
||||
resolvePluginWebFetchProviders,
|
||||
__testing: { resetWebFetchProviderSnapshotCacheForTests },
|
||||
} = await import("./web-fetch-providers.runtime.js"));
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
resetWebFetchProviderSnapshotCacheForTests();
|
||||
vi.spyOn(manifestRegistryModule, "loadPluginManifestRegistry").mockReturnValue(
|
||||
createManifestRegistryFixture() as ManifestRegistryModule["loadPluginManifestRegistry"] extends (
|
||||
...args: unknown[]
|
||||
) => infer R
|
||||
? R
|
||||
: never,
|
||||
);
|
||||
loadOpenClawPluginsMock = vi
|
||||
.spyOn(loaderModule, "loadOpenClawPlugins")
|
||||
.mockImplementation(() => {
|
||||
const registry = createEmptyPluginRegistry();
|
||||
registry.webFetchProviders = [createRuntimeWebFetchProvider()];
|
||||
return registry;
|
||||
});
|
||||
setActivePluginRegistry(createEmptyPluginRegistry());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
setActivePluginRegistry(createEmptyPluginRegistry());
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("falls back to the plugin loader when no compatible active registry exists", () => {
|
||||
const providers = resolvePluginWebFetchProviders({});
|
||||
|
||||
expect(providers.map((provider) => `${provider.pluginId}:${provider.id}`)).toEqual([
|
||||
"firecrawl:firecrawl",
|
||||
]);
|
||||
expect(loadOpenClawPluginsMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("reuses a compatible active registry for snapshot resolution when config is provided", () => {
|
||||
const env = createWebFetchEnv();
|
||||
const rawConfig = createFirecrawlAllowConfig();
|
||||
const { config, activationSourceConfig, autoEnabledReasons } =
|
||||
webFetchProvidersSharedModule.resolveBundledWebFetchResolutionConfig({
|
||||
config: rawConfig,
|
||||
bundledAllowlistCompat: true,
|
||||
env,
|
||||
});
|
||||
const { cacheKey } = loaderModule.__testing.resolvePluginLoadCacheContext({
|
||||
config,
|
||||
activationSourceConfig,
|
||||
autoEnabledReasons,
|
||||
workspaceDir: DEFAULT_WORKSPACE,
|
||||
env,
|
||||
onlyPluginIds: ["firecrawl"],
|
||||
cache: false,
|
||||
activate: false,
|
||||
});
|
||||
const registry = createEmptyPluginRegistry();
|
||||
registry.webFetchProviders.push(createRuntimeWebFetchProvider());
|
||||
setActivePluginRegistry(registry, cacheKey);
|
||||
|
||||
const providers = resolvePluginWebFetchProviders({
|
||||
config: rawConfig,
|
||||
bundledAllowlistCompat: true,
|
||||
workspaceDir: DEFAULT_WORKSPACE,
|
||||
env,
|
||||
});
|
||||
|
||||
expect(providers.map((provider) => `${provider.pluginId}:${provider.id}`)).toEqual([
|
||||
"firecrawl:firecrawl",
|
||||
]);
|
||||
expect(loadOpenClawPluginsMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -6,7 +6,11 @@ import {
|
||||
resolvePluginSnapshotCacheTtlMs,
|
||||
shouldUsePluginSnapshotCache,
|
||||
} from "./cache-controls.js";
|
||||
import { loadOpenClawPlugins, resolveRuntimePluginRegistry } from "./loader.js";
|
||||
import {
|
||||
loadOpenClawPlugins,
|
||||
resolveCompatibleRuntimePluginRegistry,
|
||||
resolveRuntimePluginRegistry,
|
||||
} from "./loader.js";
|
||||
import type { PluginLoadOptions } from "./loader.js";
|
||||
import { createPluginLoaderLogger } from "./logger.js";
|
||||
import { loadPluginManifestRegistry, type PluginManifestRecord } from "./manifest-registry.js";
|
||||
@@ -104,10 +108,11 @@ function resolveWebFetchLoadOptions(params: {
|
||||
cache?: boolean;
|
||||
}) {
|
||||
const env = params.env ?? process.env;
|
||||
const { config } = resolveBundledWebFetchResolutionConfig({
|
||||
...params,
|
||||
env,
|
||||
});
|
||||
const { config, activationSourceConfig, autoEnabledReasons } =
|
||||
resolveBundledWebFetchResolutionConfig({
|
||||
...params,
|
||||
env,
|
||||
});
|
||||
const onlyPluginIds = resolveWebFetchCandidatePluginIds({
|
||||
config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
@@ -117,6 +122,8 @@ function resolveWebFetchLoadOptions(params: {
|
||||
return {
|
||||
env,
|
||||
config,
|
||||
activationSourceConfig,
|
||||
autoEnabledReasons,
|
||||
workspaceDir: params.workspaceDir,
|
||||
cache: params.cache ?? false,
|
||||
activate: params.activate ?? false,
|
||||
@@ -170,8 +177,11 @@ export function resolvePluginWebFetchProviders(params: {
|
||||
}
|
||||
}
|
||||
const loadOptions = resolveWebFetchLoadOptions(params);
|
||||
// Keep repeated runtime reads on the already-compatible active registry when
|
||||
// possible, then fall back to a fresh snapshot load only when necessary.
|
||||
const resolved = mapRegistryWebFetchProviders({
|
||||
registry: loadOpenClawPlugins(loadOptions),
|
||||
registry:
|
||||
resolveCompatibleRuntimePluginRegistry(loadOptions) ?? loadOpenClawPlugins(loadOptions),
|
||||
});
|
||||
if (cacheOwnerConfig && shouldMemoizeSnapshot) {
|
||||
const ttlMs = resolvePluginSnapshotCacheTtlMs(env);
|
||||
|
||||
@@ -55,14 +55,17 @@ export function resolveBundledWebFetchResolutionConfig(params: {
|
||||
}): {
|
||||
config: PluginLoadOptions["config"];
|
||||
normalized: NormalizedPluginsConfig;
|
||||
activationSourceConfig?: PluginLoadOptions["config"];
|
||||
autoEnabledReasons: Record<string, string[]>;
|
||||
} {
|
||||
const autoEnabledConfig =
|
||||
const autoEnabled =
|
||||
params.config !== undefined
|
||||
? applyPluginAutoEnable({
|
||||
config: params.config,
|
||||
env: params.env ?? process.env,
|
||||
}).config
|
||||
})
|
||||
: undefined;
|
||||
const autoEnabledConfig = autoEnabled?.config;
|
||||
const bundledCompatPluginIds = resolveBundledWebFetchCompatPluginIds({
|
||||
config: autoEnabledConfig,
|
||||
workspaceDir: params.workspaceDir,
|
||||
@@ -87,5 +90,7 @@ export function resolveBundledWebFetchResolutionConfig(params: {
|
||||
return {
|
||||
config,
|
||||
normalized: normalizePluginsConfig(config?.plugins),
|
||||
activationSourceConfig: params.config,
|
||||
autoEnabledReasons: autoEnabled?.autoEnabledReasons ?? {},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -380,6 +380,51 @@ describe("resolvePluginWebSearchProviders", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("reuses a compatible active registry for snapshot resolution when config is provided", () => {
|
||||
const env = createWebSearchEnv();
|
||||
const rawConfig = createBraveAllowConfig();
|
||||
const { config, activationSourceConfig, autoEnabledReasons } =
|
||||
webSearchProvidersSharedModule.resolveBundledWebSearchResolutionConfig({
|
||||
config: rawConfig,
|
||||
bundledAllowlistCompat: true,
|
||||
env,
|
||||
});
|
||||
const { cacheKey } = loaderModule.__testing.resolvePluginLoadCacheContext({
|
||||
config,
|
||||
activationSourceConfig,
|
||||
autoEnabledReasons,
|
||||
workspaceDir: DEFAULT_WEB_SEARCH_WORKSPACE,
|
||||
env,
|
||||
onlyPluginIds: ["brave"],
|
||||
cache: false,
|
||||
activate: false,
|
||||
});
|
||||
const registry = createEmptyPluginRegistry();
|
||||
registry.webSearchProviders.push(
|
||||
createRuntimeWebSearchProvider({
|
||||
pluginId: "brave",
|
||||
pluginName: "Brave",
|
||||
id: "brave",
|
||||
label: "Brave Search",
|
||||
hint: "Brave runtime provider",
|
||||
envVar: "BRAVE_API_KEY",
|
||||
signupUrl: "https://example.com/brave",
|
||||
credentialPath: "plugins.entries.brave.config.webSearch.apiKey",
|
||||
}),
|
||||
);
|
||||
setActivePluginRegistry(registry, cacheKey);
|
||||
|
||||
const providers = resolvePluginWebSearchProviders({
|
||||
config: rawConfig,
|
||||
bundledAllowlistCompat: true,
|
||||
workspaceDir: DEFAULT_WEB_SEARCH_WORKSPACE,
|
||||
env,
|
||||
});
|
||||
|
||||
expectRuntimeProviderResolution(providers, ["brave:brave"]);
|
||||
expect(loadOpenClawPluginsMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "invalidates the snapshot cache when config contents change in place",
|
||||
|
||||
@@ -6,7 +6,11 @@ import {
|
||||
resolvePluginSnapshotCacheTtlMs,
|
||||
shouldUsePluginSnapshotCache,
|
||||
} from "./cache-controls.js";
|
||||
import { loadOpenClawPlugins, resolveRuntimePluginRegistry } from "./loader.js";
|
||||
import {
|
||||
loadOpenClawPlugins,
|
||||
resolveCompatibleRuntimePluginRegistry,
|
||||
resolveRuntimePluginRegistry,
|
||||
} from "./loader.js";
|
||||
import type { PluginLoadOptions } from "./loader.js";
|
||||
import { createPluginLoaderLogger } from "./logger.js";
|
||||
import { loadPluginManifestRegistry, type PluginManifestRecord } from "./manifest-registry.js";
|
||||
@@ -172,8 +176,11 @@ export function resolvePluginWebSearchProviders(params: {
|
||||
}
|
||||
}
|
||||
const loadOptions = resolveWebSearchLoadOptions(params);
|
||||
// Prefer the compatible active registry so repeated runtime reads do not
|
||||
// re-import the same plugin set through the snapshot path.
|
||||
const resolved = mapRegistryWebSearchProviders({
|
||||
registry: loadOpenClawPlugins(loadOptions),
|
||||
registry:
|
||||
resolveCompatibleRuntimePluginRegistry(loadOptions) ?? loadOpenClawPlugins(loadOptions),
|
||||
});
|
||||
if (cacheOwnerConfig && shouldMemoizeSnapshot) {
|
||||
const ttlMs = resolvePluginSnapshotCacheTtlMs(env);
|
||||
|
||||
Reference in New Issue
Block a user