mirror of https://github.com/openclaw/openclaw.git
Plugins: add runtime registry compatibility helper
This commit is contained in:
parent
4beb231fd8
commit
fd0aac297c
|
|
@ -2,28 +2,25 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
|||
|
||||
const hoisted = vi.hoisted(() => ({
|
||||
loadOpenClawPlugins: vi.fn(),
|
||||
getActivePluginRegistryKey: vi.fn<() => string | null>(),
|
||||
getCompatibleActivePluginRegistry: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/loader.js", () => ({
|
||||
loadOpenClawPlugins: hoisted.loadOpenClawPlugins,
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/runtime.js", () => ({
|
||||
getActivePluginRegistryKey: hoisted.getActivePluginRegistryKey,
|
||||
getCompatibleActivePluginRegistry: hoisted.getCompatibleActivePluginRegistry,
|
||||
}));
|
||||
|
||||
describe("ensureRuntimePluginsLoaded", () => {
|
||||
beforeEach(() => {
|
||||
hoisted.loadOpenClawPlugins.mockReset();
|
||||
hoisted.getActivePluginRegistryKey.mockReset();
|
||||
hoisted.getActivePluginRegistryKey.mockReturnValue(null);
|
||||
hoisted.getCompatibleActivePluginRegistry.mockReset();
|
||||
hoisted.getCompatibleActivePluginRegistry.mockReturnValue(undefined);
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
it("does not reactivate plugins when a process already has an active registry", async () => {
|
||||
const { ensureRuntimePluginsLoaded } = await import("./runtime-plugins.js");
|
||||
hoisted.getActivePluginRegistryKey.mockReturnValue("gateway-registry");
|
||||
hoisted.getCompatibleActivePluginRegistry.mockReturnValue({});
|
||||
|
||||
ensureRuntimePluginsLoaded({
|
||||
config: {} as never,
|
||||
|
|
@ -34,7 +31,7 @@ describe("ensureRuntimePluginsLoaded", () => {
|
|||
expect(hoisted.loadOpenClawPlugins).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("loads runtime plugins when no active registry exists", async () => {
|
||||
it("loads runtime plugins when no compatible active registry exists", async () => {
|
||||
const { ensureRuntimePluginsLoaded } = await import("./runtime-plugins.js");
|
||||
|
||||
ensureRuntimePluginsLoaded({
|
||||
|
|
@ -51,4 +48,23 @@ describe("ensureRuntimePluginsLoaded", () => {
|
|||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("reloads when the current active registry is incompatible with the request", async () => {
|
||||
const { ensureRuntimePluginsLoaded } = await import("./runtime-plugins.js");
|
||||
|
||||
ensureRuntimePluginsLoaded({
|
||||
config: {} as never,
|
||||
workspaceDir: "/tmp/workspace",
|
||||
allowGatewaySubagentBinding: true,
|
||||
});
|
||||
|
||||
expect(hoisted.getCompatibleActivePluginRegistry).toHaveBeenCalledWith({
|
||||
config: {} as never,
|
||||
workspaceDir: "/tmp/workspace",
|
||||
runtimeOptions: {
|
||||
allowGatewaySubagentBinding: true,
|
||||
},
|
||||
});
|
||||
expect(hoisted.loadOpenClawPlugins).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { loadOpenClawPlugins } from "../plugins/loader.js";
|
||||
import { getActivePluginRegistryKey } from "../plugins/runtime.js";
|
||||
import { getCompatibleActivePluginRegistry, loadOpenClawPlugins } from "../plugins/loader.js";
|
||||
import { resolveUserPath } from "../utils.js";
|
||||
|
||||
export function ensureRuntimePluginsLoaded(params: {
|
||||
|
|
@ -8,16 +7,11 @@ export function ensureRuntimePluginsLoaded(params: {
|
|||
workspaceDir?: string | null;
|
||||
allowGatewaySubagentBinding?: boolean;
|
||||
}): void {
|
||||
if (getActivePluginRegistryKey()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const workspaceDir =
|
||||
typeof params.workspaceDir === "string" && params.workspaceDir.trim()
|
||||
? resolveUserPath(params.workspaceDir)
|
||||
: undefined;
|
||||
|
||||
loadOpenClawPlugins({
|
||||
const loadOptions = {
|
||||
config: params.config,
|
||||
workspaceDir,
|
||||
runtimeOptions: params.allowGatewaySubagentBinding
|
||||
|
|
@ -25,5 +19,9 @@ export function ensureRuntimePluginsLoaded(params: {
|
|||
allowGatewaySubagentBinding: true,
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
};
|
||||
if (getCompatibleActivePluginRegistry(loadOptions)) {
|
||||
return;
|
||||
}
|
||||
loadOpenClawPlugins(loadOptions);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,13 @@ import { clearPluginCommands, getPluginCommandSpecs } from "./command-registry-s
|
|||
import { clearPluginDiscoveryCache } from "./discovery.js";
|
||||
import { getGlobalHookRunner, resetGlobalHookRunner } from "./hook-runner-global.js";
|
||||
import { createHookRunner } from "./hooks.js";
|
||||
import { __testing, clearPluginLoaderCache, loadOpenClawPlugins } from "./loader.js";
|
||||
import {
|
||||
__testing,
|
||||
clearPluginLoaderCache,
|
||||
getCompatibleActivePluginRegistry,
|
||||
loadOpenClawPlugins,
|
||||
resolvePluginLoadCacheContext,
|
||||
} from "./loader.js";
|
||||
import { clearPluginManifestRegistryCache } from "./manifest-registry.js";
|
||||
import {
|
||||
getMemoryEmbeddingProvider,
|
||||
|
|
@ -27,6 +33,7 @@ import { createEmptyPluginRegistry } from "./registry.js";
|
|||
import {
|
||||
getActivePluginRegistry,
|
||||
getActivePluginRegistryKey,
|
||||
resetPluginRuntimeStateForTest,
|
||||
setActivePluginRegistry,
|
||||
} from "./runtime.js";
|
||||
|
||||
|
|
@ -763,6 +770,7 @@ afterEach(() => {
|
|||
clearPluginLoaderCache();
|
||||
clearPluginDiscoveryCache();
|
||||
clearPluginManifestRegistryCache();
|
||||
resetPluginRuntimeStateForTest();
|
||||
resetDiagnosticEventsForTest();
|
||||
if (prevBundledDir === undefined) {
|
||||
delete process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
|
||||
|
|
@ -3539,6 +3547,53 @@ export const runtimeValue = helperValue;`,
|
|||
});
|
||||
});
|
||||
|
||||
describe("getCompatibleActivePluginRegistry", () => {
|
||||
it("reuses the active registry only when the load context cache key matches", () => {
|
||||
const registry = createEmptyPluginRegistry();
|
||||
const loadOptions = {
|
||||
config: {
|
||||
plugins: {
|
||||
allow: ["demo"],
|
||||
load: { paths: ["/tmp/demo.js"] },
|
||||
},
|
||||
},
|
||||
workspaceDir: "/tmp/workspace-a",
|
||||
runtimeOptions: {
|
||||
allowGatewaySubagentBinding: true,
|
||||
},
|
||||
};
|
||||
const { cacheKey } = resolvePluginLoadCacheContext(loadOptions);
|
||||
setActivePluginRegistry(registry, cacheKey);
|
||||
|
||||
expect(getCompatibleActivePluginRegistry(loadOptions)).toBe(registry);
|
||||
expect(
|
||||
getCompatibleActivePluginRegistry({
|
||||
...loadOptions,
|
||||
workspaceDir: "/tmp/workspace-b",
|
||||
}),
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
getCompatibleActivePluginRegistry({
|
||||
...loadOptions,
|
||||
onlyPluginIds: ["demo"],
|
||||
}),
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
getCompatibleActivePluginRegistry({
|
||||
...loadOptions,
|
||||
runtimeOptions: undefined,
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("falls back to the current active runtime when no compatibility-shaping inputs are supplied", () => {
|
||||
const registry = createEmptyPluginRegistry();
|
||||
setActivePluginRegistry(registry, "startup-registry");
|
||||
|
||||
expect(getCompatibleActivePluginRegistry()).toBe(registry);
|
||||
});
|
||||
});
|
||||
|
||||
describe("clearPluginLoaderCache", () => {
|
||||
it("resets registered memory plugin registries", () => {
|
||||
registerMemoryEmbeddingProvider({
|
||||
|
|
|
|||
|
|
@ -37,7 +37,11 @@ import {
|
|||
import { isPathInside, safeStatSync } from "./path-safety.js";
|
||||
import { createPluginRegistry, type PluginRecord, type PluginRegistry } from "./registry.js";
|
||||
import { resolvePluginCacheInputs } from "./roots.js";
|
||||
import { setActivePluginRegistry } from "./runtime.js";
|
||||
import {
|
||||
getActivePluginRegistry,
|
||||
getActivePluginRegistryKey,
|
||||
setActivePluginRegistry,
|
||||
} from "./runtime.js";
|
||||
import type { CreatePluginRuntimeOptions } from "./runtime/index.js";
|
||||
import type { PluginRuntime } from "./runtime/types.js";
|
||||
import { validateJsonSchemaValue } from "./schema-validator.js";
|
||||
|
|
@ -239,6 +243,80 @@ function normalizeScopedPluginIds(ids?: string[]): string[] | undefined {
|
|||
return normalized.length > 0 ? normalized : undefined;
|
||||
}
|
||||
|
||||
function resolveRuntimeSubagentMode(
|
||||
runtimeOptions: PluginLoadOptions["runtimeOptions"],
|
||||
): "default" | "explicit" | "gateway-bindable" {
|
||||
if (runtimeOptions?.allowGatewaySubagentBinding === true) {
|
||||
return "gateway-bindable";
|
||||
}
|
||||
if (runtimeOptions?.subagent) {
|
||||
return "explicit";
|
||||
}
|
||||
return "default";
|
||||
}
|
||||
|
||||
function hasExplicitCompatibilityInputs(options: PluginLoadOptions): boolean {
|
||||
return Boolean(
|
||||
options.config !== undefined ||
|
||||
options.workspaceDir !== undefined ||
|
||||
options.env !== undefined ||
|
||||
options.onlyPluginIds?.length ||
|
||||
options.runtimeOptions !== undefined ||
|
||||
options.pluginSdkResolution !== undefined ||
|
||||
options.includeSetupOnlyChannelPlugins === true ||
|
||||
options.preferSetupRuntimeForChannelPlugins === true,
|
||||
);
|
||||
}
|
||||
|
||||
export function resolvePluginLoadCacheContext(options: PluginLoadOptions = {}) {
|
||||
const env = options.env ?? process.env;
|
||||
const cfg = applyTestPluginDefaults(options.config ?? {}, env);
|
||||
const normalized = normalizePluginsConfig(cfg.plugins);
|
||||
const onlyPluginIds = normalizeScopedPluginIds(options.onlyPluginIds);
|
||||
const includeSetupOnlyChannelPlugins = options.includeSetupOnlyChannelPlugins === true;
|
||||
const preferSetupRuntimeForChannelPlugins = options.preferSetupRuntimeForChannelPlugins === true;
|
||||
const cacheKey = buildCacheKey({
|
||||
workspaceDir: options.workspaceDir,
|
||||
plugins: normalized,
|
||||
installs: cfg.plugins?.installs,
|
||||
env,
|
||||
onlyPluginIds,
|
||||
includeSetupOnlyChannelPlugins,
|
||||
preferSetupRuntimeForChannelPlugins,
|
||||
runtimeSubagentMode: resolveRuntimeSubagentMode(options.runtimeOptions),
|
||||
pluginSdkResolution: options.pluginSdkResolution,
|
||||
});
|
||||
return {
|
||||
env,
|
||||
cfg,
|
||||
normalized,
|
||||
onlyPluginIds,
|
||||
includeSetupOnlyChannelPlugins,
|
||||
preferSetupRuntimeForChannelPlugins,
|
||||
shouldActivate: options.activate !== false,
|
||||
cacheKey,
|
||||
};
|
||||
}
|
||||
|
||||
export function getCompatibleActivePluginRegistry(
|
||||
options: PluginLoadOptions = {},
|
||||
): PluginRegistry | undefined {
|
||||
const activeRegistry = getActivePluginRegistry() ?? undefined;
|
||||
if (!activeRegistry) {
|
||||
return undefined;
|
||||
}
|
||||
if (!hasExplicitCompatibilityInputs(options)) {
|
||||
return activeRegistry;
|
||||
}
|
||||
const activeCacheKey = getActivePluginRegistryKey();
|
||||
if (!activeCacheKey) {
|
||||
return undefined;
|
||||
}
|
||||
return resolvePluginLoadCacheContext(options).cacheKey === activeCacheKey
|
||||
? activeRegistry
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function validatePluginConfig(params: {
|
||||
schema?: Record<string, unknown>;
|
||||
cacheKey?: string;
|
||||
|
|
@ -687,38 +765,19 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
|
|||
"loadOpenClawPlugins: activate:false requires cache:false to prevent command registry divergence",
|
||||
);
|
||||
}
|
||||
const env = options.env ?? process.env;
|
||||
// Test env: default-disable plugins unless explicitly configured.
|
||||
// This keeps unit/gateway suites fast and avoids loading heavyweight plugin deps by accident.
|
||||
const cfg = applyTestPluginDefaults(options.config ?? {}, env);
|
||||
const logger = options.logger ?? defaultLogger();
|
||||
const validateOnly = options.mode === "validate";
|
||||
const normalized = normalizePluginsConfig(cfg.plugins);
|
||||
const onlyPluginIds = normalizeScopedPluginIds(options.onlyPluginIds);
|
||||
const onlyPluginIdSet = onlyPluginIds ? new Set(onlyPluginIds) : null;
|
||||
const includeSetupOnlyChannelPlugins = options.includeSetupOnlyChannelPlugins === true;
|
||||
const preferSetupRuntimeForChannelPlugins = options.preferSetupRuntimeForChannelPlugins === true;
|
||||
const shouldActivate = options.activate !== false;
|
||||
// NOTE: `activate` is intentionally excluded from the cache key. All non-activating
|
||||
// (snapshot) callers pass `cache: false` via loadOnboardingPluginRegistry(), so they
|
||||
// never read from or write to the cache. Including `activate` here would be misleading
|
||||
// — it would imply mixed-activate caching is supported, when in practice it is not.
|
||||
const cacheKey = buildCacheKey({
|
||||
workspaceDir: options.workspaceDir,
|
||||
plugins: normalized,
|
||||
installs: cfg.plugins?.installs,
|
||||
const {
|
||||
env,
|
||||
cfg,
|
||||
normalized,
|
||||
onlyPluginIds,
|
||||
includeSetupOnlyChannelPlugins,
|
||||
preferSetupRuntimeForChannelPlugins,
|
||||
runtimeSubagentMode:
|
||||
options.runtimeOptions?.allowGatewaySubagentBinding === true
|
||||
? "gateway-bindable"
|
||||
: options.runtimeOptions?.subagent
|
||||
? "explicit"
|
||||
: "default",
|
||||
pluginSdkResolution: options.pluginSdkResolution,
|
||||
});
|
||||
shouldActivate,
|
||||
cacheKey,
|
||||
} = resolvePluginLoadCacheContext(options);
|
||||
const logger = options.logger ?? defaultLogger();
|
||||
const validateOnly = options.mode === "validate";
|
||||
const onlyPluginIdSet = onlyPluginIds ? new Set(onlyPluginIds) : null;
|
||||
const cacheEnabled = options.cache !== false;
|
||||
if (cacheEnabled) {
|
||||
const cached = getCachedPluginRegistry(cacheKey);
|
||||
|
|
|
|||
Loading…
Reference in New Issue