test(#1189): cover ADR-0006 planningPaths() consumption in init handlers (#1226)

Add a black-box regression guard (8 tests in tests/init.test.cjs) asserting the init handlers (execute-phase, plan-phase, phase-op, milestone-op) resolve workstream-scoped planning paths under GSD_WORKSTREAM via planningPaths()/planningDir(), never the flat .planning form.

Each positive case asserts both the scoped value and not-equal-to-flat (genuine guard, not coverage credit); fixtures seeded workstream-scoped; every CLI call pins GSD_WORKSTREAM and GSD_PROJECT for hermeticity. Verified by mutation (flat join fails exactly the 4 positive assertions) and re-verified under a polluted parent env. Test-only; src/ unmodified.

Closes #1189

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-06-14 14:20:47 -04:00
committed by GitHub
parent 73b7f45140
commit 2ae5fcdcf8

View File

@@ -1654,6 +1654,229 @@ describe('withProjectRoot project identity', () => {
});
});
// ─────────────────────────────────────────────────────────────────────────────
// ADR-0006: init handlers honor GSD_WORKSTREAM (planningPaths/planningDir consumption)
// Issue #1189 — regression guard: workstream-scoped paths must be resolved
// through planningDir(cwd) which picks up GSD_WORKSTREAM from env.
// ─────────────────────────────────────────────────────────────────────────────
describe('init handlers honor GSD_WORKSTREAM (ADR-0006 planningPaths consumption)', () => {
const { seedWorkstream } = require('./fixtures/index.cjs');
/**
* Build a workstream-scoped fixture under tmpDir.
* Only workstream-scoped files exist; flat .planning/ has only the phases dir.
*/
function buildWsFixture(tmpDir, ws = 'wsx') {
const wsDir = seedWorkstream(tmpDir, { name: ws });
// Write the planning files ONLY under the workstream path
fs.writeFileSync(
path.join(wsDir, 'STATE.md'),
'# State\n'
);
fs.writeFileSync(
path.join(wsDir, 'ROADMAP.md'),
'# Roadmap\n\n### Phase 1: Setup\n**Goal:** Bootstrap\n**Requirements**: R-01\n**Plans:** 1 plans\n'
);
fs.writeFileSync(
path.join(wsDir, 'config.json'),
JSON.stringify({})
);
// Create a phase plan under the workstream
fs.mkdirSync(path.join(wsDir, 'phases', '01-setup'), { recursive: true });
fs.writeFileSync(path.join(wsDir, 'phases', '01-setup', '01-01-PLAN.md'), '# Plan\n');
return wsDir;
}
// ── Test 1: execute-phase — workstream-scoped path fields (happy) ─────────
describe('execute-phase — workstream-scoped path fields', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createFixture();
buildWsFixture(tmpDir, 'wsx');
});
afterEach(() => {
cleanup(tmpDir);
});
test('execute-phase emits workstream-scoped state/roadmap/config paths (ADR-0006)', () => {
const result = runGsdTools('init execute-phase 1', tmpDir, { GSD_WORKSTREAM: 'wsx', GSD_PROJECT: '' });
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
// Positive: paths must be workstream-scoped
assert.strictEqual(output.state_path, '.planning/workstreams/wsx/STATE.md');
assert.strictEqual(output.roadmap_path, '.planning/workstreams/wsx/ROADMAP.md');
assert.strictEqual(output.config_path, '.planning/workstreams/wsx/config.json');
// Goodhart both-directions: must NOT be the flat form
assert.notStrictEqual(output.state_path, '.planning/STATE.md');
assert.notStrictEqual(output.roadmap_path, '.planning/ROADMAP.md');
assert.notStrictEqual(output.config_path, '.planning/config.json');
// phase_dir is emitted and must be workstream-scoped
assert.ok(
output.phase_dir && output.phase_dir.includes('workstreams/wsx'),
`phase_dir should include workstreams/wsx, got: ${output.phase_dir}`
);
});
test('execute-phase WITHOUT GSD_WORKSTREAM resolves flat paths (boundary control)', () => {
// Flat fixture: the workstream fixture exists but we do NOT pass GSD_WORKSTREAM.
// Handler should resolve flat .planning/ → state/roadmap/config are flat,
// and the workstream phase is NOT found (flat phases/ is empty).
const result = runGsdTools('init execute-phase 1', tmpDir, { GSD_WORKSTREAM: '', GSD_PROJECT: '' });
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
// Flat paths must be returned when no workstream is active
assert.strictEqual(output.state_path, '.planning/STATE.md');
assert.strictEqual(output.roadmap_path, '.planning/ROADMAP.md');
assert.strictEqual(output.config_path, '.planning/config.json');
// Phase is NOT found in flat .planning/phases/ (only exists under workstream)
assert.strictEqual(output.phase_found, false,
'phase should not be found in flat path when only workstream fixture exists');
});
});
// ── Test 2: milestone-op — reads workstream-scoped roadmap/state/phases ───
describe('milestone-op — reads workstream-scoped planning files', () => {
let tmpDir;
beforeEach(() => {
// Do NOT call createFixture (which would add flat .planning/phases/).
// Create a bare temp dir so files only exist under the workstream path.
const os = require('os');
tmpDir = fs.mkdtempSync(require('path').join(os.tmpdir(), 'gsd-test-'));
buildWsFixture(tmpDir, 'wsx');
});
afterEach(() => {
cleanup(tmpDir);
});
test('milestone-op with GSD_WORKSTREAM finds roadmap/state/phases in workstream scope (ADR-0006)', () => {
const result = runGsdTools('init milestone-op', tmpDir, { GSD_WORKSTREAM: 'wsx', GSD_PROJECT: '' });
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.strictEqual(output.roadmap_exists, true,
'roadmap_exists must be true: ROADMAP.md exists only under workstream path');
assert.strictEqual(output.state_exists, true,
'state_exists must be true: STATE.md exists only under workstream path');
assert.strictEqual(output.phases_dir_exists, true,
'phases_dir_exists must be true: phases/ exists under workstream path');
});
test('milestone-op WITHOUT GSD_WORKSTREAM misses workstream-only files (negative discrimination)', () => {
const result = runGsdTools('init milestone-op', tmpDir, { GSD_WORKSTREAM: '', GSD_PROJECT: '' });
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
// Handler looked at flat .planning/ — no files there → all false
assert.strictEqual(output.roadmap_exists, false,
'roadmap_exists must be false: ROADMAP.md is only under workstream, not flat .planning/');
assert.strictEqual(output.state_exists, false,
'state_exists must be false: STATE.md is only under workstream, not flat .planning/');
});
});
// ── Test 3: plan-phase — workstream-scoped resolution ────────────────────
describe('plan-phase — workstream-scoped path resolution', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createFixture();
buildWsFixture(tmpDir, 'wsx');
});
afterEach(() => {
cleanup(tmpDir);
});
test('plan-phase emits workstream-scoped state/roadmap/requirements paths (ADR-0006)', () => {
const result = runGsdTools('init plan-phase 1', tmpDir, { GSD_WORKSTREAM: 'wsx', GSD_PROJECT: '' });
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
// Path fields must be scoped to the workstream
assert.strictEqual(output.state_path, '.planning/workstreams/wsx/STATE.md');
assert.strictEqual(output.roadmap_path, '.planning/workstreams/wsx/ROADMAP.md');
assert.strictEqual(output.requirements_path, '.planning/workstreams/wsx/REQUIREMENTS.md');
// Must NOT be flat
assert.notStrictEqual(output.state_path, '.planning/STATE.md');
assert.notStrictEqual(output.roadmap_path, '.planning/ROADMAP.md');
// phase_dir is workstream-scoped and phase is found
assert.strictEqual(output.phase_found, true);
assert.ok(
output.phase_dir && output.phase_dir.includes('workstreams/wsx'),
`phase_dir should include workstreams/wsx, got: ${output.phase_dir}`
);
});
test('plan-phase WITHOUT GSD_WORKSTREAM resolves flat paths (boundary control)', () => {
const result = runGsdTools('init plan-phase 1', tmpDir, { GSD_WORKSTREAM: '', GSD_PROJECT: '' });
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.strictEqual(output.state_path, '.planning/STATE.md');
assert.strictEqual(output.roadmap_path, '.planning/ROADMAP.md');
assert.strictEqual(output.requirements_path, '.planning/REQUIREMENTS.md');
// Phase only exists under workstream, so not found via flat path
assert.strictEqual(output.phase_found, false);
});
});
// ── Test 4: phase-op — workstream-scoped phase resolution ────────────────
describe('phase-op — workstream-scoped phase resolution', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createFixture();
buildWsFixture(tmpDir, 'wsx');
});
afterEach(() => {
cleanup(tmpDir);
});
test('phase-op with GSD_WORKSTREAM finds phase in workstream scope and emits scoped paths (ADR-0006)', () => {
const result = runGsdTools('init phase-op 1', tmpDir, { GSD_WORKSTREAM: 'wsx', GSD_PROJECT: '' });
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
// Phase is found via workstream-scoped phases dir
assert.strictEqual(output.phase_found, true,
'phase_found must be true: phase exists under workstream path');
assert.ok(
output.phase_dir && output.phase_dir.includes('workstreams/wsx'),
`phase_dir should include workstreams/wsx, got: ${output.phase_dir}`
);
// Path fields are workstream-scoped
assert.strictEqual(output.state_path, '.planning/workstreams/wsx/STATE.md');
assert.strictEqual(output.roadmap_path, '.planning/workstreams/wsx/ROADMAP.md');
assert.notStrictEqual(output.state_path, '.planning/STATE.md');
});
test('phase-op WITHOUT GSD_WORKSTREAM does not find workstream-only phase (negative discrimination)', () => {
const result = runGsdTools('init phase-op 1', tmpDir, { GSD_WORKSTREAM: '', GSD_PROJECT: '' });
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
// Phase only exists under workstream path — flat path has no matching phase dir
assert.strictEqual(output.phase_found, false,
'phase_found must be false: phase only exists under workstream path');
// Flat paths are emitted
assert.strictEqual(output.state_path, '.planning/STATE.md');
assert.strictEqual(output.roadmap_path, '.planning/ROADMAP.md');
});
});
});
// ─────────────────────────────────────────────────────────────────────────────
// roadmap analyze command
// ─────────────────────────────────────────────────────────────────────────────