diff --git a/tests/init.test.cjs b/tests/init.test.cjs index 6ed299d7a..3069ab7b5 100644 --- a/tests/init.test.cjs +++ b/tests/init.test.cjs @@ -490,6 +490,359 @@ describe('cmdInitPhaseOp fallback', () => { }); }); +// ───────────────────────────────────────────────────────────────────────────── +// cmdInitProgress (INIT-03) +// ───────────────────────────────────────────────────────────────────────────── + +describe('cmdInitProgress', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('no phases returns empty state', () => { + const result = runGsdTools('init progress', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.phase_count, 0); + assert.deepStrictEqual(output.phases, []); + assert.strictEqual(output.current_phase, null); + assert.strictEqual(output.next_phase, null); + assert.strictEqual(output.has_work_in_progress, false); + }); + + test('multiple phases with mixed statuses', () => { + // Phase 01: complete (has plan + summary) + const phase1 = path.join(tmpDir, '.planning', 'phases', '01-setup'); + fs.mkdirSync(phase1, { recursive: true }); + fs.writeFileSync(path.join(phase1, '01-01-PLAN.md'), '# Plan'); + fs.writeFileSync(path.join(phase1, '01-01-SUMMARY.md'), '# Summary'); + + // Phase 02: in_progress (has plan, no summary) + const phase2 = path.join(tmpDir, '.planning', 'phases', '02-api'); + fs.mkdirSync(phase2, { recursive: true }); + fs.writeFileSync(path.join(phase2, '02-01-PLAN.md'), '# Plan'); + + // Phase 03: pending (no plan, no research) + const phase3 = path.join(tmpDir, '.planning', 'phases', '03-ui'); + fs.mkdirSync(phase3, { recursive: true }); + fs.writeFileSync(path.join(phase3, '03-CONTEXT.md'), '# Context'); + + const result = runGsdTools('init progress', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.phase_count, 3); + assert.strictEqual(output.completed_count, 1); + assert.strictEqual(output.in_progress_count, 1); + assert.strictEqual(output.has_work_in_progress, true); + + assert.strictEqual(output.current_phase.number, '02'); + assert.strictEqual(output.current_phase.status, 'in_progress'); + + assert.strictEqual(output.next_phase.number, '03'); + assert.strictEqual(output.next_phase.status, 'pending'); + + // Verify phase entries have expected structure + const p1 = output.phases.find(p => p.number === '01'); + assert.strictEqual(p1.status, 'complete'); + assert.strictEqual(p1.plan_count, 1); + assert.strictEqual(p1.summary_count, 1); + }); + + test('researched status detected correctly', () => { + const phase1 = path.join(tmpDir, '.planning', 'phases', '01-setup'); + fs.mkdirSync(phase1, { recursive: true }); + fs.writeFileSync(path.join(phase1, '01-RESEARCH.md'), '# Research'); + + const result = runGsdTools('init progress', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + const p1 = output.phases.find(p => p.number === '01'); + assert.strictEqual(p1.status, 'researched'); + assert.strictEqual(p1.has_research, true); + assert.strictEqual(output.current_phase.number, '01'); + }); + + test('all phases complete returns no current or next', () => { + const phase1 = path.join(tmpDir, '.planning', 'phases', '01-setup'); + fs.mkdirSync(phase1, { recursive: true }); + fs.writeFileSync(path.join(phase1, '01-01-PLAN.md'), '# Plan'); + fs.writeFileSync(path.join(phase1, '01-01-SUMMARY.md'), '# Summary'); + + const result = runGsdTools('init progress', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.completed_count, 1); + assert.strictEqual(output.current_phase, null); + assert.strictEqual(output.next_phase, null); + }); + + test('paused_at detected from STATE.md', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'STATE.md'), + '# Project State\n\n**Paused At:** Phase 2, Task 3 — implementing auth\n' + ); + + const result = runGsdTools('init progress', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.ok(output.paused_at, 'paused_at should be set'); + assert.ok(output.paused_at.includes('Phase 2, Task 3'), 'paused_at should contain pause location'); + }); + + test('no paused_at when STATE.md has no pause line', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'STATE.md'), + '# Project State\n\nSome content without pause.\n' + ); + + const result = runGsdTools('init progress', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.paused_at, null); + }); +}); + +// ───────────────────────────────────────────────────────────────────────────── +// cmdInitQuick (INIT-05) +// ───────────────────────────────────────────────────────────────────────────── + +describe('cmdInitQuick', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('with description generates slug and task_dir', () => { + const result = runGsdTools('init quick "Fix login bug"', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.slug, 'fix-login-bug'); + assert.strictEqual(output.next_num, 1); + assert.strictEqual(output.task_dir, '.planning/quick/1-fix-login-bug'); + assert.strictEqual(output.description, 'Fix login bug'); + }); + + test('without description returns null slug and task_dir', () => { + const result = runGsdTools('init quick', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.slug, null); + assert.strictEqual(output.task_dir, null); + assert.strictEqual(output.description, null); + assert.strictEqual(output.next_num, 1); + }); + + test('next number increments from existing entries', () => { + const quickDir = path.join(tmpDir, '.planning', 'quick'); + fs.mkdirSync(path.join(quickDir, '1-old-task'), { recursive: true }); + fs.mkdirSync(path.join(quickDir, '3-another-task'), { recursive: true }); + + const result = runGsdTools('init quick "New task"', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.next_num, 4); + }); + + test('long description truncates slug to 40 chars', () => { + const result = runGsdTools('init quick "This is a very long description that should get truncated to forty characters maximum"', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.ok(output.slug.length <= 40, `Slug should be <= 40 chars, got ${output.slug.length}: "${output.slug}"`); + }); +}); + +// ───────────────────────────────────────────────────────────────────────────── +// cmdInitMapCodebase (INIT-05) +// ───────────────────────────────────────────────────────────────────────────── + +describe('cmdInitMapCodebase', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('no codebase dir returns empty', () => { + const result = runGsdTools('init map-codebase', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.has_maps, false); + assert.deepStrictEqual(output.existing_maps, []); + assert.strictEqual(output.codebase_dir_exists, false); + }); + + test('with existing maps lists md files only', () => { + const codebaseDir = path.join(tmpDir, '.planning', 'codebase'); + fs.mkdirSync(codebaseDir, { recursive: true }); + fs.writeFileSync(path.join(codebaseDir, 'STACK.md'), '# Stack'); + fs.writeFileSync(path.join(codebaseDir, 'ARCHITECTURE.md'), '# Architecture'); + fs.writeFileSync(path.join(codebaseDir, 'notes.txt'), 'not a markdown file'); + + const result = runGsdTools('init map-codebase', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.has_maps, true); + assert.strictEqual(output.existing_maps.length, 2); + assert.ok(output.existing_maps.includes('STACK.md'), 'Should include STACK.md'); + assert.ok(output.existing_maps.includes('ARCHITECTURE.md'), 'Should include ARCHITECTURE.md'); + }); + + test('empty codebase dir returns no maps', () => { + const codebaseDir = path.join(tmpDir, '.planning', 'codebase'); + fs.mkdirSync(codebaseDir, { recursive: true }); + + const result = runGsdTools('init map-codebase', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.has_maps, false); + assert.deepStrictEqual(output.existing_maps, []); + assert.strictEqual(output.codebase_dir_exists, true); + }); +}); + +// ───────────────────────────────────────────────────────────────────────────── +// cmdInitNewProject (INIT-06) +// ───────────────────────────────────────────────────────────────────────────── + +describe('cmdInitNewProject', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('greenfield project with no code', () => { + const result = runGsdTools('init new-project', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.has_existing_code, false); + assert.strictEqual(output.has_package_file, false); + assert.strictEqual(output.is_brownfield, false); + assert.strictEqual(output.needs_codebase_map, false); + }); + + test('brownfield with package.json detected', () => { + fs.writeFileSync(path.join(tmpDir, 'package.json'), '{"name":"test"}'); + + const result = runGsdTools('init new-project', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.has_package_file, true); + assert.strictEqual(output.is_brownfield, true); + assert.strictEqual(output.needs_codebase_map, true); + }); + + test('brownfield with codebase map does not need map', () => { + fs.writeFileSync(path.join(tmpDir, 'package.json'), '{"name":"test"}'); + fs.mkdirSync(path.join(tmpDir, '.planning', 'codebase'), { recursive: true }); + + const result = runGsdTools('init new-project', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.is_brownfield, true); + assert.strictEqual(output.needs_codebase_map, false); + }); + + test('planning_exists flag is correct', () => { + const result = runGsdTools('init new-project', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.planning_exists, true); + }); +}); + +// ───────────────────────────────────────────────────────────────────────────── +// cmdInitNewMilestone (INIT-06) +// ───────────────────────────────────────────────────────────────────────────── + +describe('cmdInitNewMilestone', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('returns expected fields', () => { + const result = runGsdTools('init new-milestone', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.ok('current_milestone' in output, 'Should have current_milestone'); + assert.ok('current_milestone_name' in output, 'Should have current_milestone_name'); + assert.ok('researcher_model' in output, 'Should have researcher_model'); + assert.ok('synthesizer_model' in output, 'Should have synthesizer_model'); + assert.ok('roadmapper_model' in output, 'Should have roadmapper_model'); + assert.ok('commit_docs' in output, 'Should have commit_docs'); + assert.strictEqual(output.project_path, '.planning/PROJECT.md'); + assert.strictEqual(output.roadmap_path, '.planning/ROADMAP.md'); + assert.strictEqual(output.state_path, '.planning/STATE.md'); + }); + + test('file existence flags reflect actual state', () => { + // Default: no STATE.md, ROADMAP.md, or PROJECT.md + const result1 = runGsdTools('init new-milestone', tmpDir); + assert.ok(result1.success, `Command failed: ${result1.error}`); + + const output1 = JSON.parse(result1.output); + assert.strictEqual(output1.state_exists, false); + assert.strictEqual(output1.roadmap_exists, false); + assert.strictEqual(output1.project_exists, false); + + // Create files and verify flags change + fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), '# State'); + fs.writeFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), '# Roadmap'); + fs.writeFileSync(path.join(tmpDir, '.planning', 'PROJECT.md'), '# Project'); + + const result2 = runGsdTools('init new-milestone', tmpDir); + assert.ok(result2.success, `Command failed: ${result2.error}`); + + const output2 = JSON.parse(result2.output); + assert.strictEqual(output2.state_exists, true); + assert.strictEqual(output2.roadmap_exists, true); + assert.strictEqual(output2.project_exists, true); + }); +}); + // ───────────────────────────────────────────────────────────────────────────── // roadmap analyze command // ─────────────────────────────────────────────────────────────────────────────