fix(cli): report stale plugin doctor config

This commit is contained in:
BKF-Gitty
2026-05-13 22:40:21 +03:00
committed by Altay
parent 4d2e708726
commit 35819ce6c8
3 changed files with 90 additions and 9 deletions
+1 -1
View File
@@ -386,7 +386,7 @@ The `--json` flag outputs a machine-readable report suitable for scripting and a
openclaw plugins doctor
```
`doctor` reports plugin load errors, manifest/discovery diagnostics, and compatibility notices. When everything is clean it prints `No plugin issues detected.`
`doctor` reports plugin load errors, manifest/discovery diagnostics, compatibility notices, and stale plugin config references such as missing plugin slots. When the install tree and plugin config are clean it prints `No plugin issues detected.` If stale config remains but the install tree is otherwise healthy, the summary says so instead of implying full plugin health.
If a configured plugin is present on disk but blocked by the loader's path-safety checks, config validation keeps the plugin entry and reports it as `present but blocked`. Fix the preceding blocked-plugin diagnostic, such as path ownership or world-writable permissions, instead of removing the `plugins.entries.<id>` or `plugins.allow` config.
+56 -1
View File
@@ -6,6 +6,8 @@ import {
buildPluginRegistrySnapshotReport,
buildPluginSnapshotReport,
inspectPluginRegistry,
loadConfig,
readConfigFileSnapshot,
resetPluginsCliTestState,
refreshPluginRegistry,
runPluginsCommand,
@@ -79,10 +81,63 @@ describe("plugins cli list", () => {
await runPluginsCommand(["plugins", "doctor"]);
expect(buildPluginDiagnosticsReport).toHaveBeenCalledWith({ effectiveOnly: true });
expect(buildPluginDiagnosticsReport).toHaveBeenCalledWith({ config: {}, effectiveOnly: true });
expect(runtimeLogs).toContain("No plugin issues detected.");
});
it("reports stale plugin config in doctor output without claiming full plugin health", async () => {
const sourceConfig = {
plugins: {
allow: ["lossless-claw"],
entries: {
"lossless-claw": { enabled: true },
},
slots: {
contextEngine: "lossless-claw",
},
},
};
loadConfig.mockReturnValue({});
readConfigFileSnapshot.mockResolvedValueOnce({
path: "/tmp/openclaw-config.json5",
exists: true,
raw: "{}",
parsed: sourceConfig,
resolved: sourceConfig,
sourceConfig,
runtimeConfig: {},
config: {},
valid: true,
hash: "mock",
issues: [],
warnings: [],
legacyIssues: [],
});
buildPluginDiagnosticsReport.mockReturnValue({
plugins: [],
diagnostics: [],
});
await runPluginsCommand(["plugins", "doctor"]);
const output = runtimeLogs.join("\n");
expect(output).toContain("Plugin configuration:");
expect(output).toContain('plugins.allow: stale plugin reference "lossless-claw" was found.');
expect(output).toContain(
'plugins.entries.lossless-claw: stale plugin reference "lossless-claw" was found.',
);
expect(output).toContain(
'plugins.slots.contextEngine: slot references missing plugin "lossless-claw".',
);
expect(output).toContain(
'Run "openclaw doctor --fix" to remove stale plugin ids and dangling channel references.',
);
expect(output).toContain(
"No plugin install-tree issues detected; configuration warnings remain.",
);
expect(output).not.toContain("No plugin issues detected.");
});
it("reports config-selected plugin source shadowing in doctor output", async () => {
buildPluginDiagnosticsReport.mockReturnValue({
plugins: [
+33 -7
View File
@@ -365,20 +365,33 @@ export function registerPluginsCli(program: Command) {
buildPluginDiagnosticsReport,
formatPluginCompatibilityNotice,
} = await import("../plugins/status.js");
const report = buildPluginDiagnosticsReport({ effectiveOnly: true });
const {
collectStalePluginConfigWarnings,
isStalePluginAutoRepairBlocked,
scanStalePluginConfig,
} = await import("../commands/doctor/shared/stale-plugin-config.js");
const cfg = getRuntimeConfig();
const configSnapshot = await readConfigFileSnapshot().catch(() => null);
const sourceCfg = (configSnapshot?.sourceConfig ?? configSnapshot?.config ?? cfg) as
| OpenClawConfig
| undefined;
const report = buildPluginDiagnosticsReport({ config: cfg, effectiveOnly: true });
const errors = report.plugins.filter((p) => p.status === "error");
const diags = report.diagnostics.filter((d) => d.level === "error");
const shadowed = report.diagnostics.filter((entry) =>
isErroredConfigSelectedShadowDiagnostic({ entry, plugins: report.plugins }),
);
const compatibility = buildPluginCompatibilityNotices({ report });
const stalePluginConfigHits = scanStalePluginConfig(sourceCfg ?? cfg, process.env);
const stalePluginConfigWarnings = collectStalePluginConfigWarnings({
hits: stalePluginConfigHits,
doctorFixCommand: "openclaw doctor --fix",
autoRepairBlocked: isStalePluginAutoRepairBlocked(sourceCfg ?? cfg, process.env),
});
const hasInstallTreeIssues =
errors.length > 0 || diags.length > 0 || shadowed.length > 0 || compatibility.length > 0;
if (
errors.length === 0 &&
diags.length === 0 &&
shadowed.length === 0 &&
compatibility.length === 0
) {
if (!hasInstallTreeIssues && stalePluginConfigWarnings.length === 0) {
defaultRuntime.log("No plugin issues detected.");
return;
}
@@ -436,6 +449,19 @@ export function registerPluginsCli(program: Command) {
lines.push(`- ${formatPluginCompatibilityNotice(notice)} [${marker}]`);
}
}
if (stalePluginConfigWarnings.length > 0) {
if (lines.length > 0) {
lines.push("");
}
lines.push(theme.warn("Plugin configuration:"));
lines.push(...stalePluginConfigWarnings);
}
if (!hasInstallTreeIssues && stalePluginConfigWarnings.length > 0) {
if (lines.length > 0) {
lines.push("");
}
lines.push("No plugin install-tree issues detected; configuration warnings remain.");
}
const docs = formatDocsLink("/plugin", "docs.openclaw.ai/plugin");
lines.push("");
lines.push(`${theme.muted("Docs:")} ${docs}`);