CLI: fix config set dry-run coverage gaps

This commit is contained in:
joshavant 2026-03-17 18:28:46 -05:00
parent ffe24955c8
commit ab5aec137c
No known key found for this signature in database
GPG Key ID: 4463B60B0DD49BC4
2 changed files with 86 additions and 7 deletions

View File

@ -513,6 +513,26 @@ describe("config cli", () => {
);
});
it("logs a dry-run note when value mode performs no validation checks", async () => {
const resolved: OpenClawConfig = {
gateway: { port: 18789 },
};
setSnapshot(resolved, resolved);
await runConfigCommand(["config", "set", "gateway.port", "19001", "--dry-run"]);
expect(mockWriteConfigFile).not.toHaveBeenCalled();
expect(mockResolveSecretRefValue).not.toHaveBeenCalled();
expect(mockLog).toHaveBeenCalledWith(
expect.stringContaining(
"Dry run note: value mode does not run schema/resolvability checks.",
),
);
expect(mockLog).toHaveBeenCalledWith(
expect.stringContaining("Dry run successful: 1 update(s) validated"),
);
});
it("supports batch mode for refs/providers in dry-run", async () => {
const resolved: OpenClawConfig = {
gateway: { port: 18789 },
@ -862,6 +882,56 @@ describe("config cli", () => {
);
expect(mockError).toHaveBeenCalledWith(expect.stringContaining("provider mismatch"));
});
it("fails dry-run for nested provider edits that make existing refs unresolvable", async () => {
const resolved: OpenClawConfig = {
gateway: { port: 18789 },
secrets: {
providers: {
vaultfile: { source: "file", path: "/tmp/secrets.json", mode: "json" },
},
},
tools: {
web: {
search: {
enabled: true,
apiKey: {
source: "file",
provider: "vaultfile",
id: "/providers/search/apiKey",
},
},
},
} as never,
};
setSnapshot(resolved, resolved);
mockResolveSecretRefValue.mockImplementationOnce(async () => {
throw new Error("provider mismatch");
});
await expect(
runConfigCommand([
"config",
"set",
"secrets.providers.vaultfile.path",
'"/tmp/other-secrets.json"',
"--strict-json",
"--dry-run",
]),
).rejects.toThrow("__exit__:1");
expect(mockResolveSecretRefValue).toHaveBeenCalledWith(
expect.objectContaining({
provider: "vaultfile",
id: "/providers/search/apiKey",
}),
expect.any(Object),
);
expect(mockError).toHaveBeenCalledWith(
expect.stringContaining("Dry run failed: 1 SecretRef assignment(s) could not be resolved."),
);
expect(mockError).toHaveBeenCalledWith(expect.stringContaining("provider mismatch"));
});
});
describe("path hardening", () => {

View File

@ -42,6 +42,7 @@ import type {
ConfigSetDryRunResult,
} from "./config-set-dryrun.js";
import {
hasBatchMode,
hasProviderBuilderOptions,
hasRefBuilderOptions,
parseBatchSource,
@ -593,7 +594,7 @@ function buildRefAssignmentOperation(params: {
function parseProviderAliasFromTargetPath(path: PathSegment[]): string | null {
if (
path.length === 3 &&
path.length >= 3 &&
path[0] === SECRET_PROVIDER_PATH_PREFIX[0] &&
path[1] === SECRET_PROVIDER_PATH_PREFIX[1]
) {
@ -857,12 +858,9 @@ export async function runConfigSet(opts: {
}) {
const runtime = opts.runtime ?? defaultRuntime;
try {
const hasBatchMode = Boolean(
(opts.cliOptions.batchJson && opts.cliOptions.batchJson.trim().length > 0) ||
(opts.cliOptions.batchFile && opts.cliOptions.batchFile.trim().length > 0),
);
const isBatchMode = hasBatchMode(opts.cliOptions);
const modeResolution = resolveConfigSetMode({
hasBatchMode,
hasBatchMode: isBatchMode,
hasRefBuilderOptions: hasRefBuilderOptions(opts.cliOptions),
hasProviderBuilderOptions: hasProviderBuilderOptions(opts.cliOptions),
strictJson: Boolean(opts.cliOptions.strictJson || opts.cliOptions.json),
@ -938,6 +936,13 @@ export async function runConfigSet(opts: {
if (opts.cliOptions.json) {
runtime.log(JSON.stringify(dryRunResult, null, 2));
} else {
if (!dryRunResult.checks.schema && !dryRunResult.checks.resolvability) {
runtime.log(
info(
"Dry run note: value mode does not run schema/resolvability checks. Use --strict-json, builder flags, or batch mode to enable validation checks.",
),
);
}
runtime.log(
info(
`Dry run successful: ${operations.length} update(s) validated against ${shortenHomePath(snapshot.path)}.`,
@ -1126,7 +1131,11 @@ export function registerConfigCli(program: Command) {
.argument("[value]", "Value (JSON5 or raw string)")
.option("--strict-json", "Strict JSON5 parsing (error instead of raw string fallback)", false)
.option("--json", "Legacy alias for --strict-json", false)
.option("--dry-run", "Validate changes without writing openclaw.json", false)
.option(
"--dry-run",
"Validate changes without writing openclaw.json (checks run in builder/json/batch modes)",
false,
)
.option("--ref-provider <alias>", "SecretRef builder: provider alias")
.option("--ref-source <source>", "SecretRef builder: source (env|file|exec)")
.option("--ref-id <id>", "SecretRef builder: ref id")