test(#1971): consolidate 48 gsd-tools CLI regression tests into subcommand suites

Fold 48 issue-named gsd-tools CLI regression files into the canonical test file
that owns each subcommand subject (state, roadmap, phase, milestone, audit, config,
router/dispatch, stats, verify, health, etc.), preserving every assertion and its
origin issue number as provenance (block-scoped describe wrappers, 299 subtests
conserved 1:1). No monolithic gsd-tools.test.cjs created — routes into 18 existing
per-subject suites.

Removes 48 tests/ files. Regenerates regression-name allowlist (271->231), ratchets
the file-count allowlist across 6 buckets (audit/milestone/phase/roadmap/state/verify),
and makes 10 relocated allow-test-rule exemptions issue-ref-compliant (ADR-456; prunes
10 stale ids). Repoints one CONTEXT.md symptom ref and ADR-3524's parity-test ref.
lint:ci green.

Part of epic #1969. Closes #1971.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-07-03 01:22:41 -04:00
parent 89ecee06fc
commit de3ba45d00
71 changed files with 10731 additions and 10317 deletions

View File

@@ -1462,3 +1462,585 @@ describe('config-set workflow.test_command (#1216)', () => {
assert.strictEqual(config.workflow?.build_command, 'npm run build', 'value must be persisted');
});
});
// ────────────────────────────────────────────────────────────────────────
// Folded from tests/bug-2601-inherit-model-profile.test.cjs — consolidation epic #1969 (B2 #1971)
// ────────────────────────────────────────────────────────────────────────
{
const { describe: __foldDescribe } = require('node:test');
__foldDescribe("folded:bug-2601-inherit-model-profile (consolidation epic #1969 B2 #1971)", () => {
'use strict';
/**
* Regression tests for bug #2601
*
* `config-set-model-profile inherit` (and `config-set model_profile inherit`)
* was rejected by the validator even though the runtime accepts 'inherit' as a
* valid model_profile value meaning "inherit from parent configuration".
*
* Root cause: VALID_PROFILES in model-profiles.cjs is derived from
* Object.keys(MODEL_PROFILES['gsd-planner']), which does not include 'inherit'.
* cmdConfigSetModelProfile() rejects any value not in VALID_PROFILES.
*/
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const { runGsdTools, createTempProject, cleanup } = require('./helpers.cjs');
describe('bug #2601: config-set-model-profile accepts inherit', () => {
test('config-set-model-profile inherit succeeds', (t) => {
const tmpDir = createTempProject();
t.after(() => cleanup(tmpDir));
const result = runGsdTools(['config-set-model-profile', 'inherit'], tmpDir);
assert.ok(result.success, `should accept inherit: ${result.error}`);
});
test('config-set model_profile inherit succeeds', (t) => {
const tmpDir = createTempProject();
t.after(() => cleanup(tmpDir));
const result = runGsdTools(['config-set', 'model_profile', 'inherit'], tmpDir);
assert.ok(result.success, `config-set model_profile inherit should succeed: ${result.error}`);
});
test('config-set-model-profile inherit writes inherit to config', (t) => {
const tmpDir = createTempProject();
t.after(() => cleanup(tmpDir));
runGsdTools(['config-set-model-profile', 'inherit'], tmpDir);
const getResult = runGsdTools(['config-get', 'model_profile'], tmpDir);
assert.ok(getResult.success, `config-get should succeed: ${getResult.error}`);
assert.strictEqual(JSON.parse(getResult.output), 'inherit');
});
test('config-set-model-profile still rejects truly invalid profiles', (t) => {
const tmpDir = createTempProject();
t.after(() => cleanup(tmpDir));
const result = runGsdTools(['config-set-model-profile', 'not-a-real-profile'], tmpDir);
assert.ok(!result.success, 'should reject invalid profiles');
});
});
});
}
// ────────────────────────────────────────────────────────────────────────
// Folded from tests/bug-3197-gsd-tools-config-whitelist.test.cjs — consolidation epic #1969 (B2 #1971)
// ────────────────────────────────────────────────────────────────────────
{
const { describe: __foldDescribe } = require('node:test');
__foldDescribe("folded:bug-3197-gsd-tools-config-whitelist (consolidation epic #1969 B2 #1971)", () => {
'use strict';
/**
* Regression test for #3197 — gsd-tools config-set rejects workflow._auto_chain_active.
*
* Root cause: RUNTIME_STATE_KEYS was added to sdk/src/query/config-schema.ts in #3162
* but not to gsd-core/bin/lib/config-schema.cjs, so gsd-tools.cjs users still hit
* "Unknown config key" when setting workflow._auto_chain_active.
*/
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');
describe('#3197 — gsd-tools.cjs config-set workflow._auto_chain_active', () => {
test('config-set workflow._auto_chain_active true succeeds via gsd-tools.cjs (CJS path)', (t) => {
const tmpDir = createTempProject();
t.after(() => cleanup(tmpDir));
const result = runGsdTools(['config-set', 'workflow._auto_chain_active', 'true'], tmpDir);
assert.ok(
result.success,
`config-set workflow._auto_chain_active true should succeed, got:\nstdout: ${result.output}\nstderr: ${result.error}`
);
});
test('config-set workflow._auto_chain_active true writes value to config.json', (t) => {
const tmpDir = createTempProject();
t.after(() => cleanup(tmpDir));
runGsdTools(['config-set', 'workflow._auto_chain_active', 'true'], 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.workflow !== undefined && config.workflow._auto_chain_active === true,
`Expected workflow._auto_chain_active: true in config.json, got: ${JSON.stringify(config)}`
);
});
test('config-set workflow._auto_chain_active false writes false to config.json', (t) => {
const tmpDir = createTempProject();
t.after(() => cleanup(tmpDir));
runGsdTools(['config-set', 'workflow._auto_chain_active', 'false'], 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.workflow !== undefined && config.workflow._auto_chain_active === false,
`Expected workflow._auto_chain_active: false in config.json, got: ${JSON.stringify(config)}`
);
});
});
});
}
// ────────────────────────────────────────────────────────────────────────
// Folded from tests/bug-3086-git-create-tag-config-gate.test.cjs — consolidation epic #1969 (B2 #1971)
// ────────────────────────────────────────────────────────────────────────
{
const { describe: __foldDescribe } = require('node:test');
__foldDescribe("folded:bug-3086-git-create-tag-config-gate (consolidation epic #1969 B2 #1971)", () => {
// allow-test-rule: workflow-markdown-is-the-runtime-contract (see #3086)
// Justification: complete-milestone.md IS the runtime — the agent reads and
// follows it directly. Asserting the <config-check> block is present in the
// markdown is the only way to verify the gate is wired. Per CONTEXT.md L611.
'use strict';
/**
* #3086 — git.create_tag config gate for milestone tagging.
*
* Tests:
* A. Default value: fresh project returns `true` for git.create_tag
* B. config-set false → config-get returns false
* C. Invalid value (e.g. "maybe") is rejected by schema validator
* D. complete-milestone.md workflow contains the <config-check> gate for git.create_tag
*/
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 WORKFLOW_PATH = path.join(
__dirname,
'..',
'gsd-core',
'workflows',
'complete-milestone.md',
);
describe('#3086: git.create_tag config key', () => {
test('A. fresh project: config-get git.create_tag returns true (default)', (t) => {
const tmpDir = createTempProject('gsd-3086-default-');
t.after(() => cleanup(tmpDir));
const result = runGsdTools(['config-get', 'git.create_tag'], tmpDir, { HOME: tmpDir });
assert.ok(result.success, `config-get git.create_tag failed:\n${result.error}`);
assert.strictEqual(
result.output.trim(),
'true',
`Expected default value 'true', got: '${result.output.trim()}'`,
);
});
test('B. config-set git.create_tag false → config-get returns false', (t) => {
const tmpDir = createTempProject('gsd-3086-set-false-');
t.after(() => cleanup(tmpDir));
const setResult = runGsdTools(['config-set', 'git.create_tag', 'false'], tmpDir, {
HOME: tmpDir,
});
assert.ok(setResult.success, `config-set git.create_tag false failed:\n${setResult.error}`);
const getResult = runGsdTools(['config-get', 'git.create_tag'], tmpDir, { HOME: tmpDir });
assert.ok(getResult.success, `config-get after set failed:\n${getResult.error}`);
assert.strictEqual(
getResult.output.trim(),
'false',
`Expected 'false' after set, got: '${getResult.output.trim()}'`,
);
});
test('C. config-set git.create_tag with invalid value "maybe" is rejected', (t) => {
const tmpDir = createTempProject('gsd-3086-invalid-');
t.after(() => cleanup(tmpDir));
const result = runGsdTools(['config-set', 'git.create_tag', 'maybe'], tmpDir, {
HOME: tmpDir,
});
assert.ok(
!result.success,
`Expected config-set to fail for invalid value "maybe", but it succeeded`,
);
});
test('D. complete-milestone.md contains <config-check> gate for git.create_tag', () => {
const content = fs.readFileSync(WORKFLOW_PATH, 'utf8');
assert.ok(
content.includes('git.create_tag'),
'complete-milestone.md must reference git.create_tag in a <config-check> block',
);
assert.ok(
content.includes('<config-check>'),
'complete-milestone.md must have a <config-check> block in the git_tag step',
);
});
});
});
}
// ────────────────────────────────────────────────────────────────────────
// Folded from tests/feat-3167-ship-pr-body-sections.test.cjs — consolidation epic #1969 (B2 #1971)
// ────────────────────────────────────────────────────────────────────────
{
const { describe: __foldDescribe } = require('node:test');
__foldDescribe("folded:feat-3167-ship-pr-body-sections (consolidation epic #1969 B2 #1971)", () => {
/**
* Regression tests for issue #3167: configurable /gsd-ship PR body sections.
*/
// allow-test-rule: source-text-is-the-product (see #3167)
'use strict';
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const { describe, test, afterEach } = require('node:test');
const { runGsdTools, createTempProject, cleanup } = require('./helpers.cjs');
const repoRoot = path.resolve(__dirname, '..');
const tmpDirs = [];
function readRepoFile(relativePath) {
return fs.readFileSync(path.join(repoRoot, relativePath), 'utf8');
}
function makeProject() {
const tmpDir = createTempProject('gsd-3167-');
tmpDirs.push(tmpDir);
return tmpDir;
}
afterEach(() => {
while (tmpDirs.length) {
cleanup(tmpDirs.pop());
}
});
describe('ship.pr_body_sections config (#3167)', () => {
test('CLI config-set accepts additional PR body section arrays', () => {
const cwd = makeProject();
const value = JSON.stringify([
{
heading: 'Risks & Rollback',
enabled: true,
source: 'PLAN.md ## Risks || PLAN.md ## Rollback',
fallback: '- Rollback: revert this PR.',
},
{
heading: 'Stakeholder Sign-off',
enabled: false,
template: '- Product owner: pending',
},
]);
const result = runGsdTools(['config-set', 'ship.pr_body_sections', value, '--raw'], cwd, { HOME: cwd });
assert.equal(result.success, true, result.error);
const config = JSON.parse(fs.readFileSync(path.join(cwd, '.planning', 'config.json'), 'utf8'));
assert.deepEqual(config.ship.pr_body_sections, [
{
heading: 'Risks & Rollback',
enabled: true,
source: 'PLAN.md ## Risks || PLAN.md ## Rollback',
fallback: '- Rollback: revert this PR.',
},
{
heading: 'Stakeholder Sign-off',
enabled: false,
template: '- Product owner: pending',
},
]);
});
test('CLI config-set rejects malformed PR body section values before writing config', () => {
const cwd = makeProject();
const notArray = runGsdTools(
['config-set', 'ship.pr_body_sections', JSON.stringify({ heading: 'Not an array' }), '--raw'],
cwd,
{ HOME: cwd }
);
assert.equal(notArray.success, false);
assert.match(notArray.error, /ship\.pr_body_sections.*JSON array/);
const missingHeading = runGsdTools(
['config-set', 'ship.pr_body_sections', JSON.stringify([{ fallback: '- Missing heading' }]), '--raw'],
cwd,
{ HOME: cwd }
);
assert.equal(missingHeading.success, false);
assert.match(missingHeading.error, /heading/);
const invalidEnabled = runGsdTools(
['config-set', 'ship.pr_body_sections', JSON.stringify([{ heading: 'Toggle', enabled: 'yes', fallback: '- item' }]), '--raw'],
cwd,
{ HOME: cwd }
);
assert.equal(invalidEnabled.success, false);
assert.match(invalidEnabled.error, /enabled/);
assert.equal(fs.existsSync(path.join(cwd, '.planning', 'config.json')), false);
});
test('CLI config-new-project validates onboarded PR body sections before writing config', () => {
const cwd = makeProject();
const choices = JSON.stringify({
ship: {
pr_body_sections: [
{
heading: 'Invalid source',
source: 'package.json ## Scripts',
},
],
},
});
const result = runGsdTools(['config-new-project', choices], cwd, { HOME: cwd });
assert.equal(result.success, false);
assert.match(result.error, /source must use selectors/);
assert.equal(fs.existsSync(path.join(cwd, '.planning', 'config.json')), false);
});
test('ship workflow composes configured sections as append-only extensions', () => {
const workflow = readRepoFile('gsd-core/workflows/ship.md');
assert.match(workflow, /config-get ship\.pr_body_sections --default '\[\]'/);
assert.match(workflow, /append-only/i);
assert.match(workflow, /enabled.*false/i);
assert.match(workflow, /cannot replace/i);
assert.match(workflow, /Summary[\s\S]*Changes[\s\S]*Requirements Addressed[\s\S]*Verification[\s\S]*Key Decisions/);
assert.match(workflow, /\{phase_number\}[\s\S]*\{phase_name\}[\s\S]*\{phase_dir\}[\s\S]*\{base_branch\}[\s\S]*\{padded_phase\}/);
assert.match(workflow, /User Stories & Acceptance Criteria/);
assert.match(workflow, /Definition of Done/);
assert.match(workflow, /--body-file/);
assert.match(workflow, /trap 'rm -f "\$\{PR_BODY_FILE:-\}"' EXIT/);
});
test('default config and documentation describe ship.pr_body_sections', () => {
const template = JSON.parse(readRepoFile('gsd-core/templates/config.json'));
assert.deepEqual(template.ship.pr_body_sections, []);
const docs = readRepoFile('docs/CONFIGURATION.md');
assert.match(docs, /`ship\.pr_body_sections`/);
assert.match(docs, /additional PR body sections/i);
assert.match(docs, /append-only/i);
assert.match(docs, /lean\/agile PRD/i);
assert.match(docs, /Definition of Done/);
const planningConfig = readRepoFile('gsd-core/references/planning-config.md');
assert.match(planningConfig, /ship\.pr_body_sections/);
});
test('new-project onboarding can seed enabled or disabled PR body sections', () => {
const workflow = readRepoFile('gsd-core/workflows/new-project.md');
assert.match(workflow, /ship\.pr_body_sections/);
assert.match(workflow, /enabled.*true/);
assert.match(workflow, /enabled.*false/);
assert.match(workflow, /User Stories & Acceptance Criteria/);
assert.match(workflow, /Risks & Dependencies/);
assert.match(workflow, /Success Metrics & Release Criteria/);
assert.match(workflow, /Stakeholder Review & Approval/);
});
});
});
}
// ────────────────────────────────────────────────────────────────────────
// Folded from tests/feat-3210-fallow-schema-enum.test.cjs — consolidation epic #1969 (B2 #1971)
// ────────────────────────────────────────────────────────────────────────
{
const { describe: __foldDescribe } = require('node:test');
__foldDescribe("folded:feat-3210-fallow-schema-enum (consolidation epic #1969 B2 #1971)", () => {
'use strict';
/**
* Enum validation for code_quality.fallow.scope and code_quality.fallow.profile.
*
* Fixes H5 from #3424 review: config-set silently accepted invalid enum values
* (e.g. scope=fullrepo) and fell through to default behavior. This test asserts
* that invalid values are rejected with a helpful error, and valid values pass.
*/
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const { createTempProject, cleanup, runGsdTools } = require('./helpers.cjs');
describe('feat-3210 / H5: enum validation for code_quality.fallow.scope and .profile', () => {
// --- code_quality.fallow.scope ---
test('config-set code_quality.fallow.scope=fullrepo is REJECTED with helpful error', (t) => {
const tmpDir = createTempProject();
t.after(() => cleanup(tmpDir));
const result = runGsdTools(
['config-set', 'code_quality.fallow.scope', 'fullrepo'],
tmpDir
);
assert.ok(
!result.success,
'config-set code_quality.fallow.scope=fullrepo must fail, but it succeeded'
);
const combined = (result.output || '') + (result.error || '');
assert.ok(
combined.includes('phase') && combined.includes('repo'),
`Error message must mention valid values "phase" and "repo", got: ${combined}`
);
});
test('config-set code_quality.fallow.scope=phase is ACCEPTED', (t) => {
const tmpDir = createTempProject();
t.after(() => cleanup(tmpDir));
const result = runGsdTools(
['config-set', 'code_quality.fallow.scope', 'phase'],
tmpDir
);
assert.ok(
result.success,
[
'config-set code_quality.fallow.scope=phase must succeed,',
'stdout: ' + result.output,
'stderr: ' + result.error,
].join('\n')
);
});
test('config-set code_quality.fallow.scope=repo is ACCEPTED', (t) => {
const tmpDir = createTempProject();
t.after(() => cleanup(tmpDir));
const result = runGsdTools(
['config-set', 'code_quality.fallow.scope', 'repo'],
tmpDir
);
assert.ok(
result.success,
[
'config-set code_quality.fallow.scope=repo must succeed,',
'stdout: ' + result.output,
'stderr: ' + result.error,
].join('\n')
);
});
test('config-set code_quality.fallow.scope=PHASE (wrong case) is REJECTED', (t) => {
const tmpDir = createTempProject();
t.after(() => cleanup(tmpDir));
const result = runGsdTools(
['config-set', 'code_quality.fallow.scope', 'PHASE'],
tmpDir
);
assert.ok(
!result.success,
'config-set code_quality.fallow.scope=PHASE must fail (values are case-sensitive)'
);
});
// --- code_quality.fallow.profile ---
test('config-set code_quality.fallow.profile=aggressive is REJECTED with helpful error', (t) => {
const tmpDir = createTempProject();
t.after(() => cleanup(tmpDir));
const result = runGsdTools(
['config-set', 'code_quality.fallow.profile', 'aggressive'],
tmpDir
);
assert.ok(
!result.success,
'config-set code_quality.fallow.profile=aggressive must fail, but it succeeded'
);
const combined = (result.output || '') + (result.error || '');
assert.ok(
combined.includes('minimal') && combined.includes('standard') && combined.includes('strict'),
`Error message must mention valid values "minimal", "standard", "strict", got: ${combined}`
);
});
test('config-set code_quality.fallow.profile=minimal is ACCEPTED', (t) => {
const tmpDir = createTempProject();
t.after(() => cleanup(tmpDir));
const result = runGsdTools(
['config-set', 'code_quality.fallow.profile', 'minimal'],
tmpDir
);
assert.ok(
result.success,
[
'config-set code_quality.fallow.profile=minimal must succeed,',
'stdout: ' + result.output,
'stderr: ' + result.error,
].join('\n')
);
});
test('config-set code_quality.fallow.profile=standard is ACCEPTED', (t) => {
const tmpDir = createTempProject();
t.after(() => cleanup(tmpDir));
const result = runGsdTools(
['config-set', 'code_quality.fallow.profile', 'standard'],
tmpDir
);
assert.ok(
result.success,
[
'config-set code_quality.fallow.profile=standard must succeed,',
'stdout: ' + result.output,
'stderr: ' + result.error,
].join('\n')
);
});
test('config-set code_quality.fallow.profile=strict is ACCEPTED', (t) => {
const tmpDir = createTempProject();
t.after(() => cleanup(tmpDir));
const result = runGsdTools(
['config-set', 'code_quality.fallow.profile', 'strict'],
tmpDir
);
assert.ok(
result.success,
[
'config-set code_quality.fallow.profile=strict must succeed,',
'stdout: ' + result.output,
'stderr: ' + result.error,
].join('\n')
);
});
test('config-set code_quality.fallow.profile=unknown is REJECTED', (t) => {
const tmpDir = createTempProject();
t.after(() => cleanup(tmpDir));
const result = runGsdTools(
['config-set', 'code_quality.fallow.profile', 'unknown'],
tmpDir
);
assert.ok(
!result.success,
'config-set code_quality.fallow.profile=unknown must fail'
);
});
});
});
}