187 lines
5.5 KiB
TypeScript
187 lines
5.5 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}`);
|
||
|
|
},
|
||
|
|
},
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
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 });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|