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

@@ -19,6 +19,14 @@ const invalidConfigRecoveryHint = [
'Run "openclaw doctor --fix" to repair, then retry.',
"If startup is still blocked, inspect the adjacent .bak backup before restoring it manually.",
].join("\n");
const pluginPackagingRecoveryHints = [
"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.",
];
const pluginPackagingHintItems = pluginPackagingRecoveryHints.map((text) => ({
kind: "generic",
text,
}));
function expectLatestRuntimeJson(payload: unknown) {
const calls = defaultRuntime.writeJson.mock.calls;
@@ -47,6 +55,8 @@ function setConfigSnapshot(params: {
exists: boolean;
valid: boolean;
issues?: Array<{ path: string; message: string }>;
warnings?: Array<{ path: string; message: string }>;
legacyIssues?: Array<{ path: string; message: string }>;
lastTouchedVersion?: string;
}) {
const config = params.lastTouchedVersion
@@ -58,6 +68,28 @@ function setConfigSnapshot(params: {
config,
sourceConfig: config,
issues: params.issues ?? [],
warnings: params.warnings ?? [],
legacyIssues: params.legacyIssues ?? [],
});
}
function setPluginPackagingInvalidSnapshot() {
setConfigSnapshot({
exists: true,
valid: false,
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.",
},
],
});
}
@@ -107,6 +139,22 @@ describe("runServiceRestart config pre-flight (#35862)", () => {
});
});
it("points restart at plugin packaging recovery for packaging-only invalid config", async () => {
setPluginPackagingInvalidSnapshot();
await expect(runServiceRestart(createServiceRunArgs())).rejects.toThrow("__exit__:1");
expect(service.restart).not.toHaveBeenCalled();
expectLatestRuntimeJson({
action: "restart",
ok: false,
error: "Gateway restart blocked: plugins.slots.memory: plugin not found: source-only-pack",
hints: pluginPackagingRecoveryHints,
hintItems: pluginPackagingHintItems,
warnings: undefined,
});
});
it("blocks restart from an older binary when config was written by a newer one", async () => {
setConfigSnapshot({ exists: true, valid: true, lastTouchedVersion: "9999.1.1" });
@@ -185,6 +233,22 @@ describe("runServiceStart config pre-flight (#35862)", () => {
});
});
it("points start at plugin packaging recovery for packaging-only invalid config", async () => {
setPluginPackagingInvalidSnapshot();
await expect(runServiceStart(createServiceRunArgs())).rejects.toThrow("__exit__:1");
expect(service.restart).not.toHaveBeenCalled();
expectLatestRuntimeJson({
action: "start",
ok: false,
error: "Gateway start blocked: plugins.slots.memory: plugin not found: source-only-pack",
hints: pluginPackagingRecoveryHints,
hintItems: pluginPackagingHintItems,
warnings: undefined,
});
});
it("aborts before not-loaded start recovery when config is invalid", async () => {
const onNotLoaded = vi.fn(async () => ({
result: "started" as const,

View File

@@ -3,6 +3,7 @@ import { readBestEffortConfig, readConfigFileSnapshot } from "../../config/confi
import { resolveFutureConfigActionBlock } from "../../config/future-version-guard.js";
import { formatConfigIssueLines } from "../../config/issue-format.js";
import { resolveIsNixMode } from "../../config/paths.js";
import { isPluginPackagingRuntimeOutputInvalidConfigSnapshot } from "../../config/recovery-policy.js";
import { checkTokenDrift } from "../../daemon/service-audit.js";
import type { GatewayServiceRestartResult } from "../../daemon/service-types.js";
import type { GatewayServiceStartRepairIssue, GatewayServiceState } from "../../daemon/service.js";
@@ -19,7 +20,10 @@ import {
import { isWSL } from "../../infra/wsl.js";
import { defaultRuntime } from "../../runtime.js";
import { formatCliCommand } from "../command-format.js";
import { formatInvalidConfigRecoveryHint } from "../config-recovery-hints.js";
import {
formatInvalidConfigRecoveryHint,
formatPluginPackagingRuntimeOutputRecoveryHint,
} from "../config-recovery-hints.js";
import { resolveGatewayTokenForDriftCheck } from "./gateway-token-drift.js";
import {
buildDaemonServiceSnapshot,
@@ -139,6 +143,10 @@ type ConfigActionPreflightFailure = {
hints?: string[];
};
function formatPluginPackagingRuntimeOutputRecoveryHints(): string[] {
return formatPluginPackagingRuntimeOutputRecoveryHint().split("\n");
}
async function getConfigActionPreflightFailure(
action: string,
): Promise<ConfigActionPreflightFailure | null> {
@@ -146,11 +154,15 @@ async function getConfigActionPreflightFailure(
try {
snapshot = await readConfigFileSnapshot();
if (snapshot.exists && !snapshot.valid) {
const message =
snapshot.issues.length > 0
? formatConfigIssueLines(snapshot.issues, "", { normalizeRoot: true }).join("\n")
: "Unknown validation issue.";
return {
message:
snapshot.issues.length > 0
? formatConfigIssueLines(snapshot.issues, "", { normalizeRoot: true }).join("\n")
: "Unknown validation issue.",
message,
...(isPluginPackagingRuntimeOutputInvalidConfigSnapshot(snapshot)
? { hints: formatPluginPackagingRuntimeOutputRecoveryHints() }
: {}),
};
}
} catch {