test(08-02): add cmdInitProgress, cmdInitQuick, cmdInitMapCodebase, cmdInitNewProject, cmdInitNewMilestone tests
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user