From 2174277ef0b978f0fa78f807e5203abff072ea2c Mon Sep 17 00:00:00 2001 From: can1357 Date: Thu, 26 Feb 2026 12:43:08 +0100 Subject: [PATCH] feat(coding-agent): added JSON syntax support for array and record config values - Added support for setting array and record configuration values using JSON syntax. - Implemented JSON parsing and validation for array and record types in config CLI. - Added 2 integration tests covering array and record configuration workflows. - Enhanced test setup with temporary directory management and settings reset for isolation. --- packages/coding-agent/CHANGELOG.md | 3 + packages/coding-agent/src/cli/config-cli.ts | 26 +++++++++ packages/coding-agent/test/config-cli.test.ts | 57 ++++++++++++++++++- 3 files changed, 84 insertions(+), 2 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 76ca7ab53..b68d7b701 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,6 +1,9 @@ # Changelog ## [Unreleased] +### Added + +- Added support for setting array and record configuration values using JSON syntax ### Changed diff --git a/packages/coding-agent/src/cli/config-cli.ts b/packages/coding-agent/src/cli/config-cli.ts index 9e724b5eb..855609ba5 100644 --- a/packages/coding-agent/src/cli/config-cli.ts +++ b/packages/coding-agent/src/cli/config-cli.ts @@ -195,6 +195,32 @@ function parseAndSetValue(path: SettingPath, rawValue: string): void { parsedValue = trimmed; break; } + case "array": { + let parsed: unknown; + try { + parsed = JSON.parse(trimmed); + } catch { + throw new Error(`Invalid array JSON: ${rawValue}`); + } + if (!Array.isArray(parsed)) { + throw new Error(`Invalid array JSON: ${rawValue}`); + } + parsedValue = parsed; + break; + } + case "record": { + let parsed: unknown; + try { + parsed = JSON.parse(trimmed); + } catch { + throw new Error(`Invalid record JSON: ${rawValue}`); + } + if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) { + throw new Error(`Invalid record JSON: ${rawValue}`); + } + parsedValue = parsed; + break; + } default: parsedValue = trimmed; } diff --git a/packages/coding-agent/test/config-cli.test.ts b/packages/coding-agent/test/config-cli.test.ts index 28bffb79e..fa76b3784 100644 --- a/packages/coding-agent/test/config-cli.test.ts +++ b/packages/coding-agent/test/config-cli.test.ts @@ -1,8 +1,31 @@ -import { afterEach, describe, expect, it, vi } from "bun:test"; +import { afterEach, beforeEach, describe, expect, it, vi } from "bun:test"; +import * as fs from "node:fs/promises"; +import * as os from "node:os"; +import * as path from "node:path"; +import { getConfigRootDir, setAgentDir } from "@oh-my-pi/pi-utils"; import { runConfigCommand } from "../src/cli/config-cli"; +import { _resetSettingsForTest } from "../src/config/settings"; -afterEach(() => { +let testAgentDir = ""; +const originalAgentDir = process.env.PI_CODING_AGENT_DIR; +const fallbackAgentDir = path.join(getConfigRootDir(), "agent"); + +beforeEach(async () => { + _resetSettingsForTest(); + testAgentDir = await fs.mkdtemp(path.join(os.tmpdir(), "omp-config-cli-")); + setAgentDir(testAgentDir); +}); + +afterEach(async () => { vi.restoreAllMocks(); + _resetSettingsForTest(); + if (originalAgentDir) { + setAgentDir(originalAgentDir); + } else { + setAgentDir(fallbackAgentDir); + delete process.env.PI_CODING_AGENT_DIR; + } + await fs.rm(testAgentDir, { recursive: true, force: true }); }); describe("config CLI schema coverage", () => { @@ -52,4 +75,34 @@ describe("config CLI schema coverage", () => { expect(modelRolesLine).toContain("(record)"); expect(modelRolesLine).not.toContain("[object Object]"); }); + + it("sets and gets record settings as JSON objects", async () => { + const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + const recordValue = '{"default":"claude-opus-4-6"}'; + + await runConfigCommand({ action: "set", key: "modelRoles", value: recordValue, flags: { json: true } }); + await runConfigCommand({ action: "get", key: "modelRoles", flags: { json: true } }); + + const payload = logSpy.mock.calls.at(-1)?.[0]; + expect(typeof payload).toBe("string"); + const parsed = JSON.parse(String(payload)) as { key: string; value: unknown; type: string }; + expect(parsed.key).toBe("modelRoles"); + expect(parsed.type).toBe("record"); + expect(parsed.value).toEqual({ default: "claude-opus-4-6" }); + }); + + it("sets and gets array settings as JSON arrays", async () => { + const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + const arrayValue = '["claude-opus-4-6","gpt-5.3-codex"]'; + + await runConfigCommand({ action: "set", key: "enabledModels", value: arrayValue, flags: { json: true } }); + await runConfigCommand({ action: "get", key: "enabledModels", flags: { json: true } }); + + const payload = logSpy.mock.calls.at(-1)?.[0]; + expect(typeof payload).toBe("string"); + const parsed = JSON.parse(String(payload)) as { key: string; value: unknown; type: string }; + expect(parsed.key).toBe("enabledModels"); + expect(parsed.type).toBe("array"); + expect(parsed.value).toEqual(["claude-opus-4-6", "gpt-5.3-codex"]); + }); });