mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-02 14:11:06 +00:00
fix: tighten empty plugin registry reuse
This commit is contained in:
@@ -7,6 +7,8 @@ const mocks = vi.hoisted(() => ({
|
||||
vi.fn<typeof import("../loader.js").resolveCompatibleRuntimePluginRegistry>(),
|
||||
resolveRuntimePluginRegistry: vi.fn<typeof import("../loader.js").resolveRuntimePluginRegistry>(),
|
||||
getActivePluginRegistry: vi.fn<typeof import("../runtime.js").getActivePluginRegistry>(),
|
||||
getActivePluginRegistryWorkspaceDir:
|
||||
vi.fn<typeof import("../runtime.js").getActivePluginRegistryWorkspaceDir>(),
|
||||
resolveConfiguredChannelPluginIds:
|
||||
vi.fn<typeof import("../channel-plugin-ids.js").resolveConfiguredChannelPluginIds>(),
|
||||
resolveDiscoverableScopedChannelPluginIds:
|
||||
@@ -76,7 +78,9 @@ vi.mock("../runtime.js", () => ({
|
||||
getActivePluginHttpRouteRegistry: () => null,
|
||||
getActivePluginRegistry: (...args: Parameters<typeof mocks.getActivePluginRegistry>) =>
|
||||
mocks.getActivePluginRegistry(...args),
|
||||
getActivePluginRegistryWorkspaceDir: () => undefined,
|
||||
getActivePluginRegistryWorkspaceDir: (
|
||||
...args: Parameters<typeof mocks.getActivePluginRegistryWorkspaceDir>
|
||||
) => mocks.getActivePluginRegistryWorkspaceDir(...args),
|
||||
}));
|
||||
|
||||
vi.mock("../channel-plugin-ids.js", () => ({
|
||||
@@ -119,6 +123,7 @@ describe("ensurePluginRegistryLoaded", () => {
|
||||
mocks.resolveCompatibleRuntimePluginRegistry.mockReset();
|
||||
mocks.resolveRuntimePluginRegistry.mockReset();
|
||||
mocks.getActivePluginRegistry.mockReset();
|
||||
mocks.getActivePluginRegistryWorkspaceDir.mockReset();
|
||||
mocks.resolveConfiguredChannelPluginIds.mockReset();
|
||||
mocks.resolveDiscoverableScopedChannelPluginIds.mockReset();
|
||||
mocks.resolveChannelPluginIds.mockReset();
|
||||
@@ -129,6 +134,7 @@ describe("ensurePluginRegistryLoaded", () => {
|
||||
resetPluginRegistryLoadedForTests();
|
||||
|
||||
mocks.getActivePluginRegistry.mockReturnValue(null);
|
||||
mocks.getActivePluginRegistryWorkspaceDir.mockReturnValue(undefined);
|
||||
mocks.resolveCompatibleRuntimePluginRegistry.mockReturnValue(undefined);
|
||||
mocks.loadOpenClawPlugins.mockReturnValue(createEmptyPluginRegistry());
|
||||
mocks.resolveRuntimePluginRegistry.mockImplementation(
|
||||
@@ -333,6 +339,33 @@ describe("ensurePluginRegistryLoaded", () => {
|
||||
expect(load.workspaceDir).toBe("/resolved-workspace");
|
||||
});
|
||||
|
||||
it("does not reuse non-empty all-scope registries without loader compatibility", () => {
|
||||
mocks.resolveEffectivePluginIds.mockReturnValue(["demo"]);
|
||||
|
||||
ensurePluginRegistryLoaded({
|
||||
scope: "all",
|
||||
config: { plugins: { allow: ["demo"] } } as never,
|
||||
});
|
||||
const activeRegistry = createEmptyPluginRegistry();
|
||||
activeRegistry.plugins.push({
|
||||
id: "demo",
|
||||
source: "/tmp/demo.js",
|
||||
origin: "workspace",
|
||||
enabled: true,
|
||||
status: "loaded",
|
||||
} as never);
|
||||
mocks.getActivePluginRegistry.mockReturnValue(activeRegistry);
|
||||
mocks.getActivePluginRegistryWorkspaceDir.mockReturnValue("/resolved-workspace");
|
||||
mocks.loadOpenClawPlugins.mockClear();
|
||||
|
||||
ensurePluginRegistryLoaded({
|
||||
scope: "all",
|
||||
config: { plugins: { allow: ["demo"], entries: { demo: { value: "changed" } } } } as never,
|
||||
});
|
||||
|
||||
expect(loadOptions().onlyPluginIds).toEqual(["demo"]);
|
||||
});
|
||||
|
||||
it("preserves empty all-scope loads instead of widening to all discovered plugins", () => {
|
||||
mocks.resolveEffectivePluginIds.mockReturnValue([]);
|
||||
|
||||
@@ -344,6 +377,149 @@ describe("ensurePluginRegistryLoaded", () => {
|
||||
expect(loadOptions().onlyPluginIds).toEqual([]);
|
||||
});
|
||||
|
||||
it("reuses an active empty registry for repeated empty all-scope loads", () => {
|
||||
mocks.resolveEffectivePluginIds.mockReturnValue([]);
|
||||
|
||||
ensurePluginRegistryLoaded({
|
||||
scope: "all",
|
||||
config: { plugins: { enabled: true } } as never,
|
||||
});
|
||||
const emptyRegistry = createEmptyPluginRegistry();
|
||||
mocks.getActivePluginRegistry.mockReturnValue(emptyRegistry);
|
||||
mocks.getActivePluginRegistryWorkspaceDir.mockReturnValue("/resolved-workspace");
|
||||
mocks.loadOpenClawPlugins.mockClear();
|
||||
|
||||
ensurePluginRegistryLoaded({
|
||||
scope: "all",
|
||||
config: { plugins: { enabled: true } } as never,
|
||||
});
|
||||
|
||||
expect(mocks.loadOpenClawPlugins).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not reuse an empty active registry from another workspace", () => {
|
||||
mocks.resolveEffectivePluginIds.mockReturnValue([]);
|
||||
|
||||
ensurePluginRegistryLoaded({
|
||||
scope: "all",
|
||||
config: { plugins: { enabled: true } } as never,
|
||||
});
|
||||
const emptyRegistry = createEmptyPluginRegistry();
|
||||
mocks.getActivePluginRegistry.mockReturnValue(emptyRegistry);
|
||||
mocks.getActivePluginRegistryWorkspaceDir.mockReturnValue("/other-workspace");
|
||||
mocks.loadOpenClawPlugins.mockClear();
|
||||
|
||||
ensurePluginRegistryLoaded({
|
||||
scope: "all",
|
||||
config: { plugins: { enabled: true } } as never,
|
||||
});
|
||||
|
||||
expect(loadOptions().onlyPluginIds).toEqual([]);
|
||||
});
|
||||
|
||||
it("does not reuse a non-empty active registry for empty all-scope loads", () => {
|
||||
mocks.resolveEffectivePluginIds.mockReturnValue([]);
|
||||
|
||||
ensurePluginRegistryLoaded({
|
||||
scope: "all",
|
||||
config: { plugins: { enabled: true } } as never,
|
||||
});
|
||||
const staleRegistry = createEmptyPluginRegistry();
|
||||
staleRegistry.plugins.push({
|
||||
id: "stale",
|
||||
source: "/tmp/stale.js",
|
||||
origin: "workspace",
|
||||
enabled: true,
|
||||
status: "loaded",
|
||||
} as never);
|
||||
mocks.getActivePluginRegistry.mockReturnValue(staleRegistry);
|
||||
mocks.loadOpenClawPlugins.mockClear();
|
||||
|
||||
ensurePluginRegistryLoaded({
|
||||
scope: "all",
|
||||
config: { plugins: { enabled: true } } as never,
|
||||
});
|
||||
|
||||
expect(loadOptions().onlyPluginIds).toEqual([]);
|
||||
});
|
||||
|
||||
it("does not reuse a disabled-record registry for empty all-scope loads", () => {
|
||||
mocks.resolveEffectivePluginIds.mockReturnValue([]);
|
||||
|
||||
ensurePluginRegistryLoaded({
|
||||
scope: "all",
|
||||
config: { plugins: { enabled: true } } as never,
|
||||
});
|
||||
const disabledRegistry = createEmptyPluginRegistry();
|
||||
disabledRegistry.plugins.push({
|
||||
id: "disabled",
|
||||
source: "/tmp/disabled.js",
|
||||
origin: "workspace",
|
||||
enabled: false,
|
||||
status: "disabled",
|
||||
} as never);
|
||||
mocks.getActivePluginRegistry.mockReturnValue(disabledRegistry);
|
||||
mocks.getActivePluginRegistryWorkspaceDir.mockReturnValue("/resolved-workspace");
|
||||
mocks.loadOpenClawPlugins.mockClear();
|
||||
|
||||
ensurePluginRegistryLoaded({
|
||||
scope: "all",
|
||||
config: { plugins: { enabled: true } } as never,
|
||||
});
|
||||
|
||||
expect(loadOptions().onlyPluginIds).toEqual([]);
|
||||
});
|
||||
|
||||
it("does not reuse a failed diagnostic registry for explicit plugin scopes", () => {
|
||||
const failedRegistry = createEmptyPluginRegistry();
|
||||
failedRegistry.plugins.push({
|
||||
id: "failed",
|
||||
source: "/tmp/failed.js",
|
||||
origin: "workspace",
|
||||
enabled: true,
|
||||
status: "error",
|
||||
} as never);
|
||||
failedRegistry.diagnostics.push({
|
||||
level: "error",
|
||||
pluginId: "failed",
|
||||
message: "failed to load",
|
||||
} as never);
|
||||
mocks.getActivePluginRegistry.mockReturnValue(failedRegistry);
|
||||
mocks.getActivePluginRegistryWorkspaceDir.mockReturnValue("/resolved-workspace");
|
||||
|
||||
ensurePluginRegistryLoaded({
|
||||
scope: "all",
|
||||
config: { plugins: { enabled: true } } as never,
|
||||
onlyPluginIds: ["failed"],
|
||||
});
|
||||
|
||||
expect(loadOptions().onlyPluginIds).toEqual(["failed"]);
|
||||
});
|
||||
|
||||
it("does not reuse a setup-only registry for explicit plugin scopes", () => {
|
||||
const setupRegistry = createEmptyPluginRegistry();
|
||||
setupRegistry.plugins.push({
|
||||
id: "setup-only",
|
||||
source: "/tmp/setup-only.js",
|
||||
origin: "workspace",
|
||||
enabled: false,
|
||||
status: "disabled",
|
||||
} as never);
|
||||
setupRegistry.channelSetups.push({
|
||||
pluginId: "setup-only",
|
||||
} as never);
|
||||
mocks.getActivePluginRegistry.mockReturnValue(setupRegistry);
|
||||
mocks.getActivePluginRegistryWorkspaceDir.mockReturnValue("/resolved-workspace");
|
||||
|
||||
ensurePluginRegistryLoaded({
|
||||
scope: "all",
|
||||
config: { plugins: { enabled: true } } as never,
|
||||
onlyPluginIds: ["setup-only"],
|
||||
});
|
||||
|
||||
expect(loadOptions().onlyPluginIds).toEqual(["setup-only"]);
|
||||
});
|
||||
|
||||
it("reuses a compatible active registry instead of forcing a broad reload", () => {
|
||||
const activeRegistry = createEmptyPluginRegistry();
|
||||
activeRegistry.plugins.push({
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import { withActivatedPluginIds } from "../activation-context.js";
|
||||
import { getLoadedRuntimePluginRegistry } from "../active-runtime-registry.js";
|
||||
import {
|
||||
getLoadedRuntimePluginRegistry,
|
||||
registryContainsRuntimePluginIds,
|
||||
} from "../active-runtime-registry.js";
|
||||
import {
|
||||
resolveChannelPluginIds,
|
||||
resolveConfiguredChannelPluginIds,
|
||||
@@ -13,7 +16,7 @@ import {
|
||||
hasNonEmptyPluginIdScope,
|
||||
normalizePluginIdScope,
|
||||
} from "../plugin-scope.js";
|
||||
import { getActivePluginRegistry } from "../runtime.js";
|
||||
import { getActivePluginRegistry, getActivePluginRegistryWorkspaceDir } from "../runtime.js";
|
||||
import {
|
||||
buildPluginRuntimeLoadOptionsFromValues,
|
||||
resolvePluginRuntimeLoadContext,
|
||||
@@ -42,18 +45,17 @@ function activeRegistrySatisfiesScope(
|
||||
active: ReturnType<typeof getActivePluginRegistry>,
|
||||
expectedChannelPluginIds: readonly string[],
|
||||
requestedPluginIds: readonly string[] | undefined,
|
||||
requestedWorkspaceDir: string | undefined,
|
||||
): boolean {
|
||||
if (!active) {
|
||||
return false;
|
||||
}
|
||||
if (requestedPluginIds !== undefined) {
|
||||
if (requestedPluginIds.length === 0) {
|
||||
const activeWorkspaceDir = getActivePluginRegistryWorkspaceDir();
|
||||
if (requestedWorkspaceDir !== undefined && activeWorkspaceDir !== requestedWorkspaceDir) {
|
||||
return false;
|
||||
}
|
||||
const activePluginIds = new Set(
|
||||
active.plugins.filter((plugin) => plugin.status === "loaded").map((plugin) => plugin.id),
|
||||
);
|
||||
return requestedPluginIds.every((pluginId) => activePluginIds.has(pluginId));
|
||||
return registryContainsRuntimePluginIds(active, requestedPluginIds);
|
||||
}
|
||||
const activeChannelPluginIds = new Set(active.channels.map((entry) => entry.plugin.id));
|
||||
switch (scope) {
|
||||
@@ -155,17 +157,30 @@ export function ensurePluginRegistryLoaded(options?: {
|
||||
? (requestedPluginIds ?? [])
|
||||
: resolveScopePluginIds({ scope, context });
|
||||
const active = getActivePluginRegistry();
|
||||
const requestedPluginIdsForScope = scope === "all" ? expectedPluginIds : undefined;
|
||||
const requestedPluginIdsForScope =
|
||||
scope === "all" && expectedPluginIds.length === 0 ? expectedPluginIds : undefined;
|
||||
if (
|
||||
!scopedLoad &&
|
||||
scopeRank(pluginRegistryLoaded) >= scopeRank(scope) &&
|
||||
activeRegistrySatisfiesScope(scope, active, expectedPluginIds, requestedPluginIdsForScope)
|
||||
activeRegistrySatisfiesScope(
|
||||
scope,
|
||||
active,
|
||||
expectedPluginIds,
|
||||
requestedPluginIdsForScope,
|
||||
context.workspaceDir,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
(pluginRegistryLoaded === "none" || scopedLoad) &&
|
||||
activeRegistrySatisfiesScope(scope, active, expectedPluginIds, requestedPluginIds)
|
||||
activeRegistrySatisfiesScope(
|
||||
scope,
|
||||
active,
|
||||
expectedPluginIds,
|
||||
requestedPluginIds,
|
||||
context.workspaceDir,
|
||||
)
|
||||
) {
|
||||
if (!scopedLoad) {
|
||||
pluginRegistryLoaded = scope;
|
||||
|
||||
Reference in New Issue
Block a user