|
| 1 | +'use strict'; |
| 2 | +// bin/roadmap-health-parser.test.cjs |
| 3 | +// Guards F8 + F3 (dogfooding): |
| 4 | +// F8 — `nf-tools roadmap analyze` returned ZERO phases on a ROADMAP that uses the |
| 5 | +// checklist phase format (`- [x] Phase N: Title`). The phasePattern regexes |
| 6 | +// only matched `## Phase` headings or **bold** checklist items, never a plain |
| 7 | +// `- [x] Phase N:`. Same blind spot in cmdValidateHealth/Consistency/compare. |
| 8 | +// F3 — `nf-tools validate health` emitted ONE W007 per orphan phase (dozens), |
| 9 | +// pinning /nf:health at DEGRADED. The orphans are now collapsed into a single |
| 10 | +// actionable W007. |
| 11 | + |
| 12 | +const { describe, it } = require('node:test'); |
| 13 | +const assert = require('node:assert/strict'); |
| 14 | +const fs = require('fs'); |
| 15 | +const os = require('os'); |
| 16 | +const path = require('path'); |
| 17 | +const { execFileSync } = require('child_process'); |
| 18 | + |
| 19 | +const TOOLS = path.join(__dirname, '..', 'core', 'bin', 'nf-tools.cjs'); |
| 20 | + |
| 21 | +function runTool(args, cwd) { |
| 22 | + const out = execFileSync('node', [TOOLS, ...args], { cwd, encoding: 'utf8' }); |
| 23 | + return JSON.parse(out); |
| 24 | +} |
| 25 | + |
| 26 | +function tmpProject() { |
| 27 | + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'nf-rmhp-')); |
| 28 | + fs.mkdirSync(path.join(dir, '.planning', 'phases'), { recursive: true }); |
| 29 | + return dir; |
| 30 | +} |
| 31 | + |
| 32 | +describe('F8 — roadmap analyze parses the checklist phase format', () => { |
| 33 | + it('extracts phases from `- [x] Phase N: Title` lines', () => { |
| 34 | + const dir = tmpProject(); |
| 35 | + try { |
| 36 | + fs.writeFileSync(path.join(dir, '.planning', 'ROADMAP.md'), |
| 37 | + '# Roadmap\n\n<details>\n<summary>Done</summary>\n\n' + |
| 38 | + '- [x] Phase 54: XML Context Packer (2/2 plans)\n' + |
| 39 | + '- [x] Phase 55: Hotspot Detection (2/2 plans)\n' + |
| 40 | + '- [ ] Phase 56: Co-Change (0/1 plan)\n</details>\n'); |
| 41 | + const d = runTool(['roadmap', 'analyze'], dir); |
| 42 | + assert.equal(d.phase_count, 3, 'all three checklist phases must be parsed'); |
| 43 | + const nums = d.phases.map(p => String(p.number)); |
| 44 | + assert.deepEqual(nums.sort(), ['54', '55', '56']); |
| 45 | + } finally { |
| 46 | + fs.rmSync(dir, { recursive: true, force: true }); |
| 47 | + } |
| 48 | + }); |
| 49 | + |
| 50 | + it('still parses heading and bold-checklist forms (no regression)', () => { |
| 51 | + const dir = tmpProject(); |
| 52 | + try { |
| 53 | + fs.writeFileSync(path.join(dir, '.planning', 'ROADMAP.md'), |
| 54 | + '# Roadmap\n\n## Phase 1: Heading Form\n\n- [ ] **Phase 2: Bold Form**\n'); |
| 55 | + const d = runTool(['roadmap', 'analyze'], dir); |
| 56 | + assert.equal(d.phase_count, 2, 'heading + bold forms must still match'); |
| 57 | + } finally { |
| 58 | + fs.rmSync(dir, { recursive: true, force: true }); |
| 59 | + } |
| 60 | + }); |
| 61 | +}); |
| 62 | + |
| 63 | +describe('F3 — health collapses orphan-phase W007s into one', () => { |
| 64 | + it('emits a single W007 listing every orphan, not one per phase', () => { |
| 65 | + const dir = tmpProject(); |
| 66 | + try { |
| 67 | + // ROADMAP knows only Phase 1; phases 50/51/52 exist on disk → 3 orphans. |
| 68 | + fs.writeFileSync(path.join(dir, '.planning', 'ROADMAP.md'), |
| 69 | + '# Roadmap\n\n- [ ] Phase 1: Known\n'); |
| 70 | + for (const n of ['50-old', '51-old', '52-old']) { |
| 71 | + fs.mkdirSync(path.join(dir, '.planning', 'phases', n), { recursive: true }); |
| 72 | + } |
| 73 | + const d = runTool(['validate', 'health', '--json'], dir); |
| 74 | + const all = d.issues || d.warnings || d.checks || []; |
| 75 | + const w007 = all.filter(i => i && i.code === 'W007'); |
| 76 | + assert.equal(w007.length, 1, 'orphans must collapse into exactly one W007'); |
| 77 | + const msg = w007[0].message || w007[0].msg || ''; |
| 78 | + assert.ok(/50/.test(msg) && /51/.test(msg) && /52/.test(msg), |
| 79 | + 'the single W007 must enumerate all orphan phases'); |
| 80 | + } finally { |
| 81 | + fs.rmSync(dir, { recursive: true, force: true }); |
| 82 | + } |
| 83 | + }); |
| 84 | +}); |
0 commit comments