mirror of https://github.com/openclaw/openclaw.git
331 lines
9.6 KiB
TypeScript
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 });
|
|
}
|
|
});
|
|
});
|