openclaw/src/cli/config-cli.integration.test.ts

331 lines
9.6 KiB
TypeScript

import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import JSON5 from "json5";
import { describe, expect, it } from "vitest";
import { clearConfigCache, clearRuntimeConfigSnapshot } from "../config/config.js";
import { captureEnv } from "../test-utils/env.js";
import { runConfigSet } from "./config-cli.js";
function createTestRuntime() {
const logs: string[] = [];
const errors: string[] = [];
return {
logs,
errors,
runtime: {
log: (...args: unknown[]) => logs.push(args.map((arg) => String(arg)).join(" ")),
error: (...args: unknown[]) => errors.push(args.map((arg) => String(arg)).join(" ")),
exit: (code: number) => {
throw new Error(`__exit__:${code}`);
},
},
};
}
function createExecDryRunBatch(params: { markerPath: string }) {
const response = JSON.stringify({
protocolVersion: 1,
values: {
dryrun_id: "ok",
},
});
const script = [
'const fs = require("node:fs");',
`fs.writeFileSync(${JSON.stringify(params.markerPath)}, "dryrun\\n", "utf8");`,
`process.stdout.write(${JSON.stringify(response)});`,
].join("");
return [
{
path: "secrets.providers.runner",
provider: {
source: "exec",
command: process.execPath,
args: ["-e", script],
allowInsecurePath: true,
},
},
{
path: "channels.discord.token",
ref: {
source: "exec",
provider: "runner",
id: "dryrun_id",
},
},
];
}
describe("config cli integration", () => {
it("supports batch-file dry-run and then writes real config changes", async () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-config-cli-int-"));
const configPath = path.join(tempDir, "openclaw.json");
const batchPath = path.join(tempDir, "batch.json");
const envSnapshot = captureEnv([
"OPENCLAW_CONFIG_PATH",
"OPENCLAW_TEST_FAST",
"DISCORD_BOT_TOKEN",
]);
try {
fs.writeFileSync(
configPath,
`${JSON.stringify(
{
gateway: { port: 18789 },
},
null,
2,
)}\n`,
"utf8",
);
fs.writeFileSync(
batchPath,
`${JSON.stringify(
[
{
path: "secrets.providers.default",
provider: { source: "env" },
},
{
path: "channels.discord.token",
ref: {
source: "env",
provider: "default",
id: "DISCORD_BOT_TOKEN",
},
},
],
null,
2,
)}\n`,
"utf8",
);
process.env.OPENCLAW_TEST_FAST = "1";
process.env.OPENCLAW_CONFIG_PATH = configPath;
process.env.DISCORD_BOT_TOKEN = "test-token";
clearConfigCache();
clearRuntimeConfigSnapshot();
const runtime = createTestRuntime();
const before = fs.readFileSync(configPath, "utf8");
await runConfigSet({
cliOptions: {
batchFile: batchPath,
dryRun: true,
},
runtime: runtime.runtime,
});
const afterDryRun = fs.readFileSync(configPath, "utf8");
expect(afterDryRun).toBe(before);
expect(runtime.errors).toEqual([]);
expect(runtime.logs.some((line) => line.includes("Dry run successful: 2 update(s)"))).toBe(
true,
);
await runConfigSet({
cliOptions: {
batchFile: batchPath,
},
runtime: runtime.runtime,
});
const afterWrite = JSON5.parse(fs.readFileSync(configPath, "utf8"));
expect(afterWrite.secrets?.providers?.default).toEqual({
source: "env",
});
expect(afterWrite.channels?.discord?.token).toEqual({
source: "env",
provider: "default",
id: "DISCORD_BOT_TOKEN",
});
} finally {
envSnapshot.restore();
clearConfigCache();
clearRuntimeConfigSnapshot();
fs.rmSync(tempDir, { recursive: true, force: true });
}
});
it("keeps file unchanged when real-file dry-run fails and reports JSON error payload", async () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-config-cli-int-fail-"));
const configPath = path.join(tempDir, "openclaw.json");
const envSnapshot = captureEnv([
"OPENCLAW_CONFIG_PATH",
"OPENCLAW_TEST_FAST",
"MISSING_TEST_SECRET",
]);
try {
fs.writeFileSync(
configPath,
`${JSON.stringify(
{
gateway: { port: 18789 },
secrets: {
providers: {
default: { source: "env" },
},
},
},
null,
2,
)}\n`,
"utf8",
);
process.env.OPENCLAW_TEST_FAST = "1";
process.env.OPENCLAW_CONFIG_PATH = configPath;
delete process.env.MISSING_TEST_SECRET;
clearConfigCache();
clearRuntimeConfigSnapshot();
const runtime = createTestRuntime();
const before = fs.readFileSync(configPath, "utf8");
await expect(
runConfigSet({
path: "channels.discord.token",
cliOptions: {
refProvider: "default",
refSource: "env",
refId: "MISSING_TEST_SECRET",
dryRun: true,
json: true,
},
runtime: runtime.runtime,
}),
).rejects.toThrow("__exit__:1");
const after = fs.readFileSync(configPath, "utf8");
expect(after).toBe(before);
expect(runtime.errors).toEqual([]);
const raw = runtime.logs.at(-1);
expect(raw).toBeTruthy();
const payload = JSON.parse(raw ?? "{}") as {
ok?: boolean;
checks?: { schema?: boolean; resolvability?: boolean };
errors?: Array<{ kind?: string; ref?: string }>;
};
expect(payload.ok).toBe(false);
expect(payload.checks?.resolvability).toBe(true);
expect(payload.errors?.some((entry) => entry.kind === "resolvability")).toBe(true);
expect(payload.errors?.some((entry) => entry.ref?.includes("MISSING_TEST_SECRET"))).toBe(
true,
);
} finally {
envSnapshot.restore();
clearConfigCache();
clearRuntimeConfigSnapshot();
fs.rmSync(tempDir, { recursive: true, force: true });
}
});
it("skips exec provider execution during dry-run by default", async () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-config-cli-int-exec-skip-"));
const configPath = path.join(tempDir, "openclaw.json");
const batchPath = path.join(tempDir, "batch.json");
const markerPath = path.join(tempDir, "marker.txt");
const envSnapshot = captureEnv(["OPENCLAW_CONFIG_PATH", "OPENCLAW_TEST_FAST"]);
try {
fs.writeFileSync(
configPath,
`${JSON.stringify(
{
gateway: { port: 18789 },
},
null,
2,
)}\n`,
"utf8",
);
fs.writeFileSync(
batchPath,
`${JSON.stringify(createExecDryRunBatch({ markerPath }), null, 2)}\n`,
"utf8",
);
process.env.OPENCLAW_TEST_FAST = "1";
process.env.OPENCLAW_CONFIG_PATH = configPath;
clearConfigCache();
clearRuntimeConfigSnapshot();
const runtime = createTestRuntime();
const before = fs.readFileSync(configPath, "utf8");
await runConfigSet({
cliOptions: {
batchFile: batchPath,
dryRun: true,
},
runtime: runtime.runtime,
});
const after = fs.readFileSync(configPath, "utf8");
expect(after).toBe(before);
expect(fs.existsSync(markerPath)).toBe(false);
expect(
runtime.logs.some((line) =>
line.includes("Dry run note: skipped 1 exec SecretRef resolvability check(s)."),
),
).toBe(true);
} finally {
envSnapshot.restore();
clearConfigCache();
clearRuntimeConfigSnapshot();
fs.rmSync(tempDir, { recursive: true, force: true });
}
});
it("executes exec providers during dry-run when --allow-exec is set", async () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-config-cli-int-exec-allow-"));
const configPath = path.join(tempDir, "openclaw.json");
const batchPath = path.join(tempDir, "batch.json");
const markerPath = path.join(tempDir, "marker.txt");
const envSnapshot = captureEnv(["OPENCLAW_CONFIG_PATH", "OPENCLAW_TEST_FAST"]);
try {
fs.writeFileSync(
configPath,
`${JSON.stringify(
{
gateway: { port: 18789 },
},
null,
2,
)}\n`,
"utf8",
);
fs.writeFileSync(
batchPath,
`${JSON.stringify(createExecDryRunBatch({ markerPath }), null, 2)}\n`,
"utf8",
);
process.env.OPENCLAW_TEST_FAST = "1";
process.env.OPENCLAW_CONFIG_PATH = configPath;
clearConfigCache();
clearRuntimeConfigSnapshot();
const runtime = createTestRuntime();
const before = fs.readFileSync(configPath, "utf8");
await runConfigSet({
cliOptions: {
batchFile: batchPath,
dryRun: true,
allowExec: true,
},
runtime: runtime.runtime,
});
const after = fs.readFileSync(configPath, "utf8");
expect(after).toBe(before);
expect(fs.existsSync(markerPath)).toBe(true);
expect(
runtime.logs.some((line) =>
line.includes("Dry run note: skipped 1 exec SecretRef resolvability check(s)."),
),
).toBe(false);
} finally {
envSnapshot.restore();
clearConfigCache();
clearRuntimeConfigSnapshot();
fs.rmSync(tempDir, { recursive: true, force: true });
}
});
});