fix: add plugin load debug shape

This commit is contained in:
Peter Steinberger
2026-04-22 18:31:37 +01:00
parent 63776bc999
commit 9d27d09d47
3 changed files with 78 additions and 2 deletions

View File

@@ -294,6 +294,10 @@ openclaw plugins doctor
compatibility notices. When everything is clean it prints `No plugin issues
detected.`
For module-shape failures such as missing `register`/`activate` exports, rerun
with `OPENCLAW_PLUGIN_LOAD_DEBUG=1` to include a compact export-shape summary in
the diagnostic output.
### Marketplace
```bash

View File

@@ -3531,6 +3531,31 @@ module.exports = { id: "throws-after-import", register() {} };`,
).toBe(true);
});
it("can include plugin export shape when register is missing", () => {
useNoBundledPlugins();
const plugin = writePlugin({
id: "missing-register-shape",
filename: "missing-register-shape.cjs",
body: `module.exports = { default: { default: { id: "missing-register-shape" } } };`,
});
const registry = withEnv({ OPENCLAW_PLUGIN_LOAD_DEBUG: "1" }, () =>
loadRegistryFromSinglePlugin({
plugin,
pluginConfig: {
allow: ["missing-register-shape"],
},
}),
);
const loaded = registry.plugins.find((entry) => entry.id === "missing-register-shape");
expect(loaded?.status).toBe("error");
expect(loaded?.error).toContain("plugin export missing register/activate");
expect(loaded?.error).toContain("module shape:");
expect(loaded?.error).toContain("export:object keys=default");
expect(loaded?.error).toContain("export.default:object keys=default");
});
it("handles single-plugin channel, context engine, and cli validation", () => {
useNoBundledPlugins();
const scenarios = [

View File

@@ -1022,6 +1022,53 @@ function resolvePluginModuleExport(moduleExport: unknown): {
return {};
}
function isPluginLoadDebugEnabled(env: NodeJS.ProcessEnv): boolean {
const normalized = normalizeLowercaseStringOrEmpty(env.OPENCLAW_PLUGIN_LOAD_DEBUG);
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
}
function describePluginModuleExportShape(
value: unknown,
label = "export",
seen: Set<unknown> = new Set(),
): string[] {
if (value === null) {
return [`${label}:null`];
}
if (typeof value !== "object") {
return [`${label}:${typeof value}`];
}
if (seen.has(value)) {
return [`${label}:circular`];
}
seen.add(value);
const record = value as Record<string, unknown>;
const keys = Object.keys(record).toSorted();
const visibleKeys = keys.slice(0, 8);
const extraCount = keys.length - visibleKeys.length;
const keySummary =
visibleKeys.length > 0
? `${visibleKeys.join(",")}${extraCount > 0 ? `,+${extraCount}` : ""}`
: "none";
const details = [`${label}:object keys=${keySummary}`];
for (const key of ["default", "module", "register", "activate"]) {
if (Object.prototype.hasOwnProperty.call(record, key)) {
details.push(...describePluginModuleExportShape(record[key], `${label}.${key}`, seen));
}
}
return details;
}
function formatMissingPluginRegisterError(moduleExport: unknown, env: NodeJS.ProcessEnv): string {
const message = "plugin export missing register/activate";
if (!isPluginLoadDebugEnabled(env)) {
return message;
}
return `${message} (module shape: ${describePluginModuleExportShape(moduleExport).join("; ")})`;
}
function mergeChannelPluginSection<T>(
baseValue: T | undefined,
overrideValue: T | undefined,
@@ -2511,7 +2558,7 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
if (typeof register !== "function") {
logger.error(`[plugins] ${record.id} missing register/activate export`);
pushPluginLoadError("plugin export missing register/activate");
pushPluginLoadError(formatMissingPluginRegisterError(mod, env));
continue;
}
@@ -2934,7 +2981,7 @@ export async function loadOpenClawPluginCliRegistry(
if (typeof register !== "function") {
logger.error(`[plugins] ${record.id} missing register/activate export`);
pushPluginLoadError("plugin export missing register/activate");
pushPluginLoadError(formatMissingPluginRegisterError(mod, env));
continue;
}