fix(plugins): keep discovery off runtime setters

This commit is contained in:
Gustavo Madeira Santana
2026-04-24 21:37:38 -04:00
parent e877a7a1b7
commit c6602fc561
4 changed files with 223 additions and 3 deletions

View File

@@ -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<ChannelPlugin>({
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",

View File

@@ -483,7 +483,6 @@ export function defineBundledChannelEntry<TPlugin = ChannelPlugin>({
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<TPlugin = ChannelPlugin>({
profile("bundled-register:registerCliMetadata", () => registerCliMetadata?.(api));
return;
}
profile("bundled-register:setChannelRuntime", () => setChannelRuntime?.(api.runtime));
if (api.registrationMode !== "full") {
return;
}

View File

@@ -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);
});
});

View File

@@ -503,12 +503,12 @@ export function defineChannelPluginEntry<TPlugin>({
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;
}