Skip to content

Commit 1db2c2a

Browse files
Treat soft plugin repair warnings as nonfatal (#84431)
* Treat soft plugin repair warnings as nonfatal * fix: scope plugin repair convergence failures --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
1 parent eb417bc commit 1db2c2a

2 files changed

Lines changed: 96 additions & 1 deletion

File tree

src/cli/update-cli/post-core-plugin-convergence.test.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,64 @@ describe("runPostCorePluginConvergence", () => {
301301
]);
302302
});
303303

304+
it("marks convergence errored when repair reports failed plugin ids", async () => {
305+
mocks.repairMissingConfiguredPluginInstalls.mockResolvedValue({
306+
changes: [],
307+
warnings: [
308+
'Failed to install missing configured plugin "discord" from @openclaw/discord: ENETUNREACH.',
309+
],
310+
failedPluginIds: ["discord"],
311+
records: {},
312+
});
313+
const result = await runPostCorePluginConvergence({
314+
cfg: {
315+
plugins: { entries: { discord: { enabled: true } } },
316+
} as unknown as OpenClawConfig,
317+
env: {},
318+
});
319+
expect(result.errored).toBe(true);
320+
expect(result.warnings).toStrictEqual([
321+
{
322+
reason:
323+
'Failed to install missing configured plugin "discord" from @openclaw/discord: ENETUNREACH.',
324+
message:
325+
'Failed to install missing configured plugin "discord" from @openclaw/discord: ENETUNREACH.',
326+
guidance: ["Run `openclaw doctor --fix` to retry plugin repair."],
327+
},
328+
]);
329+
});
330+
331+
it("keeps inactive repair failures nonblocking", async () => {
332+
mocks.repairMissingConfiguredPluginInstalls.mockResolvedValue({
333+
changes: [],
334+
warnings: [
335+
'Failed to install missing configured plugin "discord" from @openclaw/discord: ENETUNREACH.',
336+
],
337+
failedPluginIds: ["discord"],
338+
records: {
339+
discord: {
340+
source: "npm",
341+
spec: "@acme/discord",
342+
installPath: "/p/discord",
343+
},
344+
},
345+
});
346+
const result = await runPostCorePluginConvergence({
347+
cfg: {
348+
plugins: {
349+
deny: ["discord"],
350+
entries: { discord: { enabled: true } },
351+
},
352+
} as unknown as OpenClawConfig,
353+
env: {},
354+
});
355+
expect(result.errored).toBe(false);
356+
expect(mocks.runPluginPayloadSmokeCheck).toHaveBeenCalledWith({
357+
records: {},
358+
env: expect.any(Object),
359+
});
360+
});
361+
304362
it("flags errored=true when smoke check finds a missing main entry", async () => {
305363
mocks.repairMissingConfiguredPluginInstalls.mockResolvedValue({
306364
changes: [],

src/cli/update-cli/post-core-plugin-convergence.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,35 @@ const REPAIR_GUIDANCE = "Run `openclaw doctor --fix` to retry plugin repair.";
4646
const inspectGuidance = (pluginId: string) =>
4747
`Run \`openclaw plugins inspect ${pluginId} --runtime --json\` for details.`;
4848

49+
function isBlockingRepairFailure(params: {
50+
cfg: OpenClawConfig;
51+
pluginId: string;
52+
records: Record<string, PluginInstallRecord>;
53+
smokeRecords: Record<string, PluginInstallRecord>;
54+
}): boolean {
55+
if (Object.hasOwn(params.smokeRecords, params.pluginId)) {
56+
return true;
57+
}
58+
const normalizedPluginConfig = normalizePluginsConfig(params.cfg.plugins);
59+
const enableState = resolveEffectiveEnableState({
60+
id: params.pluginId,
61+
origin: "global",
62+
config: normalizedPluginConfig,
63+
rootConfig: params.cfg,
64+
});
65+
if (enableState.enabled) {
66+
return true;
67+
}
68+
const record = params.records[params.pluginId];
69+
if (!record) {
70+
return false;
71+
}
72+
return Boolean(
73+
resolveTrustedSourceLinkedOfficialNpmSpec({ pluginId: params.pluginId, record }) ||
74+
resolveTrustedSourceLinkedOfficialClawHubSpec({ pluginId: params.pluginId, record }),
75+
);
76+
}
77+
4978
async function repairManagedNpmOpenClawPeerLinks(params: {
5079
env: NodeJS.ProcessEnv;
5180
}): Promise<{ changes: string[]; warnings: PostCoreConvergenceWarning[] }> {
@@ -136,6 +165,14 @@ export async function runPostCorePluginConvergence(params: {
136165
// configured plugin whose payload was deleted on disk would block the
137166
// entire update — even though the gateway will never load that plugin.
138167
const smokeRecords = filterRecordsToActive({ cfg: params.cfg, records });
168+
const blockingFailedPluginIds = (repair.failedPluginIds ?? []).filter((pluginId) =>
169+
isBlockingRepairFailure({
170+
cfg: params.cfg,
171+
pluginId,
172+
records,
173+
smokeRecords,
174+
}),
175+
);
139176
const smoke = await runPluginPayloadSmokeCheck({ records: smokeRecords, env });
140177
for (const failure of smoke.failures) {
141178
warnings.push({
@@ -155,7 +192,7 @@ export async function runPostCorePluginConvergence(params: {
155192
...peerLinkRepair.changes,
156193
],
157194
warnings,
158-
errored: smoke.failures.length > 0,
195+
errored: blockingFailedPluginIds.length > 0 || smoke.failures.length > 0,
159196
smokeFailures: smoke.failures,
160197
installRecords: records,
161198
};

0 commit comments

Comments
 (0)