fix(cli): resolve message channel plugin scopes

This commit is contained in:
Peter Steinberger
2026-04-27 21:02:00 +01:00
parent 0c305596a2
commit 5e49e8590d
7 changed files with 98 additions and 10 deletions

View File

@@ -7,6 +7,8 @@ const mocks = vi.hoisted(() => ({
getActivePluginRegistry: vi.fn<typeof import("../runtime.js").getActivePluginRegistry>(),
resolveConfiguredChannelPluginIds:
vi.fn<typeof import("../channel-plugin-ids.js").resolveConfiguredChannelPluginIds>(),
resolveDiscoverableScopedChannelPluginIds:
vi.fn<typeof import("../channel-plugin-ids.js").resolveDiscoverableScopedChannelPluginIds>(),
resolveChannelPluginIds:
vi.fn<typeof import("../channel-plugin-ids.js").resolveChannelPluginIds>(),
applyPluginAutoEnable:
@@ -39,6 +41,9 @@ vi.mock("../channel-plugin-ids.js", () => ({
resolveConfiguredChannelPluginIds: (
...args: Parameters<typeof mocks.resolveConfiguredChannelPluginIds>
) => mocks.resolveConfiguredChannelPluginIds(...args),
resolveDiscoverableScopedChannelPluginIds: (
...args: Parameters<typeof mocks.resolveDiscoverableScopedChannelPluginIds>
) => mocks.resolveDiscoverableScopedChannelPluginIds(...args),
resolveChannelPluginIds: (...args: Parameters<typeof mocks.resolveChannelPluginIds>) =>
mocks.resolveChannelPluginIds(...args),
}));
@@ -67,6 +72,7 @@ describe("ensurePluginRegistryLoaded", () => {
mocks.resolveRuntimePluginRegistry.mockReset();
mocks.getActivePluginRegistry.mockReset();
mocks.resolveConfiguredChannelPluginIds.mockReset();
mocks.resolveDiscoverableScopedChannelPluginIds.mockReset();
mocks.resolveChannelPluginIds.mockReset();
mocks.applyPluginAutoEnable.mockReset();
mocks.resolveAgentWorkspaceDir.mockClear();
@@ -95,6 +101,7 @@ describe("ensurePluginRegistryLoaded", () => {
demo: ["demo configured"],
},
}));
mocks.resolveDiscoverableScopedChannelPluginIds.mockReturnValue([]);
});
it("uses the shared runtime load context for configured-channel loads", () => {
@@ -215,6 +222,54 @@ describe("ensurePluginRegistryLoaded", () => {
);
});
it("maps explicit channel scopes to owner plugin ids before loading", () => {
const rawConfig = { channels: { "external-chat": { token: "configured" } } };
mocks.resolveDiscoverableScopedChannelPluginIds.mockReturnValue(["external-chat-plugin"]);
ensurePluginRegistryLoaded({
scope: "configured-channels",
config: rawConfig as never,
onlyChannelIds: ["external-chat"],
});
expect(mocks.resolveDiscoverableScopedChannelPluginIds).toHaveBeenCalledWith(
expect.objectContaining({
config: expect.objectContaining({
...rawConfig,
plugins: expect.objectContaining({
entries: expect.objectContaining({
demo: { enabled: true },
}),
}),
}),
activationSourceConfig: rawConfig,
channelIds: ["external-chat"],
workspaceDir: "/resolved-workspace",
}),
);
expect(mocks.loadOpenClawPlugins).toHaveBeenCalledWith(
expect.objectContaining({
config: expect.objectContaining({
plugins: expect.objectContaining({
allow: ["external-chat-plugin"],
entries: expect.objectContaining({
"external-chat-plugin": { enabled: true },
}),
}),
}),
activationSourceConfig: expect.objectContaining({
plugins: expect.objectContaining({
allow: ["external-chat-plugin"],
entries: expect.objectContaining({
"external-chat-plugin": { enabled: true },
}),
}),
}),
onlyPluginIds: ["external-chat-plugin"],
}),
);
});
it("forwards explicit empty scopes without widening to channel resolution", () => {
ensurePluginRegistryLoaded({
scope: "configured-channels",

View File

@@ -3,6 +3,7 @@ import { withActivatedPluginIds } from "../activation-context.js";
import {
resolveChannelPluginIds,
resolveConfiguredChannelPluginIds,
resolveDiscoverableScopedChannelPluginIds,
} from "../channel-plugin-ids.js";
import { loadOpenClawPlugins, resolveRuntimePluginRegistry } from "../loader.js";
import {
@@ -90,11 +91,30 @@ export function ensurePluginRegistryLoaded(options?: {
env?: NodeJS.ProcessEnv;
workspaceDir?: string;
onlyPluginIds?: string[];
onlyChannelIds?: string[];
}): void {
const scope = options?.scope ?? "all";
const requestedPluginIds = normalizePluginIdScope(options?.onlyPluginIds);
const scopedLoad = hasExplicitPluginIdScope(requestedPluginIds);
const requestedPluginIdsFromOptions = normalizePluginIdScope(options?.onlyPluginIds);
const requestedChannelIds = normalizePluginIdScope(options?.onlyChannelIds);
const context = resolvePluginRuntimeLoadContext(options);
const requestedChannelOwnerPluginIds =
requestedChannelIds === undefined
? undefined
: resolveDiscoverableScopedChannelPluginIds({
config: context.config,
activationSourceConfig: context.activationSourceConfig,
channelIds: requestedChannelIds,
workspaceDir: context.workspaceDir,
env: context.env,
});
const requestedPluginIds =
requestedChannelOwnerPluginIds === undefined
? requestedPluginIdsFromOptions
: normalizePluginIdScope([
...(requestedPluginIdsFromOptions ?? []),
...requestedChannelOwnerPluginIds,
]);
const scopedLoad = hasExplicitPluginIdScope(requestedPluginIds);
const expectedChannelPluginIds = scopedLoad
? (requestedPluginIds ?? [])
: scope === "configured-channels"
@@ -129,14 +149,18 @@ export function ensurePluginRegistryLoaded(options?: {
return;
}
const scopedConfig =
!scopedLoad && scope === "configured-channels" && expectedChannelPluginIds.length > 0
scope === "configured-channels" &&
expectedChannelPluginIds.length > 0 &&
(!scopedLoad || requestedChannelOwnerPluginIds !== undefined)
? (withActivatedPluginIds({
config: context.config,
pluginIds: expectedChannelPluginIds,
}) ?? context.config)
: context.config;
const scopedActivationSourceConfig =
!scopedLoad && scope === "configured-channels" && expectedChannelPluginIds.length > 0
scope === "configured-channels" &&
expectedChannelPluginIds.length > 0 &&
(!scopedLoad || requestedChannelOwnerPluginIds !== undefined)
? (withActivatedPluginIds({
config: context.activationSourceConfig,
pluginIds: expectedChannelPluginIds,