fix(cli): route plugin packaging recovery hints

Route invalid-config recovery output for source-only installed plugin packages to plugin packaging guidance instead of openclaw doctor --fix.

Validated with focused config/CLI/gateway/plugin tests, autoreview, Crabbox/Testbox E2E tbx_01ksgr80tnvvc13kv6t126yv78, and green PR CI on 3b3ce73d0f.

Thanks @brokemac79.
This commit is contained in:
brokemac79
2026-05-26 01:13:20 +01:00
committed by GitHub
parent ea2496b00c
commit 56633e4f3c
14 changed files with 539 additions and 78 deletions

View File

@@ -3,6 +3,11 @@ import { note } from "../../terminal/note.js";
import { formatCliCommand } from "../command-format.js";
import { ensureConfigReady, testApi } from "./config-guard.js";
const pluginPackagingRecoveryHint = [
"This is a plugin packaging issue, not a local config problem.",
"Update or reinstall the plugin after the publisher ships compiled JavaScript, or disable/uninstall the plugin until then.",
].join("\n");
const loadAndMaybeMigrateDoctorConfigMock = vi.hoisted(() => vi.fn());
const readConfigFileSnapshotMock = vi.hoisted(() => vi.fn());
const setRuntimeConfigSnapshotMock = vi.hoisted(() => vi.fn());
@@ -23,6 +28,7 @@ function makeSnapshot() {
exists: false,
valid: true,
issues: [] as ConfigIssue[],
warnings: [] as ConfigIssue[],
legacyIssues: [] as ConfigIssue[],
path: "/tmp/openclaw.json",
};
@@ -42,20 +48,16 @@ function plainErrorCalls(runtime: ReturnType<typeof makeRuntime>): string[] {
async function withCapturedStdout(run: () => Promise<void>): Promise<string> {
const writes: string[] = [];
const writeSpy = vi
.spyOn(process.stdout, "write")
.mockImplementation(
((
chunk: unknown,
encodingOrCallback?: BufferEncoding | ((error?: Error | null) => void),
callback?: (error?: Error | null) => void,
) => {
writes.push(String(chunk));
const done = typeof encodingOrCallback === "function" ? encodingOrCallback : callback;
done?.();
return true;
}) as typeof process.stdout.write,
);
const writeSpy = vi.spyOn(process.stdout, "write").mockImplementation(((
chunk: unknown,
encodingOrCallback?: BufferEncoding | ((error?: Error | null) => void),
callback?: (error?: Error | null) => void,
) => {
writes.push(String(chunk));
const done = typeof encodingOrCallback === "function" ? encodingOrCallback : callback;
done?.();
return true;
}) as typeof process.stdout.write);
try {
await run();
return writes.join("");
@@ -185,6 +187,30 @@ describe("ensureConfigReady", () => {
expect(runtime.exit).toHaveBeenCalledWith(1);
});
it("replaces doctor fix advice for plugin packaging-only invalid config", async () => {
setInvalidSnapshot({
issues: [
{
path: "plugins.slots.memory",
message: "plugin not found: source-only-pack",
},
],
warnings: [
{
path: "plugins",
message:
"plugin source-only-pack: installed plugin package requires compiled runtime output for TypeScript entry index.ts: expected ./dist/index.js. This is a plugin packaging issue, not a local config problem.",
},
],
});
const runtime = await runEnsureConfigReady(["message"]);
const calls = plainErrorCalls(runtime);
expect(calls).toContain(`Fix: ${pluginPackagingRecoveryHint}`);
expect(calls).not.toContain(`Fix: ${formatCliCommand("openclaw doctor --fix")}`);
expect(runtime.exit).toHaveBeenCalledWith(1);
});
it("does not exit for invalid config on allowlisted commands", async () => {
setInvalidSnapshot({
issues: [{ path: "agents.defaults", message: 'Unrecognized key: "agentRuntime"' }],

View File

@@ -96,12 +96,19 @@ export async function ensureConfigReady(params: {
return;
}
const [{ colorize, isRich, theme }, { shortenHomePath }, { formatCliCommand }] =
await Promise.all([
import("../../terminal/theme.js"),
import("../../utils.js"),
import("../command-format.js"),
]);
const [
{ colorize, isRich, theme },
{ shortenHomePath },
{ formatCliCommand },
{ isPluginPackagingRuntimeOutputInvalidConfigSnapshot },
{ formatPluginPackagingRuntimeOutputRecoveryHint },
] = await Promise.all([
import("../../terminal/theme.js"),
import("../../utils.js"),
import("../command-format.js"),
import("../../config/recovery-policy.js"),
import("../config-recovery-hints.js"),
]);
const rich = isRich();
const muted = (value: string) => colorize(rich, theme.muted, value);
const error = (value: string) => colorize(rich, theme.error, value);
@@ -119,9 +126,10 @@ export async function ensureConfigReady(params: {
params.runtime.error(legacyIssues.map((issue) => ` ${error(issue)}`).join("\n"));
}
params.runtime.error("");
params.runtime.error(
`${muted("Fix:")} ${commandText(formatCliCommand("openclaw doctor --fix"))}`,
);
const fixHint = isPluginPackagingRuntimeOutputInvalidConfigSnapshot(snapshot)
? formatPluginPackagingRuntimeOutputRecoveryHint()
: commandText(formatCliCommand("openclaw doctor --fix"));
params.runtime.error(`${muted("Fix:")} ${fixHint}`);
params.runtime.error(
`${muted("Inspect:")} ${commandText(formatCliCommand("openclaw config validate"))}`,
);