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 }); } }); });