fix(channels): narrow bundled channel loader diagnostics

Narrow bundled channel wrong-loader diagnostics, preserve ordinary missing-register debug details, and add focused loader coverage.

Verification:
- pnpm test src/plugins/loader.test.ts src/plugins/loader.cli-metadata.test.ts -- --reporter=verbose
- env -u OPENCLAW_TESTBOX -u OPENCLAW_TESTBOX_REMOTE_RUN pnpm check:changed

CI notes:
- Real behavior proof passed with maintainer proof: override label.
- check-additional-boundaries-d is red from existing extension test imports outside this PR: extensions/telegram/src/bot.create-telegram-bot.test.ts and extensions/whatsapp/src/login.coverage.test.ts.

Co-authored-by: Chinar Amrutkar <chinar.amrutkar@gmail.com>
This commit is contained in:
Chinar Amrutkar
2026-05-11 16:58:06 +01:00
committed by GitHub
parent 820b21ad46
commit f2c15f94ae
4 changed files with 126 additions and 4 deletions

View File

@@ -61,6 +61,7 @@ Docs: https://docs.openclaw.ai
- Plugins/update: include beta-channel fallback details in plugin update outcomes when `@beta` is unavailable and OpenClaw uses the recorded default/latest plugin spec, making mixed beta/latest plugin cohorts visible in update summaries. Fixes #80689. Thanks @BKF-Gitty.
- Control UI/performance: scope Nodes polling to the active Nodes tab, debounce stale session-list reconciliation, and bound chat-side session refreshes so long-running dashboards avoid background reload churn. Thanks @BunsDev.
- Plugins/channels: explain bundled channel entry files that reach the legacy plugin loader as setup-runtime loader mismatches instead of generic missing-register failures. Thanks @chinar-amrutkar.
- Bonjour/Gateway: treat active ciao probing and fresh name-conflict renames as in-progress so the mDNS watchdog waits for probe settlement before retrying, preventing rapid re-advertise loops on Windows, WSL, and other multicast-hostile hosts. (#74778) Refs #74242. Thanks @fuller-stack-dev.
- Providers/MiniMax: send a minimal Anthropic-compatible user fallback when message conversion filters a turn to an empty payload, so MiniMax M2.7 no longer returns `chat content is empty` after tool-heavy sessions. Fixes #74589. Thanks @neeravmakwana and @DerekEXS.
- Tools/media: preserve implicit allow-all semantics from `tools.alsoAllow`-only policies when preconstructing built-in media generation and PDF tools, so configured media tools become live without forcing `tools.allow: ["*", ...]`. Fixes #77841. Thanks @trialanderrorstudios.

View File

@@ -26,6 +26,58 @@ afterAll(() => {
});
describe("plugin loader CLI metadata", () => {
it.each([
{
id: "wrong-cli-channel-entry",
kind: "bundled-channel-entry",
error: "bundled channel entry requires setup-runtime loader",
},
{
id: "wrong-cli-channel-setup-entry",
kind: "bundled-channel-setup-entry",
error: "bundled channel setup entry requires setup-runtime loader",
},
])(
"reports $kind loaded through CLI metadata legacy plugin path",
async ({ id, kind, error }) => {
useNoBundledPlugins();
const plugin = writePlugin({
id,
filename: `${id}.cjs`,
body: `module.exports = { id: ${JSON.stringify(id)}, kind: ${JSON.stringify(kind)} };`,
});
const errors: string[] = [];
const registry = await loadOpenClawPluginCliRegistry({
cache: false,
logger: {
info: () => {},
warn: () => {},
error: (msg: string) => errors.push(msg),
debug: () => {},
},
config: {
plugins: {
load: { paths: [plugin.file] },
allow: [id],
},
},
});
const loaded = registry.plugins.find((entry) => entry.id === id);
expect(loaded?.status).toBe("error");
expect(loaded?.error).toBe(error);
expect(
registry.diagnostics.some(
(diag) => diag.level === "error" && diag.pluginId === id && diag.message === error,
),
).toBe(true);
expect(errors).toEqual([
`[plugins] ${id} ${error}; ensure plugin is loaded via bundled channel discovery, not legacy plugin loader`,
]);
},
);
it("suppresses trust warning logs during CLI metadata loads", async () => {
useNoBundledPlugins();
const stateDir = makeTempDir();

View File

@@ -3994,6 +3994,45 @@ module.exports = { id: "throws-after-import", register() {} };`,
expect(loaded?.error).toContain("export.default:object keys=default");
});
it.each([
{
id: "wrong-channel-entry",
kind: "bundled-channel-entry",
error: "bundled channel entry requires setup-runtime loader",
},
{
id: "wrong-channel-setup-entry",
kind: "bundled-channel-setup-entry",
error: "bundled channel setup entry requires setup-runtime loader",
},
])("reports $kind loaded through the legacy plugin loader", ({ id, kind, error }) => {
useNoBundledPlugins();
const plugin = writePlugin({
id,
filename: `${id}.cjs`,
body: `module.exports = { id: ${JSON.stringify(id)}, kind: ${JSON.stringify(kind)} };`,
});
const errors: string[] = [];
const registry = loadRegistryFromSinglePlugin({
plugin,
pluginConfig: {
allow: [id],
},
options: {
logger: createErrorLogger(errors),
},
});
const loaded = registry.plugins.find((entry) => entry.id === id);
expect(loaded?.status).toBe("error");
expect(loaded?.error).toBe(error);
expectRegistryErrorDiagnostic({ registry, pluginId: id, message: error });
expect(errors).toEqual([
`[plugins] ${id} ${error}; ensure plugin is loaded via bundled channel discovery, not legacy plugin loader`,
]);
});
it("handles single-plugin channel, context engine, and cli validation", () => {
useNoBundledPlugins();
const scenarios = [

View File

@@ -1439,6 +1439,20 @@ function resolvePluginModuleExport(moduleExport: unknown): {
return {};
}
function kindIncludes(kind: unknown, target: string): boolean {
return kind === target || (Array.isArray(kind) && kind.includes(target));
}
function formatBundledChannelWrongLoaderError(kind: unknown): string | null {
if (kindIncludes(kind, "bundled-channel-setup-entry")) {
return "bundled channel setup entry requires setup-runtime loader";
}
if (kindIncludes(kind, "bundled-channel-entry")) {
return "bundled channel entry requires setup-runtime loader";
}
return null;
}
function pushDiagnostics(diagnostics: PluginDiagnostic[], append: PluginDiagnostic[]) {
diagnostics.push(...append);
}
@@ -2340,8 +2354,16 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
}
if (typeof register !== "function") {
logger.error(`[plugins] ${record.id} missing register/activate export`);
pushPluginLoadError(formatMissingPluginRegisterError(mod, env));
const bundledChannelWrongLoaderError = formatBundledChannelWrongLoaderError(record.kind);
if (bundledChannelWrongLoaderError) {
logger.error(
`[plugins] ${record.id} ${bundledChannelWrongLoaderError}; ensure plugin is loaded via bundled channel discovery, not legacy plugin loader`,
);
pushPluginLoadError(bundledChannelWrongLoaderError);
} else {
logger.error(`[plugins] ${record.id} missing register/activate export`);
pushPluginLoadError(formatMissingPluginRegisterError(mod, env));
}
continue;
}
@@ -2793,8 +2815,16 @@ export async function loadOpenClawPluginCliRegistry(
}
if (typeof register !== "function") {
logger.error(`[plugins] ${record.id} missing register/activate export`);
pushPluginLoadError(formatMissingPluginRegisterError(mod, env));
const bundledChannelWrongLoaderError = formatBundledChannelWrongLoaderError(record.kind);
if (bundledChannelWrongLoaderError) {
logger.error(
`[plugins] ${record.id} ${bundledChannelWrongLoaderError}; ensure plugin is loaded via bundled channel discovery, not legacy plugin loader`,
);
pushPluginLoadError(bundledChannelWrongLoaderError);
} else {
logger.error(`[plugins] ${record.id} missing register/activate export`);
pushPluginLoadError(formatMissingPluginRegisterError(mod, env));
}
continue;
}