fix(plugins): preserve empty provider scopes

This commit is contained in:
Vincent Koc
2026-04-12 11:04:28 +01:00
parent 269a5b0cfc
commit a5689accc4
4 changed files with 60 additions and 6 deletions

View File

@@ -55,6 +55,12 @@ describe("getCompatibleActivePluginRegistry", () => {
onlyPluginIds: ["demo"],
}),
).toBeUndefined();
expect(
__testing.getCompatibleActivePluginRegistry({
...loadOptions,
onlyPluginIds: [],
}),
).toBeUndefined();
expect(
__testing.getCompatibleActivePluginRegistry({
...loadOptions,
@@ -183,6 +189,25 @@ describe("resolveRuntimePluginRegistry", () => {
expect(resolveRuntimePluginRegistry()).toBe(registry);
});
it("does not treat an explicit empty plugin scope as the active runtime", () => {
const registry = createEmptyPluginRegistry();
const loadOptions = {
config: {
plugins: {
allow: ["demo"],
load: { paths: ["/tmp/demo.js"] },
},
},
workspaceDir: "/tmp/workspace-a",
};
const { cacheKey } = __testing.resolvePluginLoadCacheContext(loadOptions);
setActivePluginRegistry(registry, cacheKey);
const scopedEmpty = resolveRuntimePluginRegistry({ ...loadOptions, onlyPluginIds: [] });
expect(scopedEmpty).not.toBe(registry);
expect(scopedEmpty?.plugins).toEqual([]);
});
});
describe("clearPluginLoaderCache", () => {

View File

@@ -1431,6 +1431,34 @@ module.exports = { id: "throws-after-import", register() {} };`,
run();
});
it("treats an explicit empty plugin scope as scoped-empty instead of unscoped", () => {
useNoBundledPlugins();
const allowed = writePlugin({
id: "allowed-empty-scope",
filename: "allowed-empty-scope.cjs",
body: `module.exports = { id: "allowed-empty-scope", register() {} };`,
});
const extra = writePlugin({
id: "extra-empty-scope",
filename: "extra-empty-scope.cjs",
body: `module.exports = { id: "extra-empty-scope", register() {} };`,
});
const registry = loadOpenClawPlugins({
cache: false,
activate: false,
config: {
plugins: {
load: { paths: [allowed.file, extra.file] },
allow: ["allowed-empty-scope", "extra-empty-scope"],
},
},
onlyPluginIds: [],
});
expect(registry.plugins).toEqual([]);
});
it("only publishes plugin commands to the global registry during activating loads", async () => {
useNoBundledPlugins();
const plugin = writePlugin({

View File

@@ -358,7 +358,8 @@ function buildCacheKey(params: {
},
]),
);
const scopeKey = JSON.stringify(params.onlyPluginIds ?? []);
const scopeKey =
params.onlyPluginIds === undefined ? "__unscoped__" : JSON.stringify(params.onlyPluginIds);
const setupOnlyKey = params.includeSetupOnlyChannelPlugins === true ? "setup-only" : "runtime";
const startupChannelMode =
params.preferSetupRuntimeForChannelPlugins === true ? "prefer-setup" : "full";
@@ -378,7 +379,7 @@ function normalizeScopedPluginIds(ids?: string[]): string[] | undefined {
return undefined;
}
const normalized = Array.from(new Set(ids.map((id) => id.trim()).filter(Boolean))).toSorted();
return normalized.length > 0 ? normalized : undefined;
return normalized;
}
function matchesScopedPluginRequest(params: {
@@ -442,19 +443,19 @@ function buildActivationMetadataHash(params: {
}
function hasExplicitCompatibilityInputs(options: PluginLoadOptions): boolean {
return Boolean(
return (
options.config !== undefined ||
options.activationSourceConfig !== undefined ||
options.autoEnabledReasons !== undefined ||
options.workspaceDir !== undefined ||
options.env !== undefined ||
options.onlyPluginIds?.length ||
options.onlyPluginIds !== undefined ||
options.runtimeOptions !== undefined ||
options.pluginSdkResolution !== undefined ||
options.coreGatewayHandlers !== undefined ||
options.includeSetupOnlyChannelPlugins === true ||
options.preferSetupRuntimeForChannelPlugins === true ||
options.loadModules === false,
options.loadModules === false
);
}

View File

@@ -20,7 +20,7 @@ export function loadPluginMetadataRegistrySnapshot(options?: {
activate: false,
mode: "validate",
loadModules: options?.loadModules,
...(options?.onlyPluginIds?.length ? { onlyPluginIds: options.onlyPluginIds } : {}),
...(options?.onlyPluginIds !== undefined ? { onlyPluginIds: options.onlyPluginIds } : {}),
}),
);
}