diff --git a/tests/init.test.cjs b/tests/init.test.cjs index dabd289ab..6ed299d7a 100644 --- a/tests/init.test.cjs +++ b/tests/init.test.cjs @@ -199,6 +199,297 @@ describe('init commands', () => { }); }); +// ───────────────────────────────────────────────────────────────────────────── +// cmdInitTodos (INIT-01) +// ───────────────────────────────────────────────────────────────────────────── + +describe('cmdInitTodos', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('empty pending dir returns zero count', () => { + fs.mkdirSync(path.join(tmpDir, '.planning', 'todos', 'pending'), { recursive: true }); + + const result = runGsdTools('init todos', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.todo_count, 0); + assert.deepStrictEqual(output.todos, []); + assert.strictEqual(output.pending_dir_exists, true); + }); + + test('missing pending dir returns zero count', () => { + const result = runGsdTools('init todos', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.todo_count, 0); + assert.deepStrictEqual(output.todos, []); + assert.strictEqual(output.pending_dir_exists, false); + }); + + test('multiple todos with fields are read correctly', () => { + const pendingDir = path.join(tmpDir, '.planning', 'todos', 'pending'); + fs.mkdirSync(pendingDir, { recursive: true }); + + fs.writeFileSync(path.join(pendingDir, 'task-1.md'), 'title: Fix bug\narea: backend\ncreated: 2026-02-25'); + fs.writeFileSync(path.join(pendingDir, 'task-2.md'), 'title: Add feature\narea: frontend\ncreated: 2026-02-24'); + fs.writeFileSync(path.join(pendingDir, 'task-3.md'), 'title: Write docs\narea: backend\ncreated: 2026-02-23'); + + const result = runGsdTools('init todos', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.todo_count, 3); + assert.strictEqual(output.todos.length, 3); + + const task1 = output.todos.find(t => t.file === 'task-1.md'); + assert.ok(task1, 'task-1.md should be in todos'); + assert.strictEqual(task1.title, 'Fix bug'); + assert.strictEqual(task1.area, 'backend'); + assert.strictEqual(task1.created, '2026-02-25'); + assert.strictEqual(task1.path, path.join('.planning', 'todos', 'pending', 'task-1.md')); + }); + + test('area filter returns only matching todos', () => { + const pendingDir = path.join(tmpDir, '.planning', 'todos', 'pending'); + fs.mkdirSync(pendingDir, { recursive: true }); + + fs.writeFileSync(path.join(pendingDir, 'task-1.md'), 'title: Fix bug\narea: backend\ncreated: 2026-02-25'); + fs.writeFileSync(path.join(pendingDir, 'task-2.md'), 'title: Add feature\narea: frontend\ncreated: 2026-02-24'); + fs.writeFileSync(path.join(pendingDir, 'task-3.md'), 'title: Write docs\narea: backend\ncreated: 2026-02-23'); + + const result = runGsdTools('init todos backend', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.todo_count, 2); + assert.strictEqual(output.area_filter, 'backend'); + for (const todo of output.todos) { + assert.strictEqual(todo.area, 'backend'); + } + }); + + test('area filter miss returns zero count', () => { + const pendingDir = path.join(tmpDir, '.planning', 'todos', 'pending'); + fs.mkdirSync(pendingDir, { recursive: true }); + + fs.writeFileSync(path.join(pendingDir, 'task-1.md'), 'title: Fix bug\narea: backend\ncreated: 2026-02-25'); + + const result = runGsdTools('init todos nonexistent', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.todo_count, 0); + assert.strictEqual(output.area_filter, 'nonexistent'); + }); + + test('malformed file uses defaults', () => { + const pendingDir = path.join(tmpDir, '.planning', 'todos', 'pending'); + fs.mkdirSync(pendingDir, { recursive: true }); + + fs.writeFileSync(path.join(pendingDir, 'broken.md'), 'some random content without fields'); + + const result = runGsdTools('init todos', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.todo_count, 1); + const todo = output.todos[0]; + assert.strictEqual(todo.title, 'Untitled'); + assert.strictEqual(todo.area, 'general'); + assert.strictEqual(todo.created, 'unknown'); + }); + + test('non-md files are ignored', () => { + const pendingDir = path.join(tmpDir, '.planning', 'todos', 'pending'); + fs.mkdirSync(pendingDir, { recursive: true }); + + fs.writeFileSync(path.join(pendingDir, 'task.md'), 'title: Real task\narea: dev\ncreated: 2026-01-01'); + fs.writeFileSync(path.join(pendingDir, 'notes.txt'), 'title: Not a task\narea: dev\ncreated: 2026-01-01'); + + const result = runGsdTools('init todos', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.todo_count, 1); + assert.strictEqual(output.todos[0].file, 'task.md'); + }); +}); + +// ───────────────────────────────────────────────────────────────────────────── +// cmdInitMilestoneOp (INIT-02) +// ───────────────────────────────────────────────────────────────────────────── + +describe('cmdInitMilestoneOp', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('no phase directories returns zero counts', () => { + const result = runGsdTools('init milestone-op', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.phase_count, 0); + assert.strictEqual(output.completed_phases, 0); + assert.strictEqual(output.all_phases_complete, false); + }); + + test('multiple phases with no summaries', () => { + const phase1 = path.join(tmpDir, '.planning', 'phases', '01-setup'); + const phase2 = path.join(tmpDir, '.planning', 'phases', '02-api'); + fs.mkdirSync(phase1, { recursive: true }); + fs.mkdirSync(phase2, { recursive: true }); + fs.writeFileSync(path.join(phase1, '01-01-PLAN.md'), '# Plan'); + fs.writeFileSync(path.join(phase2, '02-01-PLAN.md'), '# Plan'); + + const result = runGsdTools('init milestone-op', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.phase_count, 2); + assert.strictEqual(output.completed_phases, 0); + assert.strictEqual(output.all_phases_complete, false); + }); + + test('mix of complete and incomplete phases', () => { + const phase1 = path.join(tmpDir, '.planning', 'phases', '01-setup'); + const phase2 = path.join(tmpDir, '.planning', 'phases', '02-api'); + fs.mkdirSync(phase1, { recursive: true }); + fs.mkdirSync(phase2, { recursive: true }); + fs.writeFileSync(path.join(phase1, '01-01-PLAN.md'), '# Plan'); + fs.writeFileSync(path.join(phase1, '01-01-SUMMARY.md'), '# Summary'); + fs.writeFileSync(path.join(phase2, '02-01-PLAN.md'), '# Plan'); + + const result = runGsdTools('init milestone-op', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.phase_count, 2); + assert.strictEqual(output.completed_phases, 1); + assert.strictEqual(output.all_phases_complete, false); + }); + + test('all phases complete', () => { + 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 milestone-op', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.phase_count, 1); + assert.strictEqual(output.completed_phases, 1); + assert.strictEqual(output.all_phases_complete, true); + }); + + test('archive directory scanning', () => { + fs.mkdirSync(path.join(tmpDir, '.planning', 'archive', 'v1.0'), { recursive: true }); + fs.mkdirSync(path.join(tmpDir, '.planning', 'archive', 'v0.9'), { recursive: true }); + + const result = runGsdTools('init milestone-op', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.archive_count, 2); + assert.strictEqual(output.archived_milestones.length, 2); + }); + + test('no archive directory returns empty', () => { + const result = runGsdTools('init milestone-op', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.archive_count, 0); + assert.deepStrictEqual(output.archived_milestones, []); + }); +}); + +// ───────────────────────────────────────────────────────────────────────────── +// cmdInitPhaseOp fallback (INIT-04) +// ───────────────────────────────────────────────────────────────────────────── + +describe('cmdInitPhaseOp fallback', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('normal path with existing directory', () => { + const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api'); + fs.mkdirSync(phaseDir, { recursive: true }); + fs.writeFileSync(path.join(phaseDir, '03-CONTEXT.md'), '# Context'); + fs.writeFileSync(path.join(phaseDir, '03-01-PLAN.md'), '# Plan'); + fs.writeFileSync( + path.join(tmpDir, '.planning', 'ROADMAP.md'), + '# Roadmap\n\n### Phase 3: API\n**Goal:** Build API\n**Plans:** 1 plans\n' + ); + + const result = runGsdTools('init phase-op 3', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.phase_found, true); + assert.ok(output.phase_dir.includes('03-api'), 'phase_dir should contain 03-api'); + assert.strictEqual(output.has_context, true); + assert.strictEqual(output.has_plans, true); + }); + + test('fallback to ROADMAP when no directory exists', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'ROADMAP.md'), + '# Roadmap\n\n### Phase 5: Widget Builder\n**Goal:** Build widgets\n**Plans:** TBD\n' + ); + + const result = runGsdTools('init phase-op 5', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.phase_found, true); + assert.strictEqual(output.phase_dir, null); + assert.strictEqual(output.phase_slug, 'widget-builder'); + assert.strictEqual(output.has_research, false); + assert.strictEqual(output.has_context, false); + assert.strictEqual(output.has_plans, false); + }); + + test('neither directory nor roadmap entry returns not found', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'ROADMAP.md'), + '# Roadmap\n\n### Phase 1: Setup\n**Goal:** Setup project\n**Plans:** TBD\n' + ); + + const result = runGsdTools('init phase-op 99', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.phase_found, false); + assert.strictEqual(output.phase_dir, null); + }); +}); + // ───────────────────────────────────────────────────────────────────────────── // roadmap analyze command // ─────────────────────────────────────────────────────────────────────────────