Compare commits

...

1 Commits

Author SHA1 Message Date
Tom Boucher
726003563a fix(#2544): config-get exits 1 on missing key (was 10, UNIX convention is 1)
Change ErrorClassification for 'Key not found' in configGet from Validation
(exit 10) to Execution (exit 1), matching git config --get. Callers using
`gsd-sdk query config-get k || fallback` need a non-zero exit to trigger the
fallback branch; 10 worked technically but was semantically wrong and noisy.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 09:53:46 -04:00
3 changed files with 83 additions and 2 deletions

View File

@@ -58,6 +58,43 @@ describe('configGet', () => {
await expect(configGet(['nonexistent.key'], tmpDir)).rejects.toThrow(GSDError); await expect(configGet(['nonexistent.key'], tmpDir)).rejects.toThrow(GSDError);
}); });
it('throws GSDError with Execution classification for missing key (exit code 1 not 10)', async () => {
// Regression for #2544: missing key must exit 1, not 10 (Validation).
// Callers like `gsd-sdk query config-get k || default` rely on non-zero exit.
// ErrorClassification.Execution maps to exit code 1 (matches git config --get).
const { configGet } = await import('./config-query.js');
const { ErrorClassification } = await import('../errors.js');
await writeFile(
join(tmpDir, '.planning', 'config.json'),
JSON.stringify({ model_profile: 'quality' }),
);
let thrown: unknown;
try {
await configGet(['nonexistent.key'], tmpDir);
} catch (err) {
thrown = err;
}
expect(thrown).toBeInstanceOf(GSDError);
expect((thrown as GSDError).classification).toBe(ErrorClassification.Execution);
});
it('throws GSDError with Execution classification when traversal hits non-object', async () => {
const { configGet } = await import('./config-query.js');
const { ErrorClassification } = await import('../errors.js');
await writeFile(
join(tmpDir, '.planning', 'config.json'),
JSON.stringify({ workflow: 'a-string-not-an-object' }),
);
let thrown: unknown;
try {
await configGet(['workflow.auto_advance'], tmpDir);
} catch (err) {
thrown = err;
}
expect(thrown).toBeInstanceOf(GSDError);
expect((thrown as GSDError).classification).toBe(ErrorClassification.Execution);
});
it('reads raw config without merging defaults', async () => { it('reads raw config without merging defaults', async () => {
const { configGet } = await import('./config-query.js'); const { configGet } = await import('./config-query.js');
// Write config with only model_profile -- no workflow section // Write config with only model_profile -- no workflow section

View File

@@ -90,12 +90,12 @@ export const configGet: QueryHandler = async (args, projectDir) => {
let current: unknown = config; let current: unknown = config;
for (const key of keys) { for (const key of keys) {
if (current === undefined || current === null || typeof current !== 'object') { if (current === undefined || current === null || typeof current !== 'object') {
throw new GSDError(`Key not found: ${keyPath}`, ErrorClassification.Validation); throw new GSDError(`Key not found: ${keyPath}`, ErrorClassification.Execution);
} }
current = (current as Record<string, unknown>)[key]; current = (current as Record<string, unknown>)[key];
} }
if (current === undefined) { if (current === undefined) {
throw new GSDError(`Key not found: ${keyPath}`, ErrorClassification.Validation); throw new GSDError(`Key not found: ${keyPath}`, ErrorClassification.Execution);
} }
return { data: current }; return { data: current };

View File

@@ -0,0 +1,44 @@
'use strict';
/**
* Bug #2544: `gsd-sdk query config-get` exits 0 on missing key.
*
* Callers that use `gsd-sdk query config-get k || fallback` rely on a
* non-zero exit code when the key is absent. The fix changes the
* ErrorClassification for "Key not found" from Validation (exit 10)
* to Execution (exit 1), matching the UNIX convention of `git config --get`.
*/
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const CONFIG_QUERY_SRC = path.join(
__dirname, '..', 'sdk', 'src', 'query', 'config-query.ts',
);
describe('gsd-sdk config-get exit code for missing key (#2544)', () => {
test('config-query.ts source exists', () => {
assert.ok(fs.existsSync(CONFIG_QUERY_SRC), 'sdk/src/query/config-query.ts must exist');
});
test('"Key not found" throws with Execution classification, not Validation', () => {
const src = fs.readFileSync(CONFIG_QUERY_SRC, 'utf-8');
// Find the "Key not found" throw lines and confirm they use Execution, not Validation
const keyNotFoundLines = src
.split('\n')
.filter(line => line.includes('Key not found'));
assert.ok(keyNotFoundLines.length > 0, 'Source must contain "Key not found" throw(s)');
for (const line of keyNotFoundLines) {
assert.ok(
line.includes('Execution'),
`"Key not found" throw must use ErrorClassification.Execution (exit 1), got: ${line.trim()}`
);
assert.ok(
!line.includes('Validation'),
`"Key not found" throw must NOT use ErrorClassification.Validation (exit 10), got: ${line.trim()}`
);
}
});
});