Files
msd-core/tests/milestone.test.cjs
Ethan Hurst 5778ff3e59 test(05-01): add 5 milestone complete archiving and defensive tests
- --archive-phases flag moves phase dirs to milestones/vX.Y-phases/
- archived REQUIREMENTS.md contains header with version, SHIPPED, date
- STATE.md status/activity/description updated during milestone complete
- missing ROADMAP.md handled gracefully without crash
- empty phases directory handled with zero counts
2026-02-26 05:47:19 +10:00

228 lines
9.2 KiB
JavaScript

/**
* 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
// ─────────────────────────────────────────────────────────────────────────────