mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 17:53:39 +00:00
fix: explain declined config changes in the CLI (#157548)
Explain declined config saves and explicit validation corrections while preserving failure signaling and operational diagnostics. Refs #152252.
This commit is contained in:
parent
4a9b2de198
commit
ca6d427763
6 changed files with 111 additions and 5 deletions
|
|
@ -169,6 +169,8 @@ machine-output spelling and keeps stdout reserved for the schema document.
|
|||
|
||||
### `config validate`
|
||||
|
||||
Schema refusals from `config set`, `config patch`, and `config unset` explain the affected setting and confirm that no settings were saved. Correct the reported value or use `openclaw config schema` to inspect supported settings, then retry. These refusals still exit with status 1. Explicit validation reports settings that need correction without changing the file; `config validate --json` retains its `valid: false`, `error`, and `issues` fields for scripts.
|
||||
|
||||
Human validation diagnostics quote literal record keys, such as `agents.defaults.models["provider/model.v1"].alias`, instead of displaying the dot inside a key as nested traversal. Numeric array positions use brackets, such as `agents.entries.main.skills[0]`. The `issues[].path` field in `config validate --json` keeps its existing dot-joined representation.
|
||||
|
||||
Validates the current config against the active schema without starting the gateway. It also checks provider/source compatibility for every registry-declared SecretRef, including disabled plugin or channel configuration. This strict command can report an inactive mismatch that does not block normal Gateway startup, where SecretRef resolution remains limited to effectively active surfaces.
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { replaceConfigFile } from "../config/config.js";
|
|||
import { getDeferredPluginMigrationConfigFacts } from "../config/deferred-plugin-migration-config.js";
|
||||
import { AUTO_MANAGED_CONFIG_META_PATHS } from "../config/io.meta.js";
|
||||
import { coerceConfig } from "../config/io.read-helpers.js";
|
||||
import { isConfigValidationFailedError } from "../config/io.write-errors.js";
|
||||
import { prepareConfigWriteValues } from "../config/io.write-prepare.js";
|
||||
import { prepareConfigWriteTopology } from "../config/io.write-topology.js";
|
||||
import { ConfigMutationConflictError } from "../config/mutation-conflict.js";
|
||||
|
|
@ -22,6 +23,7 @@ import type { RuntimeEnv } from "../runtime.js";
|
|||
import { ExitError, writeRuntimeJson } from "../runtime.js";
|
||||
import { toDotPath } from "../shared/dot-path.js";
|
||||
import { parseConfigPathArrayIndex } from "../shared/path-array-index.js";
|
||||
import { formatCliCommand } from "./command-format.js";
|
||||
import {
|
||||
formatPluginInstallConfigSetError,
|
||||
type ConfigMutationOptions,
|
||||
|
|
@ -624,6 +626,14 @@ export function handleConfigMutationError(params: {
|
|||
params.runtime.error(danger(message));
|
||||
exitCliAfterOutput(params.runtime, 1);
|
||||
}
|
||||
params.runtime.error(danger(message));
|
||||
if (isConfigValidationFailedError(params.err)) {
|
||||
params.runtime.error("Config change declined. No settings were saved.");
|
||||
params.runtime.error(message);
|
||||
params.runtime.error(
|
||||
`Correct the setting above and retry. Run ${formatCliCommand("openclaw config schema")} to inspect supported settings and values.`,
|
||||
);
|
||||
} else {
|
||||
params.runtime.error(danger(message));
|
||||
}
|
||||
exitCliAfterOutput(params.runtime, 1);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -488,6 +488,10 @@ describe("config cli integration", () => {
|
|||
code: 1,
|
||||
});
|
||||
const diagnostic = registeredRuntimeErrors.join("\n");
|
||||
if (args[1] === "validate") {
|
||||
expect(diagnostic).toContain("Config needs correction:");
|
||||
expect(diagnostic).toContain("openclaw config schema");
|
||||
}
|
||||
expect(diagnostic).toContain(`openclaw.json:9 — ${displayPath}:`);
|
||||
expect(diagnostic).toContain("expected string");
|
||||
expect(diagnostic).not.toContain(`${issuePath}:`);
|
||||
|
|
|
|||
88
src/cli/config-cli.rejections.integration.test.ts
Normal file
88
src/cli/config-cli.rejections.integration.test.ts
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import JSON5 from "json5";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
ConfigWritePostCommitError,
|
||||
createConfigValidationFailedError,
|
||||
} from "../config/io.write-errors.js";
|
||||
import { useConfigCliIntegrationHarness } from "./config-cli.integration.test-harness.js";
|
||||
|
||||
const configRuntime = await import("../config/config.js");
|
||||
const {
|
||||
registeredRuntimeLogs,
|
||||
registeredRuntimeErrors,
|
||||
runRegisteredConfigCommand,
|
||||
withConfigFileHarness,
|
||||
} = useConfigCliIntegrationHarness();
|
||||
|
||||
describe("config CLI rejections", () => {
|
||||
it("explains rejected settings without saving and accepts their correction", async () => {
|
||||
const setting = "channels.discord.guilds.123456789012345678.requireMention";
|
||||
const raw =
|
||||
'{"channels":{"discord":{"guilds":{"123456789012345678":{"requireMention":true}}}}}\n';
|
||||
await withConfigFileHarness(
|
||||
"openclaw-config-cli-refusal-",
|
||||
raw,
|
||||
async ({ configPath, tempDir }) => {
|
||||
const patchPath = path.join(tempDir, "patch.json");
|
||||
fs.writeFileSync(
|
||||
patchPath,
|
||||
'{"channels":{"discord":{"guilds":{"123456789012345678":{"requireMention":42}}}}}',
|
||||
);
|
||||
for (const [args, issue] of [
|
||||
[["set", setting, "oops"], "requireMention"],
|
||||
[["set", "gateway.nonexistentSetting", "true"], "nonexistentSetting"],
|
||||
[["patch", "--file", patchPath], "requireMention"],
|
||||
] as const) {
|
||||
registeredRuntimeErrors.length = 0;
|
||||
await expect(runRegisteredConfigCommand(["config", ...args])).rejects.toMatchObject({
|
||||
name: "ExitError",
|
||||
code: 1,
|
||||
});
|
||||
const output = registeredRuntimeErrors.join("\n");
|
||||
expect(output).toContain("Config change declined. No settings were saved.");
|
||||
expect(output).toContain(issue);
|
||||
expect(output).toContain("Correct the setting above and retry.");
|
||||
expect(output).toContain("openclaw config schema");
|
||||
expect(output).not.toMatch(/Stack:|Debug:|CLI failed|\bat .*\.ts:\d/);
|
||||
expect(registeredRuntimeLogs).toEqual([]);
|
||||
expect(fs.readFileSync(configPath, "utf8")).toBe(raw);
|
||||
expect(fs.existsSync(`${configPath}.bak`)).toBe(false);
|
||||
}
|
||||
registeredRuntimeErrors.length = 0;
|
||||
await runRegisteredConfigCommand(["config", "set", setting, "false"]);
|
||||
expect(
|
||||
JSON5.parse(fs.readFileSync(configPath, "utf8")).channels.discord.guilds[
|
||||
"123456789012345678"
|
||||
].requireMention,
|
||||
).toBe(false);
|
||||
expect(registeredRuntimeErrors).toEqual([]);
|
||||
expect(registeredRuntimeLogs.join("\n")).toContain("Updated");
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
new Error("Config validation failed: unexpected write failure"),
|
||||
new ConfigWritePostCommitError({
|
||||
configPath: "/tmp/openclaw.json",
|
||||
rollbackStatus: "unknown",
|
||||
cause: createConfigValidationFailedError([
|
||||
{ path: "gateway.port", message: "late validation failure" },
|
||||
]),
|
||||
}),
|
||||
])("does not relabel operational failures as unsaved settings: %s", async (error) => {
|
||||
await withConfigFileHarness("openclaw-config-cli-operational-", "{}", async () => {
|
||||
vi.spyOn(configRuntime, "replaceConfigFile").mockRejectedValueOnce(error);
|
||||
await expect(
|
||||
runRegisteredConfigCommand(["config", "set", "gateway.port", "19000"]),
|
||||
).rejects.toMatchObject({ name: "ExitError", code: 1 });
|
||||
const output = registeredRuntimeErrors.join("\n");
|
||||
expect(output).toContain(error.message);
|
||||
expect(output).not.toContain("No settings were saved");
|
||||
expect(output).not.toContain("Correct the setting above");
|
||||
expect(registeredRuntimeLogs).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1612,7 +1612,7 @@ describe("config cli", () => {
|
|||
|
||||
await expect(runConfigCommand(["config", "validate"])).rejects.toThrow(ExitError);
|
||||
|
||||
expectErrorIncludes("config is invalid");
|
||||
expectErrorIncludes("Config needs correction:");
|
||||
expectErrorIncludes("agents.defaults.unknownOption");
|
||||
expect(mockLog).not.toHaveBeenCalled();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -262,15 +262,17 @@ async function runConfigValidate(opts: { json?: boolean; runtime?: RuntimeEnv }
|
|||
issues,
|
||||
});
|
||||
} else {
|
||||
runtime.error(danger(`OpenClaw config is invalid: ${shortPath}`));
|
||||
for (const line of renderConfigValidationIssueLines(snapshot, danger("×"))) {
|
||||
runtime.error(`Config needs correction: ${shortPath}`);
|
||||
for (const line of renderConfigValidationIssueLines(snapshot, "-")) {
|
||||
runtime.error(` ${line}`);
|
||||
}
|
||||
runtime.error("");
|
||||
runtime.error(
|
||||
formatInvalidConfigRepairHint(snapshot, "to repair, or fix the keys above manually."),
|
||||
);
|
||||
runtime.error(`Inspect with ${formatCliCommand("openclaw config validate")}.`);
|
||||
runtime.error(
|
||||
`Run ${formatCliCommand("openclaw config schema")} to inspect supported settings and values, then rerun ${formatCliCommand("openclaw config validate")}.`,
|
||||
);
|
||||
}
|
||||
exitCliAfterOutput(runtime, 1);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue