diff --git a/src/plugins/loader.test.ts b/src/plugins/loader.test.ts index d56d4ad99de..1dd91fca48f 100644 --- a/src/plugins/loader.test.ts +++ b/src/plugins/loader.test.ts @@ -523,6 +523,7 @@ function createSetupEntryChannelPluginFixture(params: { configured: boolean; startupDeferConfiguredChannelFullLoadUntilAfterListen?: boolean; useBundledSetupEntryContract?: boolean; + splitBundledSetupSecrets?: boolean; }) { useNoBundledPlugins(); const pluginDir = makeTempDir(); @@ -618,6 +619,18 @@ module.exports = { }, outbound: { deliveryMode: "direct" }, }), + ${ + params.splitBundledSetupSecrets + ? `loadSetupSecrets: () => ({ + secretTargetRegistryEntries: [ + { + id: ${JSON.stringify(`channels.${params.id}.setup-token`)}, + targetType: "channel", + }, + ], + }),` + : "" + } };` : `require("node:fs").writeFileSync(${JSON.stringify(setupMarker)}, "loaded", "utf-8"); module.exports = { @@ -3215,6 +3228,33 @@ module.exports = { expectSetupLoaded: true, expectedChannels: 1, }, + { + name: "preserves bundled setupEntry split secrets for setup-runtime channel loads", + fixture: { + id: "setup-runtime-bundled-contract-secrets-test", + label: "Setup Runtime Bundled Contract Secrets Test", + packageName: "@openclaw/setup-runtime-bundled-contract-secrets-test", + fullBlurb: "full entry should not run while unconfigured", + setupBlurb: "setup runtime bundled contract secrets", + configured: false, + useBundledSetupEntryContract: true, + splitBundledSetupSecrets: true, + }, + load: ({ pluginDir }: { pluginDir: string }) => + loadOpenClawPlugins({ + cache: false, + config: { + plugins: { + load: { paths: [pluginDir] }, + allow: ["setup-runtime-bundled-contract-secrets-test"], + }, + }, + }), + expectFullLoaded: false, + expectSetupLoaded: true, + expectedChannels: 1, + expectedSetupSecretId: "channels.setup-runtime-bundled-contract-secrets-test.setup-token", + }, { name: "does not prefer setupEntry for configured channel loads without startup opt-in", fixture: { @@ -3246,15 +3286,41 @@ module.exports = { expectSetupLoaded: false, expectedChannels: 1, }, - ])("$name", ({ fixture, load, expectFullLoaded, expectSetupLoaded, expectedChannels }) => { - const built = createSetupEntryChannelPluginFixture(fixture); - const registry = load({ pluginDir: built.pluginDir }); + ])( + "$name", + ({ + fixture, + load, + expectFullLoaded, + expectSetupLoaded, + expectedChannels, + expectedSetupSecretId, + }) => { + const built = createSetupEntryChannelPluginFixture(fixture); + const registry = load({ pluginDir: built.pluginDir }); - expect(fs.existsSync(built.fullMarker)).toBe(expectFullLoaded); - expect(fs.existsSync(built.setupMarker)).toBe(expectSetupLoaded); - expect(registry.channelSetups).toHaveLength(1); - expect(registry.channels).toHaveLength(expectedChannels); - }); + expect(fs.existsSync(built.fullMarker)).toBe(expectFullLoaded); + expect(fs.existsSync(built.setupMarker)).toBe(expectSetupLoaded); + expect(registry.channelSetups).toHaveLength(1); + expect(registry.channels).toHaveLength(expectedChannels); + if (expectedSetupSecretId) { + expect(registry.channelSetups[0]?.plugin.secrets?.secretTargetRegistryEntries).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: expectedSetupSecretId, + }), + ]), + ); + expect(registry.channels[0]?.plugin.secrets?.secretTargetRegistryEntries).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: expectedSetupSecretId, + }), + ]), + ); + } + }, + ); it("isolates loadSetupPlugin errors as per-plugin diagnostics instead of crashing registry load", () => { useNoBundledPlugins(); diff --git a/src/plugins/loader.ts b/src/plugins/loader.ts index 6e4be973f6d..f714118dc25 100644 --- a/src/plugins/loader.ts +++ b/src/plugins/loader.ts @@ -644,6 +644,26 @@ function resolvePluginModuleExport(moduleExport: unknown): { return {}; } +function mergeSetupPluginSection( + baseValue: T | undefined, + setupValue: T | undefined, +): T | undefined { + if (baseValue && setupValue && typeof baseValue === "object" && typeof setupValue === "object") { + const merged = { + ...(baseValue as Record), + }; + for (const [key, value] of Object.entries(setupValue as Record)) { + if (value !== undefined) { + merged[key] = value; + } + } + return { + ...merged, + } as T; + } + return setupValue ?? baseValue; +} + function resolveSetupChannelRegistration(moduleExport: unknown): { plugin?: ChannelPlugin; loadError?: unknown; @@ -655,6 +675,7 @@ function resolveSetupChannelRegistration(moduleExport: unknown): { const setupEntryRecord = resolved as { kind?: unknown; loadSetupPlugin?: unknown; + loadSetupSecrets?: unknown; }; if ( setupEntryRecord.kind === "bundled-channel-setup-entry" && @@ -662,9 +683,20 @@ function resolveSetupChannelRegistration(moduleExport: unknown): { ) { try { const loadedPlugin = setupEntryRecord.loadSetupPlugin(); + const loadedSecrets = + typeof setupEntryRecord.loadSetupSecrets === "function" + ? (setupEntryRecord.loadSetupSecrets() as ChannelPlugin["secrets"] | undefined) + : undefined; if (loadedPlugin && typeof loadedPlugin === "object") { + const mergedSecrets = mergeSetupPluginSection( + (loadedPlugin as ChannelPlugin).secrets, + loadedSecrets, + ); return { - plugin: loadedPlugin as ChannelPlugin, + plugin: { + ...(loadedPlugin as ChannelPlugin), + ...(mergedSecrets !== undefined ? { secrets: mergedSecrets } : {}), + }, }; } } catch (err) {