Merge pull request 'feat(cli): add agentOverrides and modelOverrides to config key validation' (#554) from fix/532-config-key-validation into main

This commit is contained in:
2026-05-27 03:45:47 +00:00
2 changed files with 78 additions and 3 deletions
@@ -618,5 +618,65 @@ defaultModel: default
rmSync(tempDir, { recursive: true, force: true }); 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 });
}
});
}); });
}); });
+18 -3
View File
@@ -5,7 +5,10 @@ import { parse, stringify } from "yaml";
/** /**
* Valid configuration key schema * Valid configuration key schema
*/ */
const VALID_CONFIG_KEYS: Record<string, { nested: boolean; knownFields?: string[] }> = { const VALID_CONFIG_KEYS: Record<
string,
{ nested: boolean; knownFields?: string[]; minDepth?: number }
> = {
providers: { providers: {
nested: true, nested: true,
knownFields: ["baseUrl", "apiKey"], knownFields: ["baseUrl", "apiKey"],
@@ -18,6 +21,17 @@ const VALID_CONFIG_KEYS: Record<string, { nested: boolean; knownFields?: string[
nested: true, nested: true,
knownFields: ["command", "args"], knownFields: ["command", "args"],
}, },
agentOverrides: {
nested: true,
// agentOverrides.<workflowName>.<roleName> = agentAlias (string value)
// No knownFields — workflow/role names are user-defined
},
modelOverrides: {
nested: true,
minDepth: 2,
// modelOverrides.<scenario> = modelAlias (string value)
// No knownFields — scenarios are user-defined
},
defaultAgent: { nested: false }, defaultAgent: { nested: false },
defaultModel: { 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`); 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) // Nested keys must have at least minDepth segments (default 3)
if (schema.nested && path.length < 3) { const minDepth = schema.minDepth ?? 3;
if (schema.nested && path.length < minDepth) {
const fields = schema.knownFields?.join(", ") ?? ""; const fields = schema.knownFields?.join(", ") ?? "";
throw new Error( throw new Error(
`Incomplete path for ${topLevel}. Must specify a field (e.g., ${topLevel}.<name>.<field>). Valid fields: ${fields}`, `Incomplete path for ${topLevel}. Must specify a field (e.g., ${topLevel}.<name>.<field>). Valid fields: ${fields}`,