diff --git a/src/plugin-sdk/channel-entry-contract.test.ts b/src/plugin-sdk/channel-entry-contract.test.ts index ea842f4f109..2b01eaeee0d 100644 --- a/src/plugin-sdk/channel-entry-contract.test.ts +++ b/src/plugin-sdk/channel-entry-contract.test.ts @@ -4,7 +4,10 @@ import path from "node:path"; import { pathToFileURL } from "node:url"; import { afterEach, describe, expect, it, vi } from "vitest"; import { importFreshModule } from "../../test/helpers/import-fresh.ts"; -import { loadBundledEntryExportSync } from "./channel-entry-contract.js"; +import type { ChannelPlugin } from "../channels/plugins/types.plugin.js"; +import type { PluginRuntime } from "../plugins/runtime/types.js"; +import type { OpenClawPluginApi, PluginRegistrationMode } from "../plugins/types.js"; +import { defineBundledChannelEntry, loadBundledEntryExportSync } from "./channel-entry-contract.js"; const tempDirs: string[] = []; @@ -17,6 +20,138 @@ afterEach(() => { vi.unstubAllEnvs(); }); +function createApi(registrationMode: PluginRegistrationMode): OpenClawPluginApi { + return { + registrationMode, + runtime: { registrationMode } as unknown as PluginRuntime, + registerChannel: vi.fn(), + } as unknown as OpenClawPluginApi; +} + +function writeBundledChannelFixture(params: { + pluginRoot: string; + pluginId: string; + runtimeMarker: string; +}) { + fs.mkdirSync(params.pluginRoot, { recursive: true }); + const importerPath = path.join(params.pluginRoot, "index.js"); + fs.writeFileSync(importerPath, "export default {};\n", "utf8"); + fs.writeFileSync( + path.join(params.pluginRoot, "plugin.cjs"), + `module.exports = { + channelPlugin: { + id: ${JSON.stringify(params.pluginId)}, + meta: { + id: ${JSON.stringify(params.pluginId)}, + label: ${JSON.stringify(params.pluginId)}, + selectionLabel: ${JSON.stringify(params.pluginId)}, + docsPath: ${JSON.stringify(`/channels/${params.pluginId}`)}, + blurb: "bundled channel", + }, + capabilities: { chatTypes: ["direct"] }, + config: { + listAccountIds: () => [], + resolveAccount: () => null, + }, + outbound: { deliveryMode: "direct" }, + }, +}; +`, + "utf8", + ); + fs.writeFileSync( + path.join(params.pluginRoot, "runtime.cjs"), + `module.exports = { + setRuntime: () => { + require("node:fs").writeFileSync(${JSON.stringify(params.runtimeMarker)}, "loaded", "utf8"); + }, +}; +`, + "utf8", + ); + return { importerPath }; +} + +function createBundledChannelEntry(params: { + importerPath: string; + pluginId: string; + registerCliMetadata?: (api: OpenClawPluginApi) => void; + registerFull?: (api: OpenClawPluginApi) => void; +}) { + return defineBundledChannelEntry({ + id: params.pluginId, + name: params.pluginId, + description: "bundled channel entry test", + importMetaUrl: pathToFileURL(params.importerPath).href, + plugin: { specifier: "./plugin.cjs", exportName: "channelPlugin" }, + runtime: { specifier: "./runtime.cjs", exportName: "setRuntime" }, + registerCliMetadata: params.registerCliMetadata, + registerFull: params.registerFull, + }); +} + +describe("defineBundledChannelEntry", () => { + it("keeps runtime sidecars out of discovery registration", () => { + const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-bundled-entry-runtime-")); + tempDirs.push(tempRoot); + const runtimeMarker = path.join(tempRoot, "runtime-loaded"); + const pluginId = "bundled-discovery"; + const { importerPath } = writeBundledChannelFixture({ + pluginRoot: path.join(tempRoot, "dist", "extensions", pluginId), + pluginId, + runtimeMarker, + }); + const registerCliMetadata = vi.fn<(api: OpenClawPluginApi) => void>(); + const registerFull = vi.fn<(api: OpenClawPluginApi) => void>(); + const entry = createBundledChannelEntry({ + importerPath, + pluginId, + registerCliMetadata, + registerFull, + }); + + const api = createApi("discovery"); + entry.register(api); + + expect(api.registerChannel).toHaveBeenCalledTimes(1); + expect(registerCliMetadata).toHaveBeenCalledWith(api); + expect(registerFull).not.toHaveBeenCalled(); + expect(fs.existsSync(runtimeMarker)).toBe(false); + }); + + it("keeps setup-runtime and full registration wired to runtime sidecars", () => { + const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-bundled-entry-runtime-")); + tempDirs.push(tempRoot); + const runtimeMarker = path.join(tempRoot, "runtime-loaded"); + const pluginId = "bundled-runtime"; + const { importerPath } = writeBundledChannelFixture({ + pluginRoot: path.join(tempRoot, "dist", "extensions", pluginId), + pluginId, + runtimeMarker, + }); + const registerCliMetadata = vi.fn<(api: OpenClawPluginApi) => void>(); + const registerFull = vi.fn<(api: OpenClawPluginApi) => void>(); + const entry = createBundledChannelEntry({ + importerPath, + pluginId, + registerCliMetadata, + registerFull, + }); + + entry.register(createApi("setup-runtime")); + expect(fs.existsSync(runtimeMarker)).toBe(true); + expect(registerCliMetadata).not.toHaveBeenCalled(); + expect(registerFull).not.toHaveBeenCalled(); + + fs.rmSync(runtimeMarker, { force: true }); + const fullApi = createApi("full"); + entry.register(fullApi); + expect(fs.existsSync(runtimeMarker)).toBe(true); + expect(registerCliMetadata).toHaveBeenCalledWith(fullApi); + expect(registerFull).toHaveBeenCalledWith(fullApi); + }); +}); + async function expectBuiltArtifactNodeRequireFastPath( scope: string, artifactRoot = "dist", diff --git a/src/plugin-sdk/channel-entry-contract.ts b/src/plugin-sdk/channel-entry-contract.ts index 1d795e91db1..8674d489f7b 100644 --- a/src/plugin-sdk/channel-entry-contract.ts +++ b/src/plugin-sdk/channel-entry-contract.ts @@ -483,7 +483,6 @@ export function defineBundledChannelEntry({ return; } const profile = createProfiler({ pluginId: id, source: importMetaUrl }); - profile("bundled-register:setChannelRuntime", () => setChannelRuntime?.(api.runtime)); const channelPlugin = profile("bundled-register:loadChannelPlugin", loadChannelPlugin); profile("bundled-register:registerChannel", () => api.registerChannel({ plugin: channelPlugin as ChannelPlugin }), @@ -492,6 +491,7 @@ export function defineBundledChannelEntry({ profile("bundled-register:registerCliMetadata", () => registerCliMetadata?.(api)); return; } + profile("bundled-register:setChannelRuntime", () => setChannelRuntime?.(api.runtime)); if (api.registrationMode !== "full") { return; } diff --git a/src/plugin-sdk/core.test.ts b/src/plugin-sdk/core.test.ts new file mode 100644 index 00000000000..1e21133393c --- /dev/null +++ b/src/plugin-sdk/core.test.ts @@ -0,0 +1,85 @@ +import { describe, expect, it, vi } from "vitest"; +import type { ChannelPlugin } from "../channels/plugins/types.plugin.js"; +import type { PluginRuntime } from "../plugins/runtime/types.js"; +import type { OpenClawPluginApi, PluginRegistrationMode } from "../plugins/types.js"; +import { defineChannelPluginEntry } from "./core.js"; + +function createChannelPlugin(id: string): ChannelPlugin { + return { + id, + meta: { + id, + label: id, + selectionLabel: id, + docsPath: `/channels/${id}`, + blurb: `${id} channel`, + }, + capabilities: { chatTypes: ["direct"] }, + config: { + listAccountIds: () => [], + resolveAccount: () => null, + }, + outbound: { deliveryMode: "direct" }, + }; +} + +function createApi(registrationMode: PluginRegistrationMode): OpenClawPluginApi { + return { + registrationMode, + runtime: { registrationMode } as unknown as PluginRuntime, + registerChannel: vi.fn(), + } as unknown as OpenClawPluginApi; +} + +describe("defineChannelPluginEntry", () => { + it("keeps runtime helpers out of discovery registration", () => { + const setRuntime = vi.fn<(runtime: PluginRuntime) => void>(); + const registerCliMetadata = vi.fn<(api: OpenClawPluginApi) => void>(); + const registerFull = vi.fn<(api: OpenClawPluginApi) => void>(); + const entry = defineChannelPluginEntry({ + id: "runtime-discovery", + name: "Runtime Discovery", + description: "runtime discovery test", + plugin: createChannelPlugin("runtime-discovery"), + setRuntime, + registerCliMetadata, + registerFull, + }); + + const api = createApi("discovery"); + entry.register(api); + + expect(api.registerChannel).toHaveBeenCalledTimes(1); + expect(registerCliMetadata).toHaveBeenCalledTimes(1); + expect(setRuntime).not.toHaveBeenCalled(); + expect(registerFull).not.toHaveBeenCalled(); + }); + + it("keeps setup-runtime and full registration wired to runtime helpers", () => { + const setRuntime = vi.fn<(runtime: PluginRuntime) => void>(); + const registerCliMetadata = vi.fn<(api: OpenClawPluginApi) => void>(); + const registerFull = vi.fn<(api: OpenClawPluginApi) => void>(); + const entry = defineChannelPluginEntry({ + id: "runtime-activation", + name: "Runtime Activation", + description: "runtime activation test", + plugin: createChannelPlugin("runtime-activation"), + setRuntime, + registerCliMetadata, + registerFull, + }); + + const setupApi = createApi("setup-runtime"); + entry.register(setupApi); + expect(setRuntime).toHaveBeenCalledWith(setupApi.runtime); + expect(registerCliMetadata).not.toHaveBeenCalled(); + expect(registerFull).not.toHaveBeenCalled(); + + setRuntime.mockClear(); + const fullApi = createApi("full"); + entry.register(fullApi); + expect(setRuntime).toHaveBeenCalledWith(fullApi.runtime); + expect(registerCliMetadata).toHaveBeenCalledWith(fullApi); + expect(registerFull).toHaveBeenCalledWith(fullApi); + }); +}); diff --git a/src/plugin-sdk/core.ts b/src/plugin-sdk/core.ts index f1feeb8efb9..6d0cd751a5e 100644 --- a/src/plugin-sdk/core.ts +++ b/src/plugin-sdk/core.ts @@ -503,12 +503,12 @@ export function defineChannelPluginEntry({ registerCliMetadata?.(api); return; } - setRuntime?.(api.runtime); api.registerChannel({ plugin: plugin as ChannelPlugin }); if (api.registrationMode === "discovery") { registerCliMetadata?.(api); return; } + setRuntime?.(api.runtime); if (api.registrationMode !== "full") { return; }