test(08-01): add cmdInitTodos, cmdInitMilestoneOp, cmdInitPhaseOp fallback tests
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user