fix(config): allow model_overrides.<agent-id> in config-set (#3227) (#3253)

* test: reproduce config-set rejecting model_overrides.<agent-id> (#3227)

* fix(config): allow model_overrides.<agent-id> in config-set (#3227)

* changeset: pr=3253 for #3227
This commit is contained in:
Tom Boucher
2026-05-08 08:40:53 -04:00
committed by GitHub
parent 6299b9181f
commit f5fe5bc063
4 changed files with 134 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 3253
---
**`gsd-sdk query config-set model_overrides.<agent-id>` now accepted** — was rejected with "Unknown config key" despite the override mechanism working. Sibling fix to #3162.

View File

@@ -107,6 +107,10 @@ const DYNAMIC_KEY_PATTERNS = [
{ topLevel: 'dynamic_routing',
test: (k) => /^dynamic_routing\.(enabled|escalate_on_failure|max_escalations|tier_models\.(light|standard|heavy))$/.test(k),
description: 'dynamic_routing.<enabled|escalate_on_failure|max_escalations|tier_models.<light|standard|heavy>>' },
// #3227 — per-agent model overrides: model_overrides.<agent-id>
// Full model IDs (e.g. "openai/o3") and tier aliases (opus/sonnet/haiku/inherit)
// are both accepted. Value validation is handled by the resolver at read time.
{ topLevel: 'model_overrides', test: (k) => /^model_overrides\.[a-zA-Z0-9_-]+$/.test(k), description: 'model_overrides.<agent-id>' },
];
/**

View File

@@ -133,6 +133,12 @@ export const DYNAMIC_KEY_PATTERNS: readonly DynamicKeyPattern[] = [
description: 'dynamic_routing.<enabled|escalate_on_failure|max_escalations|tier_models.<light|standard|heavy>>',
test: (k) => /^dynamic_routing\.(enabled|escalate_on_failure|max_escalations|tier_models\.(light|standard|heavy))$/.test(k),
},
// #3227 — per-agent model overrides: model_overrides.<agent-id>
{
source: '^model_overrides\\.[a-zA-Z0-9_-]+$',
description: 'model_overrides.<agent-id>',
test: (k) => /^model_overrides\.[a-zA-Z0-9_-]+$/.test(k),
},
];
/** Returns true if keyPath is a valid config key (exact, runtime-state, or dynamic pattern). */

View File

@@ -0,0 +1,119 @@
'use strict';
/**
* Regression test for bug #3227 — config-set rejects model_overrides.<agent-id>.
*
* `gsd-sdk query config-set model_overrides.gsd-plan-checker opus` was
* rejected with "Unknown config key" because `model_overrides.<agent-id>` was
* missing from DYNAMIC_KEY_PATTERNS in both the CJS schema and the SDK schema.
*
* The override mechanism itself worked correctly (resolve-model returned the
* override after a direct file edit). Only the write path was gated wrong.
*/
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const { createTempProject, cleanup, runGsdTools } = require('./helpers.cjs');
const { DYNAMIC_KEY_PATTERNS, isValidConfigKey } = require('../get-shit-done/bin/lib/config-schema.cjs');
describe('#3227 — config-set accepts model_overrides.<agent-id>', () => {
test('isValidConfigKey accepts model_overrides.gsd-plan-checker', () => {
assert.ok(
isValidConfigKey('model_overrides.gsd-plan-checker'),
'model_overrides.gsd-plan-checker must be accepted by isValidConfigKey'
);
});
test('isValidConfigKey accepts model_overrides with various agent-id formats', () => {
const validKeys = [
'model_overrides.gsd-executor',
'model_overrides.gsd-planner',
'model_overrides.gsd-codebase-mapper',
'model_overrides.my_custom_agent',
'model_overrides.agent123',
];
for (const key of validKeys) {
assert.ok(isValidConfigKey(key), `isValidConfigKey must accept ${key}`);
}
});
test('isValidConfigKey rejects bare model_overrides (no agent-id)', () => {
assert.ok(
!isValidConfigKey('model_overrides'),
'bare model_overrides must be rejected (use model_overrides.<agent-id>)'
);
});
test('DYNAMIC_KEY_PATTERNS includes an entry for model_overrides', () => {
const hasPattern = DYNAMIC_KEY_PATTERNS.some(
(p) => p.description && p.description.includes('model_overrides')
);
assert.ok(hasPattern, 'DYNAMIC_KEY_PATTERNS must have an entry covering model_overrides.<agent-id>');
});
test('config-set model_overrides.gsd-plan-checker opus succeeds via gsd-tools.cjs', (t) => {
const tmpDir = createTempProject();
t.after(() => cleanup(tmpDir));
const result = runGsdTools(
['config-set', 'model_overrides.gsd-plan-checker', 'opus'],
tmpDir
);
assert.ok(
result.success,
[
'config-set model_overrides.gsd-plan-checker opus should succeed,',
'got:',
'stdout: ' + result.output,
'stderr: ' + result.error,
].join('\n')
);
});
test('config-set model_overrides.gsd-plan-checker opus writes to config.json', (t) => {
const tmpDir = createTempProject();
t.after(() => cleanup(tmpDir));
runGsdTools(['config-set', 'model_overrides.gsd-plan-checker', 'opus'], tmpDir);
const configPath = path.join(tmpDir, '.planning', 'config.json');
assert.ok(fs.existsSync(configPath), '.planning/config.json must exist after config-set');
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
assert.ok(
config.model_overrides !== undefined &&
config.model_overrides['gsd-plan-checker'] === 'opus',
[
'Expected model_overrides["gsd-plan-checker"]: "opus" in config.json,',
'got: ' + JSON.stringify(config),
].join('\n')
);
});
test('config-get model_overrides.gsd-plan-checker returns opus after config-set', (t) => {
const tmpDir = createTempProject();
t.after(() => cleanup(tmpDir));
runGsdTools(['config-set', 'model_overrides.gsd-plan-checker', 'opus'], tmpDir);
const getResult = runGsdTools(
['config-get', 'model_overrides.gsd-plan-checker'],
tmpDir
);
assert.ok(
getResult.success,
[
'config-get model_overrides.gsd-plan-checker should succeed,',
'got:',
'stdout: ' + getResult.output,
'stderr: ' + getResult.error,
].join('\n')
);
assert.ok(
getResult.output.includes('opus'),
'config-get output should contain "opus", got: ' + getResult.output
);
});
});