| import { Command } from "commander"; |
| import fs from "node:fs/promises"; |
| import os from "node:os"; |
| import path from "node:path"; |
| import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; |
|
|
| |
| |
| |
| |
| |
| |
|
|
| const mockLog = vi.fn(); |
| const mockError = vi.fn(); |
| const mockExit = vi.fn((code: number) => { |
| const errorMessages = mockError.mock.calls.map((c) => c.join(" ")).join("; "); |
| throw new Error(`__exit__:${code} - ${errorMessages}`); |
| }); |
|
|
| vi.mock("../runtime.js", () => ({ |
| defaultRuntime: { |
| log: (...args: unknown[]) => mockLog(...args), |
| error: (...args: unknown[]) => mockError(...args), |
| exit: (code: number) => mockExit(code), |
| }, |
| })); |
|
|
| async function withTempHome(run: (home: string) => Promise<void>): Promise<void> { |
| const home = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-config-cli-")); |
| const originalEnv = { ...process.env }; |
| try { |
| |
| process.env.OPENCLAW_CONFIG_PATH = path.join(home, ".openclaw", "openclaw.json"); |
| await fs.mkdir(path.join(home, ".openclaw"), { recursive: true }); |
| await run(home); |
| } finally { |
| process.env = originalEnv; |
| await fs.rm(home, { recursive: true, force: true }); |
| } |
| } |
|
|
| async function readConfigFile(home: string): Promise<Record<string, unknown>> { |
| const configPath = path.join(home, ".openclaw", "openclaw.json"); |
| const content = await fs.readFile(configPath, "utf-8"); |
| return JSON.parse(content); |
| } |
|
|
| async function writeConfigFile(home: string, config: Record<string, unknown>): Promise<void> { |
| const configPath = path.join(home, ".openclaw", "openclaw.json"); |
| await fs.writeFile(configPath, JSON.stringify(config, null, 2)); |
| } |
|
|
| describe("config cli", () => { |
| beforeEach(() => { |
| vi.clearAllMocks(); |
| }); |
|
|
| afterEach(() => { |
| vi.restoreAllMocks(); |
| }); |
|
|
| describe("config set - issue #6070", () => { |
| it("preserves existing config keys when setting a new value", async () => { |
| await withTempHome(async (home) => { |
| |
| const initialConfig = { |
| agents: { |
| list: [{ id: "main" }, { id: "oracle", workspace: "~/oracle-workspace" }], |
| }, |
| gateway: { |
| port: 18789, |
| }, |
| tools: { |
| allow: ["group:fs"], |
| }, |
| logging: { |
| level: "debug", |
| }, |
| }; |
| await writeConfigFile(home, initialConfig); |
|
|
| |
| const { registerConfigCli } = await import("./config-cli.js"); |
| const program = new Command(); |
| program.exitOverride(); |
| registerConfigCli(program); |
|
|
| await program.parseAsync(["config", "set", "gateway.auth.mode", "token"], { from: "user" }); |
|
|
| |
| const finalConfig = await readConfigFile(home); |
|
|
| |
| expect((finalConfig.gateway as Record<string, unknown>).auth).toEqual({ mode: "token" }); |
|
|
| |
| |
| |
| expect(finalConfig.agents).not.toHaveProperty("defaults"); |
| expect((finalConfig.agents as Record<string, unknown>).list).toEqual( |
| initialConfig.agents.list, |
| ); |
| expect((finalConfig.gateway as Record<string, unknown>).port).toBe(18789); |
| expect(finalConfig.tools).toEqual(initialConfig.tools); |
| expect(finalConfig.logging).toEqual(initialConfig.logging); |
| }); |
| }); |
|
|
| it("does not inject runtime defaults into the written config", async () => { |
| await withTempHome(async (home) => { |
| |
| const initialConfig = { |
| gateway: { port: 18789 }, |
| }; |
| await writeConfigFile(home, initialConfig); |
|
|
| |
| const { registerConfigCli } = await import("./config-cli.js"); |
| const program = new Command(); |
| program.exitOverride(); |
| registerConfigCli(program); |
|
|
| await program.parseAsync(["config", "set", "gateway.auth.mode", "token"], { |
| from: "user", |
| }); |
|
|
| |
| const finalConfig = await readConfigFile(home); |
|
|
| |
| |
| expect(finalConfig).not.toHaveProperty("agents.defaults.model"); |
| expect(finalConfig).not.toHaveProperty("agents.defaults.contextWindow"); |
| expect(finalConfig).not.toHaveProperty("agents.defaults.maxTokens"); |
| expect(finalConfig).not.toHaveProperty("messages.ackReaction"); |
| expect(finalConfig).not.toHaveProperty("sessions.persistence"); |
|
|
| |
| expect((finalConfig.gateway as Record<string, unknown>).port).toBe(18789); |
| |
| expect((finalConfig.gateway as Record<string, unknown>).auth).toEqual({ mode: "token" }); |
| }); |
| }); |
| }); |
|
|
| describe("config unset - issue #6070", () => { |
| it("preserves existing config keys when unsetting a value", async () => { |
| await withTempHome(async (home) => { |
| |
| const initialConfig = { |
| agents: { list: [{ id: "main" }] }, |
| gateway: { port: 18789 }, |
| tools: { |
| profile: "coding", |
| alsoAllow: ["agents_list"], |
| }, |
| logging: { |
| level: "debug", |
| }, |
| }; |
| await writeConfigFile(home, initialConfig); |
|
|
| |
| const { registerConfigCli } = await import("./config-cli.js"); |
| const program = new Command(); |
| program.exitOverride(); |
| registerConfigCli(program); |
|
|
| await program.parseAsync(["config", "unset", "tools.alsoAllow"], { from: "user" }); |
|
|
| |
| const finalConfig = await readConfigFile(home); |
|
|
| |
| expect(finalConfig.tools as Record<string, unknown>).not.toHaveProperty("alsoAllow"); |
|
|
| |
| expect(finalConfig.agents).not.toHaveProperty("defaults"); |
| expect((finalConfig.agents as Record<string, unknown>).list).toEqual( |
| initialConfig.agents.list, |
| ); |
| expect(finalConfig.gateway).toEqual(initialConfig.gateway); |
| expect((finalConfig.tools as Record<string, unknown>).profile).toBe("coding"); |
| expect(finalConfig.logging).toEqual(initialConfig.logging); |
| }); |
| }); |
| }); |
| }); |
|
|