diff --git a/src/channels/plugins/read-only.test.ts b/src/channels/plugins/read-only.test.ts index c92ed9a7496..8afa3f9a5f0 100644 --- a/src/channels/plugins/read-only.test.ts +++ b/src/channels/plugins/read-only.test.ts @@ -269,6 +269,48 @@ describe("listReadOnlyChannelPluginsForConfig", () => { expect(fs.existsSync(fullMarker)).toBe(false); }); + it("clones setup-only plugins when only another owned channel is configured", () => { + const { pluginDir, fullMarker, setupMarker } = writeExternalSetupChannelPlugin({ + pluginId: "external-chat-plugin", + channelId: "alpha-chat", + manifestChannelIds: ["alpha-chat", "beta-chat"], + setupChannelId: "alpha-chat", + }); + const plugins = listReadOnlyChannelPluginsForConfig( + { + channels: { + "beta-chat": { token: "beta-token" }, + }, + plugins: { + load: { paths: [pluginDir] }, + allow: ["external-chat-plugin"], + }, + } as never, + { + env: { ...process.env }, + includePersistedAuthState: false, + }, + ); + + expect(plugins.some((entry) => entry.id === "alpha-chat")).toBe(false); + const betaPlugin = plugins.find((entry) => entry.id === "beta-chat"); + expect(betaPlugin?.meta.id).toBe("beta-chat"); + expect( + betaPlugin?.secrets?.secretTargetRegistryEntries?.some( + (entry) => entry.id === "channels.beta-chat.token", + ), + ).toBe(true); + expect( + betaPlugin?.config.resolveAccount({ + channels: { + "beta-chat": { token: "beta-token" }, + }, + } as never), + ).toMatchObject({ token: "beta-token" }); + expect(fs.existsSync(setupMarker)).toBe(true); + expect(fs.existsSync(fullMarker)).toBe(false); + }); + it("keeps configured external channels visible when no setup entry exists", () => { const { pluginDir, fullMarker, setupMarker } = writeExternalSetupChannelPlugin({ setupEntry: false, @@ -363,7 +405,7 @@ describe("listReadOnlyChannelPluginsForConfig", () => { const { pluginDir, fullMarker, setupMarker } = writeExternalSetupChannelPlugin({ pluginId: "external-chat-plugin", channelId: "external-chat", - manifestChannelIds: ["external-chat", "spoofed-chat"], + manifestChannelIds: ["external-chat"], setupChannelId: "spoofed-chat", }); const plugins = listReadOnlyChannelPluginsForConfig( diff --git a/src/channels/plugins/read-only.ts b/src/channels/plugins/read-only.ts index cd4f7fe641a..c64c8b3707d 100644 --- a/src/channels/plugins/read-only.ts +++ b/src/channels/plugins/read-only.ts @@ -258,6 +258,7 @@ function addSetupChannelPlugins( plugin: ChannelPlugin; }>, options: { + ownedChannelIdsByPluginId: ReadonlyMap; ownedMissingChannelIdsByPluginId: ReadonlyMap; }, ): void { @@ -283,7 +284,8 @@ function addSetupChannelPlugins( ); continue; } - if (setup.plugin.id !== setup.pluginId) { + const ownedChannelIds = options.ownedChannelIdsByPluginId.get(setup.pluginId) ?? []; + if (setup.plugin.id !== setup.pluginId && !ownedChannelIds.includes(setup.plugin.id)) { continue; } addChannelPlugins( @@ -422,16 +424,17 @@ export function resolveReadOnlyChannelPluginsForConfig( }); if (externalPluginIds.length > 0) { const missingChannelIdSet = new Set(missingConfiguredChannelIds); - const ownedMissingChannelIdsByPluginId = new Map( + const externalPluginIdSet = new Set(externalPluginIds); + const ownedChannelIdsByPluginId = new Map( externalManifestRecords - .filter((record) => externalPluginIds.includes(record.id)) - .map( - (record) => - [ - record.id, - record.channels.filter((channelId) => missingChannelIdSet.has(channelId)), - ] as const, - ), + .filter((record) => externalPluginIdSet.has(record.id)) + .map((record) => [record.id, record.channels] as const), + ); + const ownedMissingChannelIdsByPluginId = new Map( + [...ownedChannelIdsByPluginId].map( + ([pluginId, channelIds]) => + [pluginId, channelIds.filter((channelId) => missingChannelIdSet.has(channelId))] as const, + ), ); const registry = loadOpenClawPlugins({ config: cfg, @@ -446,6 +449,7 @@ export function resolveReadOnlyChannelPluginsForConfig( onlyPluginIds: externalPluginIds, }); addSetupChannelPlugins(byId, registry.channelSetups, { + ownedChannelIdsByPluginId, ownedMissingChannelIdsByPluginId, }); }