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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* docs(#4443): backfill changeset PR number

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: sim <sim@local>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-09-07 17:29:22 -04:00
committed by GitHub
parent a0f8f956c4
commit 8dcdcb253e
3 changed files with 68 additions and 0 deletions

View File

@@ -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)

View File

@@ -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",

View File

@@ -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);
});
});