From 8dcdcb253e239d7b2e4a2195e0df245449569a08 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Mon, 7 Sep 2026 17:29:22 -0400 Subject: [PATCH] fix(#4443): register hooks.commit_types (and sibling hooks.community) in config schema (#4501) * test(#4443): failing-first regression coverage for hooks.commit_types config key isValidConfigKey('hooks.commit_types') currently returns false and config-set hooks.commit_types rejects with "Unknown config key", because the key was never added to config-schema.manifest.json's validKeys when it shipped (#3811/#4340, 1.13.0) despite being documented (docs/COMMANDS.md) and consumed by hooks/gsd-validate-commit.sh. This commit adds the regression coverage only; the manifest fix lands in the next commit. Co-Authored-By: Claude Sonnet 5 * test(#4443): avoid false-positive docs-guard registration trip The assert message for the new hooks.commit_types test mentioned "docs/COMMANDS.md" literally, which happened to land between two unrelated pre-existing backticks and tripped lint-docs-guard-registration.cjs's template-literal co-occurrence detector (a known, documented false-positive shape for that lint). Rephrased to drop the literal docs/ path from the message; the test's intent (documenting why the key must be valid) is unchanged. Co-Authored-By: Claude Sonnet 5 * fix(#4443): register hooks.commit_types (and sibling hooks.community) in config schema config-schema.manifest.json's validKeys never got hooks.commit_types added when the feature shipped (#3811/#4340, 1.13.0) despite it being documented (docs/COMMANDS.md) and consumed by hooks/gsd-validate-commit.sh -- so config-set hooks.commit_types rejected with "Unknown config key", and the only way to configure a documented feature was hand-editing .planning/config.json. While auditing every hooks.* key actually read by shipped code against validKeys (CLAUDE.md's no-deferrals rule: a defect found anywhere in the tree while working an issue is fixed in the current change, not filed separately), hooks.community -- gsd-validate-commit.sh's own opt-in gate -- turned out to have the exact same gap. Both are added here; an audit of every hooks.* read site confirmed these are the only two missing entries. Co-Authored-By: Claude Sonnet 5 * test(#4443): e2e coverage for hooks.community + changeset Closes the coverage-rigor gap the Standards review flagged: hooks.community had only a unit-level isValidConfigKey assertion, not the same real config-set CLI round-trip hooks.commit_types already got. Also adds the changeset fragment the same review flagged as a missing hard requirement. Co-Authored-By: Claude Sonnet 5 * test(#4443): use PROBE_TIMEOUT_MS instead of a bare 15000 literal local/no-adhoc-timeout-literal (lint:ci) correctly flagged both new spawnSync calls' bare timeout: 15000 -- this call class (a short CLI probe against a temp fixture) is exactly what tests/helpers/timeouts.cjs's PROBE_TIMEOUT_MS documents. Co-Authored-By: Claude Sonnet 5 * docs(#4443): backfill changeset PR number Co-Authored-By: Claude Sonnet 5 --------- Co-authored-by: sim Co-authored-by: Claude Sonnet 5 --- .changeset/sturdy-jays-cheer.md | 5 ++ .../bin/shared/config-schema.manifest.json | 2 + tests/config-schema.property.test.cjs | 61 +++++++++++++++++++ 3 files changed, 68 insertions(+) create mode 100644 .changeset/sturdy-jays-cheer.md diff --git a/.changeset/sturdy-jays-cheer.md b/.changeset/sturdy-jays-cheer.md new file mode 100644 index 000000000..262ebebe4 --- /dev/null +++ b/.changeset/sturdy-jays-cheer.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 4501 +--- +**`hooks.commit_types` and `hooks.community` are now settable via `config-set`** — both keys are consumed by shipped hooks (`hooks/gsd-validate-commit.sh`), and `hooks.commit_types` is documented in `docs/COMMANDS.md`, but neither was registered in `config-schema.manifest.json`'s `validKeys`, so `config-set` rejected them with "Unknown config key" — the only way to configure either was hand-editing `.planning/config.json`. (#4443) diff --git a/gsd-core/bin/shared/config-schema.manifest.json b/gsd-core/bin/shared/config-schema.manifest.json index 0212ba304..ad08ba1e7 100644 --- a/gsd-core/bin/shared/config-schema.manifest.json +++ b/gsd-core/bin/shared/config-schema.manifest.json @@ -77,6 +77,8 @@ "workflow.inline_plan_threshold", "hooks.context_warnings", "hooks.workflow_guard", + "hooks.commit_types", + "hooks.community", "workflow.context_coverage_gate", "statusline.show_last_command", "statusline.context_position", diff --git a/tests/config-schema.property.test.cjs b/tests/config-schema.property.test.cjs index 8d02ccdf1..f9081f76d 100644 --- a/tests/config-schema.property.test.cjs +++ b/tests/config-schema.property.test.cjs @@ -24,6 +24,7 @@ const os = require('node:os'); const path = require('node:path'); const fc = require('./helpers/fast-check-setup.cjs'); const { cleanup } = require('./helpers.cjs'); +const { PROBE_TIMEOUT_MS } = require('./helpers/timeouts.cjs'); const { isValidConfigKey, @@ -150,6 +151,22 @@ describe('config-schema: isValidConfigKey properties', () => { assert.equal(result, false, 'empty string must not be a valid config key'); }); + test('hooks.commit_types is a valid config key (#4443)', () => { + assert.equal( + isValidConfigKey('hooks.commit_types'), + true, + 'hooks.commit_types is a documented, hook-consumed key (see the CLI reference) and must be settable via config-set' + ); + }); + + test('hooks.community is a valid config key (sibling of #4443)', () => { + assert.equal( + isValidConfigKey('hooks.community'), + true, + 'hooks.community is the gsd-validate-commit.sh opt-in gate and must be settable via config-set' + ); + }); + // Boundary: null/undefined/number return false (not throw, not true) test('null, undefined, number inputs return false', () => { assert.equal(isValidConfigKey(null), false); @@ -1232,3 +1249,47 @@ describe('feat-3210: workflow and config contracts', () => { }); }); } + +describe('config-set: hooks.commit_types end-to-end (#4443)', () => { + const { spawnSync } = require('node:child_process'); + + test('config-set hooks.commit_types accepts a JSON array and does not report Unknown config key', (t) => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-4443-')); + t.after(() => cleanup(dir)); + fs.mkdirSync(path.join(dir, '.planning'), { recursive: true }); + fs.writeFileSync(path.join(dir, '.planning', 'config.json'), '{}\n'); + + const gsdTools = path.join(__dirname, '..', 'gsd-core', 'bin', 'gsd-tools.cjs'); + const result = spawnSync( + process.execPath, + [gsdTools, 'config-set', 'hooks.commit_types', '["enhance"]'], + { cwd: dir, encoding: 'utf8', timeout: PROBE_TIMEOUT_MS }, + ); + + assert.equal(result.status, 0, `config-set failed: ${result.stderr || result.stdout}`); + assert.doesNotMatch(result.stdout + result.stderr, /Unknown config key/); + + const written = JSON.parse(fs.readFileSync(path.join(dir, '.planning', 'config.json'), 'utf8')); + assert.deepEqual(written.hooks.commit_types, ['enhance']); + }); + + test('config-set hooks.community accepts a boolean and does not report Unknown config key', (t) => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-4443-community-')); + t.after(() => cleanup(dir)); + fs.mkdirSync(path.join(dir, '.planning'), { recursive: true }); + fs.writeFileSync(path.join(dir, '.planning', 'config.json'), '{}\n'); + + const gsdTools = path.join(__dirname, '..', 'gsd-core', 'bin', 'gsd-tools.cjs'); + const result = spawnSync( + process.execPath, + [gsdTools, 'config-set', 'hooks.community', 'true'], + { cwd: dir, encoding: 'utf8', timeout: PROBE_TIMEOUT_MS }, + ); + + assert.equal(result.status, 0, `config-set failed: ${result.stderr || result.stdout}`); + assert.doesNotMatch(result.stdout + result.stderr, /Unknown config key/); + + const written = JSON.parse(fs.readFileSync(path.join(dir, '.planning', 'config.json'), 'utf8')); + assert.equal(written.hooks.community, true); + }); +});