/**
* GSD Tools Tests - Verify
*/
const { test, describe, beforeEach, afterEach } = require('node:test');
const assert = require('node:assert');
const fs = require('fs');
const path = require('path');
const { runGsdTools, createTempProject, cleanup } = require('./helpers.cjs');
// ─── helpers ──────────────────────────────────────────────────────────────────
// Build a minimal valid PLAN.md content with all required frontmatter fields
function validPlanContent({ wave = 1, dependsOn = '[]', autonomous = 'true', extraTasks = '' } = {}) {
return [
'---',
'phase: 01-test',
'plan: 01',
'type: execute',
`wave: ${wave}`,
`depends_on: ${dependsOn}`,
'files_modified: [some/file.ts]',
`autonomous: ${autonomous}`,
'must_haves:',
' truths:',
' - "something is true"',
'---',
'',
'',
'',
'',
' Task 1: Do something',
' some/file.ts',
' Do the thing',
' echo ok',
' Thing is done',
'',
extraTasks,
'',
'',
].join('\n');
}
describe('validate consistency command', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
});
afterEach(() => {
cleanup(tmpDir);
});
test('passes for consistent project', () => {
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
`# Roadmap\n### Phase 1: A\n### Phase 2: B\n### Phase 3: C\n`
);
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-a'), { recursive: true });
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '02-b'), { recursive: true });
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '03-c'), { recursive: true });
const result = runGsdTools('validate consistency', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.strictEqual(output.passed, true, 'should pass');
assert.strictEqual(output.warning_count, 0, 'no warnings');
});
test('warns about phase on disk but not in roadmap', () => {
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
`# Roadmap\n### Phase 1: A\n`
);
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-a'), { recursive: true });
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '02-orphan'), { recursive: true });
const result = runGsdTools('validate consistency', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.ok(output.warning_count > 0, 'should have warnings');
assert.ok(
output.warnings.some(w => w.includes('disk but not in ROADMAP')),
'should warn about orphan directory'
);
});
test('warns about gaps in phase numbering', () => {
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
`# Roadmap\n### Phase 1: A\n### Phase 3: C\n`
);
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-a'), { recursive: true });
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '03-c'), { recursive: true });
const result = runGsdTools('validate consistency', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.ok(
output.warnings.some(w => w.includes('Gap in phase numbering')),
'should warn about gap'
);
});
});
// ─────────────────────────────────────────────────────────────────────────────
// verify plan-structure command
// ─────────────────────────────────────────────────────────────────────────────
describe('verify plan-structure command', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-test'), { recursive: true });
});
afterEach(() => {
cleanup(tmpDir);
});
test('reports missing required frontmatter fields', () => {
const planPath = path.join(tmpDir, '.planning', 'phases', '01-test', '01-01-PLAN.md');
fs.writeFileSync(planPath, '# No frontmatter here\n\nJust a plan without YAML.\n');
const result = runGsdTools('verify plan-structure .planning/phases/01-test/01-01-PLAN.md', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.strictEqual(output.valid, false, 'should be invalid');
assert.ok(
output.errors.some(e => e.includes('Missing required frontmatter field')),
`Expected "Missing required frontmatter field" in errors: ${JSON.stringify(output.errors)}`
);
});
test('validates complete plan with all required fields and tasks', () => {
const planPath = path.join(tmpDir, '.planning', 'phases', '01-test', '01-01-PLAN.md');
fs.writeFileSync(planPath, validPlanContent());
const result = runGsdTools('verify plan-structure .planning/phases/01-test/01-01-PLAN.md', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.strictEqual(output.valid, true, `should be valid, errors: ${JSON.stringify(output.errors)}`);
assert.deepStrictEqual(output.errors, [], 'should have no errors');
assert.strictEqual(output.task_count, 1, 'should have 1 task');
});
test('reports task missing name element', () => {
const content = [
'---',
'phase: 01-test',
'plan: 01',
'type: execute',
'wave: 1',
'depends_on: []',
'files_modified: [some/file.ts]',
'autonomous: true',
'must_haves:',
' truths:',
' - "something"',
'---',
'',
'',
'',
' Do it',
' echo ok',
' Done',
'',
'',
].join('\n');
const planPath = path.join(tmpDir, '.planning', 'phases', '01-test', '01-01-PLAN.md');
fs.writeFileSync(planPath, content);
const result = runGsdTools('verify plan-structure .planning/phases/01-test/01-01-PLAN.md', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.ok(
output.errors.some(e => e.includes('Task missing ')),
`Expected "Task missing " in errors: ${JSON.stringify(output.errors)}`
);
});
test('reports task missing action element', () => {
const content = [
'---',
'phase: 01-test',
'plan: 01',
'type: execute',
'wave: 1',
'depends_on: []',
'files_modified: [some/file.ts]',
'autonomous: true',
'must_haves:',
' truths:',
' - "something"',
'---',
'',
'',
'',
' Task 1: No action',
' echo ok',
' Done',
'',
'',
].join('\n');
const planPath = path.join(tmpDir, '.planning', 'phases', '01-test', '01-01-PLAN.md');
fs.writeFileSync(planPath, content);
const result = runGsdTools('verify plan-structure .planning/phases/01-test/01-01-PLAN.md', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.ok(
output.errors.some(e => e.includes('missing ')),
`Expected "missing " in errors: ${JSON.stringify(output.errors)}`
);
});
test('warns about wave > 1 with empty depends_on', () => {
const planPath = path.join(tmpDir, '.planning', 'phases', '01-test', '01-01-PLAN.md');
fs.writeFileSync(planPath, validPlanContent({ wave: 2, dependsOn: '[]' }));
const result = runGsdTools('verify plan-structure .planning/phases/01-test/01-01-PLAN.md', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.ok(
output.warnings.some(w => w.includes('Wave > 1 but depends_on is empty')),
`Expected "Wave > 1 but depends_on is empty" in warnings: ${JSON.stringify(output.warnings)}`
);
});
test('errors when checkpoint task but autonomous is true', () => {
const content = [
'---',
'phase: 01-test',
'plan: 01',
'type: execute',
'wave: 1',
'depends_on: []',
'files_modified: [some/file.ts]',
'autonomous: true',
'must_haves:',
' truths:',
' - "something"',
'---',
'',
'',
'',
' Task 1: Normal',
' some/file.ts',
' Do it',
' echo ok',
' Done',
'',
'',
' Task 2: Verify UI',
' some/file.ts',
' Check the UI',
' Visit the app',
' UI verified',
'',
'',
].join('\n');
const planPath = path.join(tmpDir, '.planning', 'phases', '01-test', '01-01-PLAN.md');
fs.writeFileSync(planPath, content);
const result = runGsdTools('verify plan-structure .planning/phases/01-test/01-01-PLAN.md', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.ok(
output.errors.some(e => e.includes('checkpoint tasks but autonomous is not false')),
`Expected checkpoint/autonomous error in errors: ${JSON.stringify(output.errors)}`
);
});
test('returns error for nonexistent file', () => {
const result = runGsdTools('verify plan-structure .planning/phases/01-test/nonexistent.md', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.ok(output.error, `Expected error field in output: ${JSON.stringify(output)}`);
assert.ok(
output.error.includes('File not found'),
`Expected "File not found" in error: ${output.error}`
);
});
});
// ─────────────────────────────────────────────────────────────────────────────
// verify phase-completeness command
// ─────────────────────────────────────────────────────────────────────────────
describe('verify phase-completeness command', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
// Create ROADMAP.md referencing phase 01 so findPhaseInternal can locate it
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
'# Roadmap\n\n### Phase 1: Test\n**Goal**: Test phase\n'
);
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-test'), { recursive: true });
});
afterEach(() => {
cleanup(tmpDir);
});
test('reports complete phase with matching plans and summaries', () => {
const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-test');
fs.writeFileSync(path.join(phaseDir, '01-01-PLAN.md'), '# Plan\n');
fs.writeFileSync(path.join(phaseDir, '01-01-SUMMARY.md'), '# Summary\n');
const result = runGsdTools('verify phase-completeness 01', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.strictEqual(output.complete, true, `should be complete, errors: ${JSON.stringify(output.errors)}`);
assert.strictEqual(output.plan_count, 1, 'should have 1 plan');
assert.strictEqual(output.summary_count, 1, 'should have 1 summary');
assert.deepStrictEqual(output.incomplete_plans, [], 'should have no incomplete plans');
});
test('reports incomplete phase with plan missing summary', () => {
const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-test');
fs.writeFileSync(path.join(phaseDir, '01-01-PLAN.md'), '# Plan\n');
const result = runGsdTools('verify phase-completeness 01', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.strictEqual(output.complete, false, 'should be incomplete');
assert.ok(
output.incomplete_plans.some(id => id.includes('01-01')),
`Expected "01-01" in incomplete_plans: ${JSON.stringify(output.incomplete_plans)}`
);
assert.ok(
output.errors.some(e => e.includes('Plans without summaries')),
`Expected "Plans without summaries" in errors: ${JSON.stringify(output.errors)}`
);
});
test('warns about orphan summaries', () => {
const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-test');
fs.writeFileSync(path.join(phaseDir, '01-01-SUMMARY.md'), '# Summary\n');
const result = runGsdTools('verify phase-completeness 01', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.ok(
output.warnings.some(w => w.includes('Summaries without plans')),
`Expected "Summaries without plans" in warnings: ${JSON.stringify(output.warnings)}`
);
});
test('returns error for nonexistent phase', () => {
const result = runGsdTools('verify phase-completeness 99', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.ok(output.error, `Expected error field in output: ${JSON.stringify(output)}`);
});
});
// ─────────────────────────────────────────────────────────────────────────────
// progress command
// ─────────────────────────────────────────────────────────────────────────────