/** * GSD Tools Tests - core.cjs * * Tests for the foundational module's exports including regressions * for known bugs (REG-01: loadConfig model_overrides, REG-02: getRoadmapPhaseInternal export). */ const { test, describe, beforeEach, afterEach } = require('node:test'); const assert = require('node:assert'); const fs = require('fs'); const path = require('path'); const os = require('os'); const { loadConfig, resolveModelInternal, MODEL_PROFILES, escapeRegex, generateSlugInternal, normalizePhaseName, comparePhaseNum, safeReadFile, pathExistsInternal, getMilestoneInfo, getMilestonePhaseFilter, getRoadmapPhaseInternal, searchPhaseInDir, findPhaseInternal, } = require('../get-shit-done/bin/lib/core.cjs'); // ─── loadConfig ──────────────────────────────────────────────────────────────── describe('loadConfig', () => { let tmpDir; let originalCwd; beforeEach(() => { tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-core-test-')); fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true }); originalCwd = process.cwd(); }); afterEach(() => { process.chdir(originalCwd); fs.rmSync(tmpDir, { recursive: true, force: true }); }); function writeConfig(obj) { fs.writeFileSync( path.join(tmpDir, '.planning', 'config.json'), JSON.stringify(obj, null, 2) ); } test('returns defaults when config.json is missing', () => { const config = loadConfig(tmpDir); assert.strictEqual(config.model_profile, 'balanced'); assert.strictEqual(config.commit_docs, true); assert.strictEqual(config.research, true); assert.strictEqual(config.plan_checker, true); assert.strictEqual(config.brave_search, false); assert.strictEqual(config.parallelization, true); }); test('reads model_profile from config.json', () => { writeConfig({ model_profile: 'quality' }); const config = loadConfig(tmpDir); assert.strictEqual(config.model_profile, 'quality'); }); test('reads nested config keys', () => { writeConfig({ planning: { commit_docs: false } }); const config = loadConfig(tmpDir); assert.strictEqual(config.commit_docs, false); }); test('reads branching_strategy from git section', () => { writeConfig({ git: { branching_strategy: 'per-phase' } }); const config = loadConfig(tmpDir); assert.strictEqual(config.branching_strategy, 'per-phase'); }); // Bug: loadConfig previously omitted model_overrides from return value test('returns model_overrides when present (REG-01)', () => { writeConfig({ model_overrides: { 'gsd-executor': 'opus' } }); const config = loadConfig(tmpDir); assert.deepStrictEqual(config.model_overrides, { 'gsd-executor': 'opus' }); }); test('returns model_overrides as null when not in config', () => { writeConfig({ model_profile: 'balanced' }); const config = loadConfig(tmpDir); assert.strictEqual(config.model_overrides, null); }); test('returns defaults when config.json contains invalid JSON', () => { fs.writeFileSync( path.join(tmpDir, '.planning', 'config.json'), 'not valid json {{{{' ); const config = loadConfig(tmpDir); assert.strictEqual(config.model_profile, 'balanced'); assert.strictEqual(config.commit_docs, true); }); test('handles parallelization as boolean', () => { writeConfig({ parallelization: false }); const config = loadConfig(tmpDir); assert.strictEqual(config.parallelization, false); }); test('handles parallelization as object with enabled field', () => { writeConfig({ parallelization: { enabled: false } }); const config = loadConfig(tmpDir); assert.strictEqual(config.parallelization, false); }); test('prefers top-level keys over nested keys', () => { writeConfig({ commit_docs: false, planning: { commit_docs: true } }); const config = loadConfig(tmpDir); assert.strictEqual(config.commit_docs, false); }); }); // ─── resolveModelInternal ────────────────────────────────────────────────────── describe('resolveModelInternal', () => { let tmpDir; beforeEach(() => { tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-core-test-')); fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true }); }); afterEach(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); }); function writeConfig(obj) { fs.writeFileSync( path.join(tmpDir, '.planning', 'config.json'), JSON.stringify(obj, null, 2) ); } describe('model profile structural validation', () => { test('all known agents resolve to a valid string for each profile', () => { const knownAgents = ['gsd-planner', 'gsd-executor', 'gsd-phase-researcher', 'gsd-codebase-mapper']; const profiles = ['quality', 'balanced', 'budget']; const validValues = ['inherit', 'sonnet', 'haiku', 'opus']; for (const profile of profiles) { writeConfig({ model_profile: profile }); for (const agent of knownAgents) { const result = resolveModelInternal(tmpDir, agent); assert.ok( validValues.includes(result), `profile=${profile} agent=${agent} returned unexpected value: ${result}` ); } } }); }); describe('override precedence', () => { test('per-agent override takes precedence over profile', () => { writeConfig({ model_profile: 'balanced', model_overrides: { 'gsd-executor': 'haiku' }, }); assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-executor'), 'haiku'); }); test('opus override resolves to inherit', () => { writeConfig({ model_overrides: { 'gsd-executor': 'opus' }, }); assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-executor'), 'inherit'); }); test('agents not in override fall back to profile', () => { writeConfig({ model_profile: 'quality', model_overrides: { 'gsd-executor': 'haiku' }, }); // gsd-planner not overridden, should use quality profile -> opus -> inherit assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-planner'), 'inherit'); }); }); describe('edge cases', () => { test('returns sonnet for unknown agent type', () => { writeConfig({ model_profile: 'balanced' }); assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-nonexistent'), 'sonnet'); }); test('defaults to balanced profile when model_profile missing', () => { writeConfig({}); // balanced profile, gsd-planner -> opus -> inherit assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-planner'), 'inherit'); }); }); }); // ─── escapeRegex ─────────────────────────────────────────────────────────────── describe('escapeRegex', () => { test('escapes dots', () => { assert.strictEqual(escapeRegex('file.txt'), 'file\\.txt'); }); test('escapes all special regex characters', () => { const input = '1.0 (alpha) [test] {ok} $100 ^start end$ a+b a*b a?b pipe|or back\\slash'; const result = escapeRegex(input); // Verify each special char is escaped assert.ok(result.includes('\\.')); assert.ok(result.includes('\\(')); assert.ok(result.includes('\\)')); assert.ok(result.includes('\\[')); assert.ok(result.includes('\\]')); assert.ok(result.includes('\\{')); assert.ok(result.includes('\\}')); assert.ok(result.includes('\\$')); assert.ok(result.includes('\\^')); assert.ok(result.includes('\\+')); assert.ok(result.includes('\\*')); assert.ok(result.includes('\\?')); assert.ok(result.includes('\\|')); assert.ok(result.includes('\\\\')); }); test('handles empty string', () => { assert.strictEqual(escapeRegex(''), ''); }); test('returns plain string unchanged', () => { assert.strictEqual(escapeRegex('hello'), 'hello'); }); }); // ─── generateSlugInternal ────────────────────────────────────────────────────── describe('generateSlugInternal', () => { test('converts text to lowercase kebab-case', () => { assert.strictEqual(generateSlugInternal('Hello World'), 'hello-world'); }); test('removes special characters', () => { assert.strictEqual(generateSlugInternal('core.cjs Tests!'), 'core-cjs-tests'); }); test('trims leading and trailing hyphens', () => { assert.strictEqual(generateSlugInternal('---hello---'), 'hello'); }); test('returns null for null input', () => { assert.strictEqual(generateSlugInternal(null), null); }); test('returns null for empty string', () => { assert.strictEqual(generateSlugInternal(''), null); }); }); // ─── normalizePhaseName ──────────────────────────────────────────────────────── describe('normalizePhaseName', () => { test('pads single digit', () => { assert.strictEqual(normalizePhaseName('1'), '01'); }); test('preserves double digit', () => { assert.strictEqual(normalizePhaseName('12'), '12'); }); test('handles letter suffix', () => { assert.strictEqual(normalizePhaseName('1A'), '01A'); }); test('handles decimal phases', () => { assert.strictEqual(normalizePhaseName('2.1'), '02.1'); }); test('handles multi-level decimals', () => { assert.strictEqual(normalizePhaseName('1.2.3'), '01.2.3'); }); test('returns non-matching input unchanged', () => { assert.strictEqual(normalizePhaseName('abc'), 'abc'); }); }); // ─── comparePhaseNum ─────────────────────────────────────────────────────────── describe('comparePhaseNum', () => { test('sorts integer phases numerically', () => { assert.ok(comparePhaseNum('1', '2') < 0); assert.ok(comparePhaseNum('10', '2') > 0); }); test('sorts letter suffixes', () => { assert.ok(comparePhaseNum('12', '12A') < 0); assert.ok(comparePhaseNum('12A', '12B') < 0); }); test('sorts decimal phases', () => { assert.ok(comparePhaseNum('2', '2.1') < 0); assert.ok(comparePhaseNum('2.1', '2.2') < 0); }); test('handles multi-level decimals', () => { assert.ok(comparePhaseNum('1.1', '1.1.2') < 0); assert.ok(comparePhaseNum('1.1.2', '1.2') < 0); }); test('returns 0 for equal phases', () => { assert.strictEqual(comparePhaseNum('1', '1'), 0); assert.strictEqual(comparePhaseNum('2.1', '2.1'), 0); }); }); // ─── safeReadFile ────────────────────────────────────────────────────────────── describe('safeReadFile', () => { let tmpDir; beforeEach(() => { tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-core-test-')); }); afterEach(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); }); test('reads existing file', () => { const filePath = path.join(tmpDir, 'test.txt'); fs.writeFileSync(filePath, 'hello world'); assert.strictEqual(safeReadFile(filePath), 'hello world'); }); test('returns null for missing file', () => { assert.strictEqual(safeReadFile('/nonexistent/path/file.txt'), null); }); }); // ─── pathExistsInternal ──────────────────────────────────────────────────────── describe('pathExistsInternal', () => { let tmpDir; beforeEach(() => { tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-core-test-')); fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true }); }); afterEach(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); }); test('returns true for existing path', () => { assert.strictEqual(pathExistsInternal(tmpDir, '.planning'), true); }); test('returns false for non-existing path', () => { assert.strictEqual(pathExistsInternal(tmpDir, 'nonexistent'), false); }); test('handles absolute paths', () => { assert.strictEqual(pathExistsInternal(tmpDir, tmpDir), true); }); }); // ─── getMilestoneInfo ────────────────────────────────────────────────────────── describe('getMilestoneInfo', () => { let tmpDir; beforeEach(() => { tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-core-test-')); fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true }); }); afterEach(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); }); test('extracts version and name from roadmap', () => { fs.writeFileSync( path.join(tmpDir, '.planning', 'ROADMAP.md'), '# Roadmap\n\n## Roadmap v1.2: My Cool Project\n\nSome content' ); const info = getMilestoneInfo(tmpDir); assert.strictEqual(info.version, 'v1.2'); assert.strictEqual(info.name, 'My Cool Project'); }); test('returns defaults when roadmap missing', () => { const info = getMilestoneInfo(tmpDir); assert.strictEqual(info.version, 'v1.0'); assert.strictEqual(info.name, 'milestone'); }); test('returns active milestone when shipped milestone is collapsed in details block', () => { const roadmap = [ '# Milestones', '', '| Version | Status |', '|---------|--------|', '| v0.1 | Shipped |', '| v0.2 | Active |', '', '
', 'v0.1 — Legacy Feature Parity (Shipped)', '', '## Roadmap v0.1: Legacy Feature Parity', '', '### Phase 1: Core Setup', 'Some content about phase 1', '', '
', '', '## Roadmap v0.2: Dashboard Overhaul', '', '### Phase 8: New Dashboard Layout', 'Some content about phase 8', ].join('\n'); fs.writeFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), roadmap); const info = getMilestoneInfo(tmpDir); assert.strictEqual(info.version, 'v0.2'); assert.strictEqual(info.name, 'Dashboard Overhaul'); }); test('returns active milestone when multiple shipped milestones exist in details blocks', () => { const roadmap = [ '# Milestones', '', '| Version | Status |', '|---------|--------|', '| v0.1 | Shipped |', '| v0.2 | Shipped |', '| v0.3 | Active |', '', '
', 'v0.1 — Initial Release (Shipped)', '', '## Roadmap v0.1: Initial Release', '', '
', '', '
', 'v0.2 — Feature Expansion (Shipped)', '', '## Roadmap v0.2: Feature Expansion', '', '
', '', '## Roadmap v0.3: Performance Tuning', '', '### Phase 12: Optimize Queries', ].join('\n'); fs.writeFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), roadmap); const info = getMilestoneInfo(tmpDir); assert.strictEqual(info.version, 'v0.3'); assert.strictEqual(info.name, 'Performance Tuning'); }); test('returns defaults when roadmap has no heading matches', () => { fs.writeFileSync( path.join(tmpDir, '.planning', 'ROADMAP.md'), '# Roadmap\n\nSome content without version headings' ); const info = getMilestoneInfo(tmpDir); assert.strictEqual(info.version, 'v1.0'); assert.strictEqual(info.name, 'milestone'); }); }); // ─── searchPhaseInDir ────────────────────────────────────────────────────────── describe('searchPhaseInDir', () => { let tmpDir; let phasesDir; beforeEach(() => { tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-core-test-')); phasesDir = path.join(tmpDir, 'phases'); fs.mkdirSync(phasesDir, { recursive: true }); }); afterEach(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); }); test('finds phase directory by normalized prefix', () => { fs.mkdirSync(path.join(phasesDir, '01-foundation')); const result = searchPhaseInDir(phasesDir, '.planning/phases', '01'); assert.strictEqual(result.found, true); assert.strictEqual(result.phase_number, '01'); assert.strictEqual(result.phase_name, 'foundation'); }); test('returns plans and summaries', () => { const phaseDir = path.join(phasesDir, '01-foundation'); fs.mkdirSync(phaseDir); fs.writeFileSync(path.join(phaseDir, '01-01-PLAN.md'), '# Plan'); fs.writeFileSync(path.join(phaseDir, '01-01-SUMMARY.md'), '# Summary'); const result = searchPhaseInDir(phasesDir, '.planning/phases', '01'); assert.ok(result.plans.includes('01-01-PLAN.md')); assert.ok(result.summaries.includes('01-01-SUMMARY.md')); assert.strictEqual(result.incomplete_plans.length, 0); }); test('identifies incomplete plans', () => { const phaseDir = path.join(phasesDir, '01-foundation'); fs.mkdirSync(phaseDir); fs.writeFileSync(path.join(phaseDir, '01-01-PLAN.md'), '# Plan 1'); fs.writeFileSync(path.join(phaseDir, '01-02-PLAN.md'), '# Plan 2'); fs.writeFileSync(path.join(phaseDir, '01-01-SUMMARY.md'), '# Summary 1'); const result = searchPhaseInDir(phasesDir, '.planning/phases', '01'); assert.strictEqual(result.incomplete_plans.length, 1); assert.ok(result.incomplete_plans.includes('01-02-PLAN.md')); }); test('detects research and context files', () => { const phaseDir = path.join(phasesDir, '01-foundation'); fs.mkdirSync(phaseDir); fs.writeFileSync(path.join(phaseDir, '01-RESEARCH.md'), '# Research'); fs.writeFileSync(path.join(phaseDir, '01-CONTEXT.md'), '# Context'); const result = searchPhaseInDir(phasesDir, '.planning/phases', '01'); assert.strictEqual(result.has_research, true); assert.strictEqual(result.has_context, true); }); test('returns null when phase not found', () => { fs.mkdirSync(path.join(phasesDir, '01-foundation')); const result = searchPhaseInDir(phasesDir, '.planning/phases', '99'); assert.strictEqual(result, null); }); test('generates phase_slug from directory name', () => { fs.mkdirSync(path.join(phasesDir, '01-core-cjs-tests')); const result = searchPhaseInDir(phasesDir, '.planning/phases', '01'); assert.strictEqual(result.phase_slug, 'core-cjs-tests'); }); }); // ─── findPhaseInternal ───────────────────────────────────────────────────────── describe('findPhaseInternal', () => { let tmpDir; beforeEach(() => { tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-core-test-')); fs.mkdirSync(path.join(tmpDir, '.planning', 'phases'), { recursive: true }); }); afterEach(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); }); test('finds phase in current phases directory', () => { fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-foundation')); const result = findPhaseInternal(tmpDir, '1'); assert.strictEqual(result.found, true); assert.strictEqual(result.phase_number, '01'); }); test('returns null for non-existent phase', () => { const result = findPhaseInternal(tmpDir, '99'); assert.strictEqual(result, null); }); test('returns null for null phase', () => { const result = findPhaseInternal(tmpDir, null); assert.strictEqual(result, null); }); test('searches archived milestones when not in current', () => { // Create archived milestone structure (no current phase match) const archiveDir = path.join(tmpDir, '.planning', 'milestones', 'v1.0-phases', '01-foundation'); fs.mkdirSync(archiveDir, { recursive: true }); const result = findPhaseInternal(tmpDir, '1'); assert.strictEqual(result.found, true); assert.strictEqual(result.archived, 'v1.0'); }); }); // ─── getRoadmapPhaseInternal ─────────────────────────────────────────────────── describe('getRoadmapPhaseInternal', () => { let tmpDir; beforeEach(() => { tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-core-test-')); fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true }); }); afterEach(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); }); // Bug: getRoadmapPhaseInternal was missing from module.exports test('is exported from core.cjs (REG-02)', () => { assert.strictEqual(typeof getRoadmapPhaseInternal, 'function'); // Also verify it works with a real roadmap (note: goal regex expects **Goal:** with colon inside bold) fs.writeFileSync( path.join(tmpDir, '.planning', 'ROADMAP.md'), '### Phase 1: Foundation\n**Goal:** Build the base\n' ); const result = getRoadmapPhaseInternal(tmpDir, '1'); assert.strictEqual(result.found, true); assert.strictEqual(result.phase_name, 'Foundation'); assert.strictEqual(result.goal, 'Build the base'); }); test('extracts phase name and goal from roadmap', () => { fs.writeFileSync( path.join(tmpDir, '.planning', 'ROADMAP.md'), '### Phase 2: API Layer\n**Goal:** Create REST endpoints\n**Depends on**: Phase 1\n' ); const result = getRoadmapPhaseInternal(tmpDir, '2'); assert.strictEqual(result.phase_name, 'API Layer'); assert.strictEqual(result.goal, 'Create REST endpoints'); }); test('returns null goal when Goal uses colon-outside-bold format', () => { // Actual ROADMAP.md uses **Goal**: (colon outside bold) which the regex does not match fs.writeFileSync( path.join(tmpDir, '.planning', 'ROADMAP.md'), '### Phase 1: Foundation\n**Goal**: Build the base\n' ); const result = getRoadmapPhaseInternal(tmpDir, '1'); assert.strictEqual(result.found, true); assert.strictEqual(result.phase_name, 'Foundation'); assert.strictEqual(result.goal, null); }); test('returns null when roadmap missing', () => { const result = getRoadmapPhaseInternal(tmpDir, '1'); assert.strictEqual(result, null); }); test('returns null when phase not in roadmap', () => { fs.writeFileSync( path.join(tmpDir, '.planning', 'ROADMAP.md'), '### Phase 1: Foundation\n**Goal**: Build the base\n' ); const result = getRoadmapPhaseInternal(tmpDir, '99'); assert.strictEqual(result, null); }); test('returns null for null phase number', () => { const result = getRoadmapPhaseInternal(tmpDir, null); assert.strictEqual(result, null); }); test('extracts full section text', () => { fs.writeFileSync( path.join(tmpDir, '.planning', 'ROADMAP.md'), '### Phase 1: Foundation\n**Goal**: Build the base\n**Requirements**: TEST-01\nSome details here\n\n### Phase 2: API\n**Goal**: REST\n' ); const result = getRoadmapPhaseInternal(tmpDir, '1'); assert.ok(result.section.includes('Phase 1: Foundation')); assert.ok(result.section.includes('Some details here')); // Should not include Phase 2 content assert.ok(!result.section.includes('Phase 2: API')); }); }); // ─── getMilestonePhaseFilter ──────────────────────────────────────────────────── describe('getMilestonePhaseFilter', () => { let tmpDir; beforeEach(() => { tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-core-test-')); fs.mkdirSync(path.join(tmpDir, '.planning', 'phases'), { recursive: true }); }); afterEach(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); }); test('filters directories to only current milestone phases', () => { // ROADMAP lists only phases 5-7 fs.writeFileSync( path.join(tmpDir, '.planning', 'ROADMAP.md'), [ '## Roadmap v2.0: Next Release', '', '### Phase 5: Auth', '**Goal:** Add authentication', '', '### Phase 6: Dashboard', '**Goal:** Build dashboard', '', '### Phase 7: Polish', '**Goal:** Final polish', ].join('\n') ); // Create phase dirs 1-7 on disk (leftover from previous milestones) for (let i = 1; i <= 7; i++) { const padded = String(i).padStart(2, '0'); fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', `${padded}-phase-${i}`)); } const filter = getMilestonePhaseFilter(tmpDir); // Only phases 5, 6, 7 should match assert.strictEqual(filter('05-auth'), true); assert.strictEqual(filter('06-dashboard'), true); assert.strictEqual(filter('07-polish'), true); // Phases 1-4 should NOT match assert.strictEqual(filter('01-phase-1'), false); assert.strictEqual(filter('02-phase-2'), false); assert.strictEqual(filter('03-phase-3'), false); assert.strictEqual(filter('04-phase-4'), false); }); test('returns pass-all filter when ROADMAP.md is missing', () => { const filter = getMilestonePhaseFilter(tmpDir); assert.strictEqual(filter('01-foundation'), true); assert.strictEqual(filter('99-anything'), true); }); test('returns pass-all filter when ROADMAP has no phase headings', () => { fs.writeFileSync( path.join(tmpDir, '.planning', 'ROADMAP.md'), '# Roadmap\n\nSome content without phases.\n' ); const filter = getMilestonePhaseFilter(tmpDir); assert.strictEqual(filter('01-foundation'), true); assert.strictEqual(filter('05-api'), true); }); test('handles letter-suffix phases (e.g. 3A)', () => { fs.writeFileSync( path.join(tmpDir, '.planning', 'ROADMAP.md'), '### Phase 3A: Sub-feature\n**Goal:** Sub work\n' ); const filter = getMilestonePhaseFilter(tmpDir); assert.strictEqual(filter('03A-sub-feature'), true); assert.strictEqual(filter('03-main'), false); assert.strictEqual(filter('04-other'), false); }); test('handles decimal phases (e.g. 5.1)', () => { fs.writeFileSync( path.join(tmpDir, '.planning', 'ROADMAP.md'), '### Phase 5: Main\n**Goal:** Main work\n\n### Phase 5.1: Patch\n**Goal:** Patch work\n' ); const filter = getMilestonePhaseFilter(tmpDir); assert.strictEqual(filter('05-main'), true); assert.strictEqual(filter('05.1-patch'), true); assert.strictEqual(filter('04-other'), false); }); test('returns false for non-phase directory names', () => { fs.writeFileSync( path.join(tmpDir, '.planning', 'ROADMAP.md'), '### Phase 1: Init\n**Goal:** Start\n' ); const filter = getMilestonePhaseFilter(tmpDir); assert.strictEqual(filter('not-a-phase'), false); assert.strictEqual(filter('.gitkeep'), false); }); test('phaseCount reflects ROADMAP phase count', () => { fs.writeFileSync( path.join(tmpDir, '.planning', 'ROADMAP.md'), '### Phase 5: Auth\n### Phase 6: Dashboard\n### Phase 7: Polish\n' ); const filter = getMilestonePhaseFilter(tmpDir); assert.strictEqual(filter.phaseCount, 3); }); test('phaseCount is 0 when ROADMAP is missing', () => { const filter = getMilestonePhaseFilter(tmpDir); assert.strictEqual(filter.phaseCount, 0); }); test('phaseCount is 0 when ROADMAP has no phase headings', () => { fs.writeFileSync( path.join(tmpDir, '.planning', 'ROADMAP.md'), '# Roadmap\n\nSome content.\n' ); const filter = getMilestonePhaseFilter(tmpDir); assert.strictEqual(filter.phaseCount, 0); }); });