diff --git a/packages/cli-workflow/src/__tests__/config.test.ts b/packages/cli-workflow/src/__tests__/config.test.ts index 2c08777..078c2f2 100644 --- a/packages/cli-workflow/src/__tests__/config.test.ts +++ b/packages/cli-workflow/src/__tests__/config.test.ts @@ -618,5 +618,65 @@ defaultModel: default rmSync(tempDir, { recursive: true, force: true }); } }); + + test("agentOverrides — accepts valid 3-segment path", async () => { + const tempDir = mkdtempSync(join(tmpdir(), "test-config-")); + try { + createTestConfig(tempDir, sampleConfig); + await cmdConfigSet(tempDir, "agentOverrides.solve-issue.planner", "claude-code"); + const value = await cmdConfigGet(tempDir, "agentOverrides.solve-issue.planner"); + expect(value).toBe("claude-code"); + } finally { + rmSync(tempDir, { recursive: true, force: true }); + } + }); + + test("agentOverrides — rejects incomplete path (2 segments)", async () => { + const tempDir = mkdtempSync(join(tmpdir(), "test-config-")); + try { + createTestConfig(tempDir, sampleConfig); + await expect(cmdConfigSet(tempDir, "agentOverrides.solve-issue", "hermes")).rejects.toThrow( + /incomplete path|must specify a field/i, + ); + } finally { + rmSync(tempDir, { recursive: true, force: true }); + } + }); + + test("modelOverrides — accepts valid 2-segment path", async () => { + const tempDir = mkdtempSync(join(tmpdir(), "test-config-")); + try { + createTestConfig(tempDir, sampleConfig); + await cmdConfigSet(tempDir, "modelOverrides.extract", "gpt4"); + const value = await cmdConfigGet(tempDir, "modelOverrides.extract"); + expect(value).toBe("gpt4"); + } finally { + rmSync(tempDir, { recursive: true, force: true }); + } + }); + + test("modelOverrides — rejects incomplete path (1 segment only)", async () => { + const tempDir = mkdtempSync(join(tmpdir(), "test-config-")); + try { + createTestConfig(tempDir, sampleConfig); + await expect(cmdConfigSet(tempDir, "modelOverrides", "gpt4")).rejects.toThrow( + /incomplete path|must specify a field/i, + ); + } finally { + rmSync(tempDir, { recursive: true, force: true }); + } + }); + + test("rejects unknown top-level key (regression)", async () => { + const tempDir = mkdtempSync(join(tmpdir(), "test-config-")); + try { + createTestConfig(tempDir, sampleConfig); + await expect(cmdConfigSet(tempDir, "randomKey", "value")).rejects.toThrow( + /Unknown config key/, + ); + } finally { + rmSync(tempDir, { recursive: true, force: true }); + } + }); }); }); diff --git a/packages/cli-workflow/src/commands/config.ts b/packages/cli-workflow/src/commands/config.ts index 63d3a9b..a791fe1 100644 --- a/packages/cli-workflow/src/commands/config.ts +++ b/packages/cli-workflow/src/commands/config.ts @@ -5,7 +5,10 @@ import { parse, stringify } from "yaml"; /** * Valid configuration key schema */ -const VALID_CONFIG_KEYS: Record = { +const VALID_CONFIG_KEYS: Record< + string, + { nested: boolean; knownFields?: string[]; minDepth?: number } +> = { providers: { nested: true, knownFields: ["baseUrl", "apiKey"], @@ -18,6 +21,17 @@ const VALID_CONFIG_KEYS: Record. = agentAlias (string value) + // No knownFields — workflow/role names are user-defined + }, + modelOverrides: { + nested: true, + minDepth: 2, + // modelOverrides. = modelAlias (string value) + // No knownFields — scenarios are user-defined + }, defaultAgent: { nested: false }, defaultModel: { nested: false }, }; @@ -43,8 +57,9 @@ function validateConfigKey(path: string[]): void { throw new Error(`${topLevel} is a scalar key and cannot have nested properties`); } - // Nested keys must have at least 3 segments (e.g., providers.myProvider.baseUrl) - if (schema.nested && path.length < 3) { + // Nested keys must have at least minDepth segments (default 3) + const minDepth = schema.minDepth ?? 3; + if (schema.nested && path.length < minDepth) { const fields = schema.knownFields?.join(", ") ?? ""; throw new Error( `Incomplete path for ${topLevel}. Must specify a field (e.g., ${topLevel}..). Valid fields: ${fields}`,