fix: repair trapped non-boolean keys under [features] on Codex re-install

Pre-#1346 GSD installs prepended [features] before bare top-level keys
in ~/.codex/config.toml, trapping keys like model="gpt-5.3-codex" under
[features] where Codex expects only booleans. The #1346 fix prevented
NEW corruption but did not repair EXISTING corrupted configs. Re-installing
GSD left the trapped keys in place, causing "invalid type: string, expected
a boolean" on every Codex launch.

repairTrappedFeaturesKeys() now detects non-boolean key-value lines inside
[features] and relocates them before the [features] header during
ensureCodexHooksFeature(), so re-installs heal previously corrupted configs.

Fixes #1379

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-03-25 14:50:02 -04:00
parent 4ef309e0e4
commit f4d8858188
2 changed files with 125 additions and 2 deletions

View File

@@ -760,6 +760,49 @@ describe('Codex install hook configuration (e2e)', () => {
assert.ok(!content.includes('config_file = "agents/'), 'no relative config_file paths');
});
test('re-install repairs non-boolean keys trapped under [features] by previous install (#1379)', () => {
// Bug: a pre-#1346 install prepended [features] before bare top-level keys,
// trapping model= under [features]. Re-installing with the fix must detect
// and relocate those keys back to the top level so Codex can parse them.
writeCodexConfig(codexHome, [
'[features]',
'codex_hooks = true',
'',
'model = "gpt-5.3-codex"',
'model_reasoning_effort = "high"',
'',
'[projects."/Users/oltmannk/myproject"]',
'trust_level = "trusted"',
'',
].join('\n'));
runCodexInstall(codexHome);
const content = readCodexConfig(codexHome);
// model= and model_reasoning_effort= must NOT be under [features]
const featuresIndex = content.indexOf('[features]');
const modelIndex = content.indexOf('model = "gpt-5.3-codex"');
const reasoningIndex = content.indexOf('model_reasoning_effort = "high"');
assert.ok(modelIndex !== -1, 'model key is present');
assert.ok(reasoningIndex !== -1, 'model_reasoning_effort key is present');
assert.ok(modelIndex < featuresIndex, 'model= relocated before [features]');
assert.ok(reasoningIndex < featuresIndex, 'model_reasoning_effort= relocated before [features]');
// [features] should only contain boolean keys
const featuresMatch = content.match(/\[features\]\n([\s\S]*?)(?=\n\[|$)/);
assert.ok(featuresMatch, 'features section found');
const featuresBody = featuresMatch[1];
const nonBooleanKeys = featuresBody.split('\n')
.filter(line => line.match(/^\s*\w+\s*=/) && !line.match(/=\s*(true|false)\s*(#.*)?$/));
assert.strictEqual(nonBooleanKeys.length, 0, 'no non-boolean keys under [features]');
// User content preserved
assert.ok(content.includes('[projects."/Users/oltmannk/myproject"]'), 'preserves project section');
assert.ok(content.includes('trust_level = "trusted"'), 'preserves project trust level');
assert.strictEqual(countMatches(content, /^codex_hooks = true$/gm), 1, 'one codex_hooks key');
});
test('existing LF config without [features] gets one features block and preserves user content', () => {
writeCodexConfig(codexHome, [
'# user comment',