plugins: isolate setup-entry loader failures

This commit is contained in:
Mason Huang
2026-04-14 19:20:20 +08:00
parent 46f631c8c6
commit 2958b03d0a
2 changed files with 94 additions and 5 deletions

View File

@@ -3256,6 +3256,75 @@ module.exports = {
expect(registry.channels).toHaveLength(expectedChannels);
});
it("isolates loadSetupPlugin errors as per-plugin diagnostics instead of crashing registry load", () => {
useNoBundledPlugins();
const pluginDir = makeTempDir();
// Plugin whose setup-entry uses the bundled contract but loadSetupPlugin() throws
fs.writeFileSync(
path.join(pluginDir, "package.json"),
JSON.stringify(
{
name: "@openclaw/setup-entry-throws-test",
openclaw: {
extensions: ["./index.cjs"],
setupEntry: "./setup-entry.cjs",
},
},
null,
2,
),
"utf-8",
);
fs.writeFileSync(
path.join(pluginDir, "openclaw.plugin.json"),
JSON.stringify(
{
id: "setup-entry-throws-test",
configSchema: EMPTY_PLUGIN_SCHEMA,
channels: ["setup-entry-throws-test"],
},
null,
2,
),
"utf-8",
);
// index.cjs: full entry (should NOT be reached if setup-entry is used)
fs.writeFileSync(
path.join(pluginDir, "index.cjs"),
`module.exports = { id: "setup-entry-throws-test", register() {} };`,
"utf-8",
);
// setup-entry.cjs: bundled contract whose loadSetupPlugin throws
fs.writeFileSync(
path.join(pluginDir, "setup-entry.cjs"),
`module.exports = {
kind: "bundled-channel-setup-entry",
loadSetupPlugin: () => { throw new Error("boom: setup plugin missing"); },
};`,
"utf-8",
);
const registry = loadOpenClawPlugins({
cache: false,
config: {
plugins: {
load: { paths: [pluginDir] },
allow: ["setup-entry-throws-test"],
},
},
});
// The registry load should NOT crash; the error should be recorded as a
// per-plugin diagnostic rather than aborting the whole load.
expect(registry.diagnostics.length).toBeGreaterThanOrEqual(1);
const diagnostic = registry.diagnostics.find(
(d) => d.pluginId === "setup-entry-throws-test" && d.level === "error",
);
expect(diagnostic).toBeDefined();
expect(diagnostic!.message).toContain("failed to load setup entry");
});
it("prefers setupEntry for configured channel loads during startup when opted in", () => {
expect(
__testing.shouldLoadChannelPluginInSetupRuntime({

View File

@@ -646,6 +646,7 @@ function resolvePluginModuleExport(moduleExport: unknown): {
function resolveSetupChannelRegistration(moduleExport: unknown): {
plugin?: ChannelPlugin;
loadError?: unknown;
} {
const resolved = unwrapDefaultModuleExport(moduleExport);
if (!resolved || typeof resolved !== "object") {
@@ -659,11 +660,15 @@ function resolveSetupChannelRegistration(moduleExport: unknown): {
setupEntryRecord.kind === "bundled-channel-setup-entry" &&
typeof setupEntryRecord.loadSetupPlugin === "function"
) {
const loadedPlugin = setupEntryRecord.loadSetupPlugin();
if (loadedPlugin && typeof loadedPlugin === "object") {
return {
plugin: loadedPlugin as ChannelPlugin,
};
try {
const loadedPlugin = setupEntryRecord.loadSetupPlugin();
if (loadedPlugin && typeof loadedPlugin === "object") {
return {
plugin: loadedPlugin as ChannelPlugin,
};
}
} catch (err) {
return { loadError: err };
}
}
const setup = resolved as {
@@ -1650,6 +1655,21 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
manifestRecord.setupSource
) {
const setupRegistration = resolveSetupChannelRegistration(mod);
if (setupRegistration.loadError) {
recordPluginError({
logger,
registry,
record,
seenIds,
pluginId,
origin: candidate.origin,
phase: "load",
error: setupRegistration.loadError,
logPrefix: `[plugins] ${record.id} failed to load setup entry from ${record.source}: `,
diagnosticMessagePrefix: "failed to load setup entry: ",
});
continue;
}
if (setupRegistration.plugin) {
if (setupRegistration.plugin.id && setupRegistration.plugin.id !== record.id) {
pushPluginLoadError(