CLI: add config validate tests and docs

This commit is contained in:
Gustavo Madeira Santana
2026-03-01 23:25:41 -05:00
parent f3b8104bba
commit 6f0089935b
4 changed files with 116 additions and 5 deletions

View File

@@ -1,5 +1,5 @@
---
summary: "CLI reference for `openclaw config` (get/set/unset values and config file path)"
summary: "CLI reference for `openclaw config` (get/set/unset/file/validate)"
read_when:
- You want to read or edit config non-interactively
title: "config"
@@ -7,8 +7,8 @@ title: "config"
# `openclaw config`
Config helpers: get/set/unset values by path and print the active config file.
Run without a subcommand to open
Config helpers: get/set/unset/validate values by path and print the active
config file. Run without a subcommand to open
the configure wizard (same as `openclaw configure`).
## Examples
@@ -20,6 +20,8 @@ openclaw config set browser.executablePath "/usr/bin/google-chrome"
openclaw config set agents.defaults.heartbeat.every "2h"
openclaw config set agents.list[0].tools.exec.node "node-id-or-name"
openclaw config unset tools.web.search.apiKey
openclaw config validate
openclaw config validate --json
```
## Paths
@@ -54,3 +56,13 @@ openclaw config set channels.whatsapp.groups '["*"]' --strict-json
- `config file`: Print the active config file path (resolved from `OPENCLAW_CONFIG_PATH` or default location).
Restart the gateway after edits.
## Validate
Validate the current config against the active schema without starting the
gateway.
```bash
openclaw config validate
openclaw config validate --json
```

View File

@@ -380,7 +380,7 @@ Interactive configuration wizard (models, channels, skills, gateway).
### `config`
Non-interactive config helpers (get/set/unset/file). Running `openclaw config` with no
Non-interactive config helpers (get/set/unset/file/validate). Running `openclaw config` with no
subcommand launches the wizard.
Subcommands:
@@ -389,6 +389,8 @@ Subcommands:
- `config set <path> <value>`: set a value (JSON5 or raw string).
- `config unset <path>`: remove a value.
- `config file`: print the active config file path.
- `config validate`: validate the current config against the schema without starting the gateway.
- `config validate --json`: emit machine-readable JSON output.
### `doctor`

View File

@@ -56,6 +56,10 @@ function setSnapshot(resolved: OpenClawConfig, config: OpenClawConfig) {
mockReadConfigFileSnapshot.mockResolvedValueOnce(buildSnapshot({ resolved, config }));
}
function setSnapshotOnce(snapshot: ConfigFileSnapshot) {
mockReadConfigFileSnapshot.mockResolvedValueOnce(snapshot);
}
let registerConfigCli: typeof import("./config-cli.js").registerConfigCli;
async function runConfigCommand(args: string[]) {
@@ -178,6 +182,99 @@ describe("config cli", () => {
});
});
describe("config validate", () => {
it("prints success and exits 0 when config is valid", async () => {
const resolved: OpenClawConfig = {
gateway: { port: 18789 },
};
setSnapshot(resolved, resolved);
await runConfigCommand(["config", "validate"]);
expect(mockExit).not.toHaveBeenCalled();
expect(mockError).not.toHaveBeenCalled();
expect(mockLog).toHaveBeenCalledWith(expect.stringContaining("Config valid:"));
});
it("prints issues and exits 1 when config is invalid", async () => {
setSnapshotOnce({
path: "/tmp/openclaw.json",
exists: true,
raw: "{}",
parsed: {},
resolved: {},
valid: false,
config: {},
issues: [
{
path: "agents.defaults.suppressToolErrorWarnings",
message: "Unrecognized key(s) in object",
},
],
warnings: [],
legacyIssues: [],
});
await expect(runConfigCommand(["config", "validate"])).rejects.toThrow("__exit__:1");
expect(mockError).toHaveBeenCalledWith(expect.stringContaining("Config invalid at"));
expect(mockError).toHaveBeenCalledWith(
expect.stringContaining("agents.defaults.suppressToolErrorWarnings"),
);
expect(mockLog).not.toHaveBeenCalled();
});
it("returns machine-readable JSON with --json for invalid config", async () => {
setSnapshotOnce({
path: "/tmp/openclaw.json",
exists: true,
raw: "{}",
parsed: {},
resolved: {},
valid: false,
config: {},
issues: [{ path: "gateway.bind", message: "Invalid enum value" }],
warnings: [],
legacyIssues: [],
});
await expect(runConfigCommand(["config", "validate", "--json"])).rejects.toThrow(
"__exit__:1",
);
const raw = mockLog.mock.calls.at(0)?.[0];
expect(typeof raw).toBe("string");
const payload = JSON.parse(String(raw)) as {
valid: boolean;
path: string;
issues: Array<{ path: string; message: string }>;
};
expect(payload.valid).toBe(false);
expect(payload.path).toContain("openclaw.json");
expect(payload.issues).toEqual([{ path: "gateway.bind", message: "Invalid enum value" }]);
expect(mockError).not.toHaveBeenCalled();
});
it("prints file-not-found and exits 1 when config file is missing", async () => {
setSnapshotOnce({
path: "/tmp/openclaw.json",
exists: false,
raw: null,
parsed: {},
resolved: {},
valid: true,
config: {},
issues: [],
warnings: [],
legacyIssues: [],
});
await expect(runConfigCommand(["config", "validate"])).rejects.toThrow("__exit__:1");
expect(mockError).toHaveBeenCalledWith(expect.stringContaining("Config file not found:"));
expect(mockLog).not.toHaveBeenCalled();
});
});
describe("config set parsing flags", () => {
it("falls back to raw string when parsing fails and strict mode is off", async () => {
const resolved: OpenClawConfig = { gateway: { port: 18789 } };

View File

@@ -83,7 +83,7 @@ const coreEntries: CoreCliEntry[] = [
{
name: "config",
description:
"Non-interactive config helpers (get/set/unset/file). Default: starts setup wizard.",
"Non-interactive config helpers (get/set/unset/file/validate). Default: starts setup wizard.",
hasSubcommands: true,
},
],