/** * GSD Tools Tests - Milestone */ 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'); describe('milestone complete command', () => { let tmpDir; beforeEach(() => { tmpDir = createTempProject(); }); afterEach(() => { cleanup(tmpDir); }); test('archives roadmap, requirements, creates MILESTONES.md', () => { fs.writeFileSync( path.join(tmpDir, '.planning', 'ROADMAP.md'), `# Roadmap v1.0 MVP\n\n### Phase 1: Foundation\n**Goal:** Setup\n` ); fs.writeFileSync( path.join(tmpDir, '.planning', 'REQUIREMENTS.md'), `# Requirements\n\n- [ ] User auth\n- [ ] Dashboard\n` ); fs.writeFileSync( path.join(tmpDir, '.planning', 'STATE.md'), `# State\n\n**Status:** In progress\n**Last Activity:** 2025-01-01\n**Last Activity Description:** Working\n` ); const p1 = path.join(tmpDir, '.planning', 'phases', '01-foundation'); fs.mkdirSync(p1, { recursive: true }); fs.writeFileSync( path.join(p1, '01-01-SUMMARY.md'), `---\none-liner: Set up project infrastructure\n---\n# Summary\n` ); const result = runGsdTools('milestone complete v1.0 --name MVP Foundation', tmpDir); assert.ok(result.success, `Command failed: ${result.error}`); const output = JSON.parse(result.output); assert.strictEqual(output.version, 'v1.0'); assert.strictEqual(output.phases, 1); assert.ok(output.archived.roadmap, 'roadmap should be archived'); assert.ok(output.archived.requirements, 'requirements should be archived'); // Verify archive files exist assert.ok( fs.existsSync(path.join(tmpDir, '.planning', 'milestones', 'v1.0-ROADMAP.md')), 'archived roadmap should exist' ); assert.ok( fs.existsSync(path.join(tmpDir, '.planning', 'milestones', 'v1.0-REQUIREMENTS.md')), 'archived requirements should exist' ); // Verify MILESTONES.md created assert.ok( fs.existsSync(path.join(tmpDir, '.planning', 'MILESTONES.md')), 'MILESTONES.md should be created' ); const milestones = fs.readFileSync(path.join(tmpDir, '.planning', 'MILESTONES.md'), 'utf-8'); assert.ok(milestones.includes('v1.0 MVP Foundation'), 'milestone entry should contain name'); assert.ok(milestones.includes('Set up project infrastructure'), 'accomplishments should be listed'); }); test('appends to existing MILESTONES.md', () => { fs.writeFileSync( path.join(tmpDir, '.planning', 'MILESTONES.md'), `# Milestones\n\n## v0.9 Alpha (Shipped: 2025-01-01)\n\n---\n\n` ); fs.writeFileSync( path.join(tmpDir, '.planning', 'ROADMAP.md'), `# Roadmap v1.0\n` ); fs.writeFileSync( path.join(tmpDir, '.planning', 'STATE.md'), `# State\n\n**Status:** In progress\n**Last Activity:** 2025-01-01\n**Last Activity Description:** Working\n` ); const result = runGsdTools('milestone complete v1.0 --name Beta', tmpDir); assert.ok(result.success, `Command failed: ${result.error}`); const milestones = fs.readFileSync(path.join(tmpDir, '.planning', 'MILESTONES.md'), 'utf-8'); assert.ok(milestones.includes('v0.9 Alpha'), 'existing entry should be preserved'); assert.ok(milestones.includes('v1.0 Beta'), 'new entry should be appended'); }); test('archives phase directories with --archive-phases flag', () => { fs.writeFileSync( path.join(tmpDir, '.planning', 'ROADMAP.md'), `# Roadmap v1.0\n` ); fs.writeFileSync( path.join(tmpDir, '.planning', 'STATE.md'), `# State\n\n**Status:** In progress\n**Last Activity:** 2025-01-01\n**Last Activity Description:** Working\n` ); const p1 = path.join(tmpDir, '.planning', 'phases', '01-foundation'); fs.mkdirSync(p1, { recursive: true }); fs.writeFileSync( path.join(p1, '01-01-SUMMARY.md'), `---\none-liner: Set up project infrastructure\n---\n# Summary\n` ); const result = runGsdTools('milestone complete v1.0 --name MVP --archive-phases', tmpDir); assert.ok(result.success, `Command failed: ${result.error}`); const output = JSON.parse(result.output); assert.strictEqual(output.archived.phases, true, 'phases should be archived'); // Phase directory moved to milestones/v1.0-phases/ assert.ok( fs.existsSync(path.join(tmpDir, '.planning', 'milestones', 'v1.0-phases', '01-foundation')), 'archived phase directory should exist in milestones/v1.0-phases/' ); // Original phase directory no longer exists assert.ok( !fs.existsSync(p1), 'original phase directory should no longer exist' ); }); test('archived REQUIREMENTS.md contains archive header', () => { fs.writeFileSync( path.join(tmpDir, '.planning', 'REQUIREMENTS.md'), `# Requirements\n\n- [ ] **TEST-01**: core.cjs has tests\n- [ ] **TEST-02**: more tests\n` ); fs.writeFileSync( path.join(tmpDir, '.planning', 'ROADMAP.md'), `# Roadmap v1.0\n` ); fs.writeFileSync( path.join(tmpDir, '.planning', 'STATE.md'), `# State\n\n**Status:** In progress\n**Last Activity:** 2025-01-01\n**Last Activity Description:** Working\n` ); const result = runGsdTools('milestone complete v1.0 --name MVP', tmpDir); assert.ok(result.success, `Command failed: ${result.error}`); const archivedReq = fs.readFileSync( path.join(tmpDir, '.planning', 'milestones', 'v1.0-REQUIREMENTS.md'), 'utf-8' ); assert.ok(archivedReq.includes('Requirements Archive: v1.0'), 'should contain archive version'); assert.ok(archivedReq.includes('SHIPPED'), 'should contain SHIPPED status'); assert.ok(archivedReq.includes('Archived:'), 'should contain Archived: date line'); // Original content preserved after header assert.ok(archivedReq.includes('# Requirements'), 'original content should be preserved'); assert.ok(archivedReq.includes('**TEST-01**'), 'original requirement items should be preserved'); }); test('STATE.md gets updated during milestone complete', () => { fs.writeFileSync( path.join(tmpDir, '.planning', 'ROADMAP.md'), `# Roadmap v1.0\n` ); fs.writeFileSync( path.join(tmpDir, '.planning', 'STATE.md'), `# State\n\n**Status:** In progress\n**Last Activity:** 2025-01-01\n**Last Activity Description:** Working\n` ); const result = runGsdTools('milestone complete v1.0 --name Test', tmpDir); assert.ok(result.success, `Command failed: ${result.error}`); const output = JSON.parse(result.output); assert.strictEqual(output.state_updated, true, 'state_updated should be true'); const state = fs.readFileSync(path.join(tmpDir, '.planning', 'STATE.md'), 'utf-8'); assert.ok(state.includes('v1.0 milestone complete'), 'status should be updated to milestone complete'); assert.ok( state.includes('v1.0 milestone completed and archived'), 'last activity description should reference milestone completion' ); }); test('handles missing ROADMAP.md gracefully', () => { // Only STATE.md — no ROADMAP.md, no REQUIREMENTS.md fs.writeFileSync( path.join(tmpDir, '.planning', 'STATE.md'), `# State\n\n**Status:** In progress\n**Last Activity:** 2025-01-01\n**Last Activity Description:** Working\n` ); const result = runGsdTools('milestone complete v1.0 --name NoRoadmap', tmpDir); assert.ok(result.success, `Command failed: ${result.error}`); const output = JSON.parse(result.output); assert.strictEqual(output.archived.roadmap, false, 'roadmap should not be archived'); assert.strictEqual(output.archived.requirements, false, 'requirements should not be archived'); assert.strictEqual(output.milestones_updated, true, 'MILESTONES.md should still be created'); assert.ok( fs.existsSync(path.join(tmpDir, '.planning', 'MILESTONES.md')), 'MILESTONES.md should be created even without ROADMAP.md' ); }); test('handles empty phases directory', () => { fs.writeFileSync( path.join(tmpDir, '.planning', 'ROADMAP.md'), `# Roadmap v1.0\n` ); fs.writeFileSync( path.join(tmpDir, '.planning', 'STATE.md'), `# State\n\n**Status:** In progress\n**Last Activity:** 2025-01-01\n**Last Activity Description:** Working\n` ); // phases directory exists but is empty (from createTempProject) const result = runGsdTools('milestone complete v1.0 --name EmptyPhases', tmpDir); assert.ok(result.success, `Command failed: ${result.error}`); const output = JSON.parse(result.output); assert.strictEqual(output.phases, 0, 'phase count should be 0'); assert.strictEqual(output.plans, 0, 'plan count should be 0'); assert.strictEqual(output.tasks, 0, 'task count should be 0'); }); }); // ───────────────────────────────────────────────────────────────────────────── // validate consistency command // ─────────────────────────────────────────────────────────────────────────────