fix(health): activated plugin failures are silently omitted (#129161)

* fix(health): surface activated plugin and service failures

* test(health): use complete deep-status snapshot fixture
This commit is contained in:
Peter Steinberger
2026-08-25 01:53:54 -07:00
committed by GitHub
parent aa3e0ae429
commit b7d5e4fb66
4 changed files with 125 additions and 2 deletions
+48
View File
@@ -257,6 +257,54 @@ describe("formatHealthChannelLines", () => {
]);
});
it("surfaces activated plugin failures without promoting inactive load errors", () => {
const summary = createHealthSummary({ channels: {}, channelOrder: [], channelLabels: {} });
summary.plugins = {
loaded: ["calendar"],
errors: [
{
id: "calendar",
origin: "workspace",
activated: true,
failurePhase: "service",
error: "service scheduler: address already in use",
},
{
id: "inactive",
origin: "workspace",
activated: false,
failurePhase: "load",
error: "inactive plugin load failed",
},
],
};
expect(formatHealthChannelLines(summary)).toStrictEqual([
"Plugin calendar: failed - service scheduler: address already in use; run openclaw doctor",
]);
});
it("bounds activated plugin failure details and summarizes omitted failures", () => {
const summary = createHealthSummary({ channels: {}, channelOrder: [], channelLabels: {} });
summary.plugins = {
loaded: [],
errors: Array.from({ length: 22 }, (_, index) => ({
id: `plugin-${index}`,
origin: "workspace",
activated: true,
error: "x".repeat(600),
})),
};
const lines = formatHealthChannelLines(summary);
expect(lines).toHaveLength(21);
expect(lines[0]).toBe(`Plugin plugin-0: failed - ${"x".repeat(500)}; run openclaw doctor`);
expect(lines.at(-1)).toBe(
"Plugins: failed - 2 additional activated failures; run openclaw doctor",
);
});
it("formats iMessage probe failures as failed health lines", () => {
const summary = createHealthSummary({
channels: {
+12 -1
View File
@@ -139,7 +139,7 @@ const isProbeFailure = (summary: ChannelAccountHealthSummary): boolean => {
return ok === false;
};
/** Formats one terse health line per channel, optionally including every account. */
/** Formats terse channel and activated-plugin health lines for shared CLI surfaces. */
export const formatHealthChannelLines = (
summary: HealthSummary,
opts: {
@@ -269,5 +269,16 @@ export const formatHealthChannelLines = (
: "unknown";
lines.push(`${label}: ${passiveState}`);
}
const failedPlugins = (summary.plugins?.errors ?? []).filter((plugin) => plugin.activated);
for (const plugin of failedPlugins.slice(0, 20)) {
const id = sanitizeTerminalText(plugin.id).slice(0, 120);
const error = sanitizeTerminalText(plugin.error).slice(0, 500);
lines.push(`Plugin ${id}: failed - ${error}; run openclaw doctor`);
}
if (failedPlugins.length > 20) {
lines.push(
`Plugins: failed - ${failedPlugins.length - 20} additional activated failures; run openclaw doctor`,
);
}
return lines;
};
+24 -1
View File
@@ -170,7 +170,7 @@ describe("healthCommand", () => {
probeGatewayStatusMock.mockReset();
});
it("renders the gateway session path identically in JSON and text", async () => {
it("preserves plugin health in JSON while surfacing activated failures in text", async () => {
const agentSessions = {
path: "/tmp/sessions.json",
count: 1,
@@ -194,6 +194,24 @@ describe("healthCommand", () => {
},
sessions: agentSessions,
});
snapshot.plugins = {
loaded: ["calendar"],
errors: [
{
id: "calendar",
origin: "workspace",
activated: true,
failurePhase: "service",
error: "service scheduler: address already in use",
},
{
id: "inactive",
origin: "workspace",
activated: false,
error: "inactive plugin load failed",
},
],
};
callGatewayMock.mockResolvedValueOnce(snapshot);
await healthCommand({ json: true, timeoutMs: 5000, config: {} }, runtime as never);
@@ -204,6 +222,7 @@ describe("healthCommand", () => {
expect(parsed.channels.whatsapp?.linked).toBe(true);
expect(parsed.channels.telegram?.configured).toBe(true);
expect(parsed.sessions.count).toBe(1);
expect(parsed.plugins).toEqual(snapshot.plugins);
runtime.log.mockClear();
callGatewayMock.mockResolvedValueOnce(snapshot);
@@ -211,6 +230,10 @@ describe("healthCommand", () => {
const output = stripAnsi(runtime.log.mock.calls.map((call) => String(call[0])).join("\n"));
expect(output).toContain(`Session store (main): ${parsed.sessions.path}`);
expect(output).toContain(
"Plugin calendar: failed - service scheduler: address already in use; run openclaw doctor",
);
expect(output).not.toContain("inactive plugin load failed");
});
it("prints the gateway probe duration in text output", async () => {
@@ -1,5 +1,6 @@
// Status command section tests cover footer, health, and report section rendering.
import { describe, expect, it } from "vitest";
import { formatHealthChannelLines } from "./health-format.js";
import type { HealthSummary } from "./health.js";
import {
buildStatusFooterLines,
@@ -291,6 +292,46 @@ describe("status.command-sections", () => {
]);
});
it("marks activated plugin service failures as warnings in deep health rows", () => {
const health: HealthSummary = {
ok: true,
ts: 0,
durationMs: 42,
heartbeatSeconds: 60,
defaultAgentId: "main",
agents: [],
sessions: { path: "/tmp/sessions.json", count: 0, recent: [] },
channels: {},
channelOrder: [],
channelLabels: {},
plugins: {
loaded: ["calendar"],
errors: [
{
id: "calendar",
origin: "workspace",
activated: true,
failurePhase: "service",
error: "service scheduler: address already in use",
},
],
},
};
const rows = buildStatusHealthRows({
health,
formatHealthChannelLines,
ok: (value) => `ok(${value})`,
warn: (value) => `warn(${value})`,
muted: (value) => `muted(${value})`,
});
expect(rows).toContainEqual({
Item: "Plugin calendar",
Status: "warn(WARN)",
Detail: "failed - service scheduler: address already in use; run openclaw doctor",
});
});
it("adds degraded event-loop health to status rows", () => {
const rows = buildStatusHealthRows({
health: {