Files
msd-core/tests/roadmap-parser.test.cjs
Tom Boucher 2dedbdd11c fix(#1455): resolve project-code-prefixed roadmap headings
* fix: remove hardcoded phase project-code prefix cap

Centralize project-code prefix stripping/matching and replace fixed {1,6} caps so long codes (for example MANIFOLD-117) resolve across phase, roadmap parser, roadmap upgrade, and validate flows.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: address review feedback on prefix parsing

- allow project_code prefixes with digits and underscores while preserving milestone parsing
- use shared optional project-code prefix source in phase dir parsing
- extend regression coverage for APP1/APP_1 prefixed phases

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* chore: resolve review nit in phase-id test comment

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: resolve project-code-prefixed roadmap headings

Make getRoadmapPhaseInternal recover from drifted project-code-prefixed ROADMAP headings while preserving canonical bare-heading preference. Add init.phase-op and parser regressions for #1455 and guard gsd-roadmapper against emitting project_code in headings.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* chore: add changeset for project-code phase fix

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* chore: update roadmapper agent size baseline

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(#1455): tighten project-code prefix regex to [A-Z] start; add boundary test; document source order

Resolves blockers from review:
- Regex changed from [A-Z_][A-Z0-9_]* to [A-Z][A-Z0-9_]* so leading
  underscores (_FOO-7, _-7) are never misread as project-code prefixes;
  adds boundary test asserting both do NOT strip.
- Adds comment to roadmapPhaseLookupSources explaining why 3 sources are
  needed (order-dependent canonical-heading preference).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Solvely-Colin <211764741+Solvely-Colin@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 18:45:41 -04:00

611 lines
24 KiB
JavaScript

/**
* roadmap-parser.cjs — unit tests
*
* Covers the 6 functions extracted from core.cjs per ADR-857 rollout
* phase 2b (#870): stripShippedMilestones, extractCurrentMilestone,
* replaceInCurrentMilestone, getRoadmapPhaseInternal, getMilestoneInfo,
* getMilestonePhaseFilter.
*
* Includes:
* - Behavioral tests against realistic ROADMAP.md content
* - Adversarial fixtures (malformed frontmatter, unclosed fences,
* headings inside fences, unicode headings, repeated/decimal phase
* IDs, mixed CRLF/LF)
* - Shim-identity assertions verifying core.cjs re-exports are the
* same function objects as roadmap-parser.cjs exports
*/
'use strict';
const { describe, test, beforeEach, afterEach } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const roadmapParser = require('../gsd-core/bin/lib/roadmap-parser.cjs');
const { createTempProject, cleanup } = require('./helpers.cjs');
const {
stripShippedMilestones,
extractCurrentMilestone,
replaceInCurrentMilestone,
getRoadmapPhaseInternal,
getMilestoneInfo,
getMilestonePhaseFilter,
} = roadmapParser;
// ─── helpers ─────────────────────────────────────────────────────────────────
function writeRoadmap(tmpDir, content) {
fs.writeFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), content);
}
function writeState(tmpDir, fields) {
const lines = Object.entries(fields).map(([k, v]) => `${k}: ${v}`);
fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), lines.join('\n') + '\n');
}
// ─── stripShippedMilestones ───────────────────────────────────────────────────
describe('roadmap-parser: stripShippedMilestones', () => {
test('strips a single <details> block', () => {
const input = 'before\n<details>\nsome shipped content\n</details>\nafter';
const result = stripShippedMilestones(input);
assert.ok(!result.includes('<details>'), 'details tag should be removed');
assert.ok(!result.includes('shipped content'), 'shipped content should be removed');
assert.ok(result.includes('before'), 'before content preserved');
assert.ok(result.includes('after'), 'after content preserved');
});
test('strips multiple <details> blocks', () => {
const input = '<details>\nA\n</details>\nmiddle\n<details>\nB\n</details>\nend';
const result = stripShippedMilestones(input);
assert.ok(result.includes('middle'), 'middle content preserved');
assert.ok(result.includes('end'), 'end content preserved');
assert.ok(!result.includes('<details>'), 'all details tags removed');
});
test('returns unchanged string when no <details> blocks', () => {
const input = '## v1.0: Launch\n### Phase 1: Setup\n**Goal:** init\n';
assert.strictEqual(stripShippedMilestones(input), input);
});
test('handles case-insensitive <DETAILS> tags', () => {
const input = '<DETAILS>\nclosed content\n</DETAILS>\nafter';
const result = stripShippedMilestones(input);
assert.ok(!result.includes('closed content'), 'content removed');
assert.ok(result.includes('after'), 'after content preserved');
});
});
// ─── extractCurrentMilestone ──────────────────────────────────────────────────
describe('roadmap-parser: extractCurrentMilestone', () => {
let tmpDir;
beforeEach(() => { tmpDir = createTempProject(); });
afterEach(() => { cleanup(tmpDir); });
test('no cwd — strips <details> only', () => {
const input = '<details>\nshipped\n</details>\n## v2.0: Next\n### Phase 1: Setup\n';
const result = extractCurrentMilestone(input);
assert.ok(!result.includes('<details>'), 'details stripped');
assert.ok(result.includes('v2.0'), 'version heading preserved');
});
test('reads milestone from STATE.md and extracts that section', () => {
writeState(tmpDir, { milestone: 'v2.0' });
const content = [
'<details>',
'<summary>v1.0</summary>',
'### Phase 1: Old',
'</details>',
'## v2.0: Current',
'### Phase 2-01: Setup',
'**Goal:** build',
].join('\n');
writeRoadmap(tmpDir, content);
const roadmap = fs.readFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), 'utf-8');
const result = extractCurrentMilestone(roadmap, tmpDir);
assert.ok(result.includes('v2.0'), 'current milestone section included');
assert.ok(!result.includes('Old'), 'shipped milestone section excluded');
});
test('falls back to 🚧 marker when STATE.md has no milestone field', () => {
writeState(tmpDir, { phase: 'some-phase' });
const content = [
'## 🚧 **v2.0 Work in Progress**',
'### Phase 1: Active',
'**Goal:** do work',
].join('\n');
writeRoadmap(tmpDir, content);
const roadmap = fs.readFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), 'utf-8');
const result = extractCurrentMilestone(roadmap, tmpDir);
assert.ok(result.includes('v2.0'), 'inferred v2.0 milestone section included');
});
test('strips shipped milestones when no STATE.md and no 🚧 marker', () => {
const content = [
'<details>',
'<summary>v1.0 done</summary>',
'### Phase 1: Done',
'</details>',
'## v2.0: Next (no WIP marker)',
'### Phase 2: Future',
].join('\n');
const result = extractCurrentMilestone(content);
assert.ok(!result.includes('<details>'), 'details stripped');
assert.ok(result.includes('v2.0'), 'remaining content preserved');
});
test('unicode heading — emoji-prefixed milestone', () => {
writeState(tmpDir, { milestone: 'v3.0' });
const content = [
'## ✅ v1.0: Shipped',
'## 🚧 v3.0: In Progress',
'### Phase 3-01: Unicode Héros',
'**Goal:** тест',
].join('\n');
writeRoadmap(tmpDir, content);
const roadmap = fs.readFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), 'utf-8');
const result = extractCurrentMilestone(roadmap, tmpDir);
assert.ok(result.includes('v3.0'), 'v3.0 heading included');
assert.ok(result.includes('Unicode'), 'unicode phase name included');
});
test('CRLF line endings are handled', () => {
writeState(tmpDir, { milestone: 'v1.0' });
const content = '## v1.0: CRLF\r\n### Phase 1: Setup\r\n**Goal:** crlf goal\r\n';
writeRoadmap(tmpDir, content);
const roadmap = fs.readFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), 'utf-8');
const result = extractCurrentMilestone(roadmap, tmpDir);
assert.ok(result.includes('v1.0'), 'section found despite CRLF');
});
test('heading inside fenced code block not confused for milestone boundary', () => {
writeState(tmpDir, { milestone: 'v1.0' });
const content = [
'## v1.0: Current Milestone',
'### Phase 1: Real Phase',
'**Goal:** real goal',
'```markdown',
'## v2.0: Fake Heading Inside Fence',
'```',
'### Phase 2: Also Real',
'**Goal:** also real',
].join('\n');
writeRoadmap(tmpDir, content);
const roadmap = fs.readFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), 'utf-8');
const result = extractCurrentMilestone(roadmap, tmpDir);
// The section should include Phase 1 content; the fenced heading should not terminate section early
assert.ok(result.includes('real goal'), 'phase 1 content included');
assert.ok(result.includes('Also Real'), 'phase 2 content also included');
});
});
// ─── replaceInCurrentMilestone ────────────────────────────────────────────────
describe('roadmap-parser: replaceInCurrentMilestone', () => {
test('replaces in content after last </details> when present', () => {
const content = '<details>\nold\n</details>\n**Plans:** 0/1 plans';
const result = replaceInCurrentMilestone(content, /0\/1 plans/, '1/1 plans complete');
assert.ok(result.includes('1/1 plans complete'), 'replacement applied after </details>');
assert.ok(result.includes('<details>'), 'details block untouched');
});
test('replaces anywhere when no </details> present', () => {
const content = '**Plans:** 0/1 plans';
const result = replaceInCurrentMilestone(content, /0\/1 plans/, '1/1 plans complete');
assert.strictEqual(result, '**Plans:** 1/1 plans complete');
});
test('does not replace in shipped sections', () => {
const content = '<details>\n**Plans:** 0/1 plans\n</details>\n## v2.0\n**Plans:** 0/1 plans';
const result = replaceInCurrentMilestone(content, /0\/1 plans/, '1/1 plans complete');
// Only the SECOND occurrence (after </details>) should be replaced
assert.ok(result.includes('<details>\n**Plans:** 0/1 plans\n</details>'), 'shipped section unchanged');
assert.ok(result.includes('## v2.0\n**Plans:** 1/1 plans complete'), 'current section updated');
});
});
// ─── getRoadmapPhaseInternal ──────────────────────────────────────────────────
describe('roadmap-parser: getRoadmapPhaseInternal', () => {
let tmpDir;
beforeEach(() => { tmpDir = createTempProject(); });
afterEach(() => { cleanup(tmpDir); });
test('returns null when ROADMAP.md missing', () => {
const result = getRoadmapPhaseInternal(tmpDir, '1');
assert.strictEqual(result, null);
});
test('returns null when phaseNum is falsy', () => {
writeRoadmap(tmpDir, '### Phase 1: Foo\n**Goal:** bar\n');
assert.strictEqual(getRoadmapPhaseInternal(tmpDir, null), null);
assert.strictEqual(getRoadmapPhaseInternal(tmpDir, ''), null);
assert.strictEqual(getRoadmapPhaseInternal(tmpDir, 0), null);
});
test('finds a phase by number', () => {
writeRoadmap(tmpDir, [
'## v1.0: Current',
'### Phase 1: Foundation',
'**Goal:** Set up infrastructure',
'',
'### Phase 2: API',
'**Goal:** Build the API',
].join('\n'));
const result = getRoadmapPhaseInternal(tmpDir, '1');
assert.ok(result !== null, 'result should not be null');
assert.strictEqual(result.found, true);
assert.strictEqual(result.phase_name, 'Foundation');
assert.strictEqual(result.goal, 'Set up infrastructure');
});
test('finds drifted project-code-prefixed headings by bare number (#1455)', () => {
writeRoadmap(tmpDir, [
'## v1.0: Current',
'### Phase MANIFOLD-117: Prefixed Heading',
'**Goal:** Recover from roadmapper heading drift',
].join('\n'));
const result = getRoadmapPhaseInternal(tmpDir, '117');
assert.ok(result !== null, 'bare number lookup should tolerate a prefixed heading');
assert.strictEqual(result.found, true);
assert.strictEqual(result.phase_number, '117');
assert.strictEqual(result.phase_name, 'Prefixed Heading');
assert.strictEqual(result.goal, 'Recover from roadmapper heading drift');
});
test('finds drifted project-code-prefixed headings by prefixed query (#1455)', () => {
writeRoadmap(tmpDir, [
'## v1.0: Current',
'### Phase MANIFOLD-117: Prefixed Heading',
'**Goal:** Exact prefixed lookup works on init resolver',
].join('\n'));
const result = getRoadmapPhaseInternal(tmpDir, 'MANIFOLD-117');
assert.ok(result !== null, 'prefixed lookup should resolve the matching prefixed heading');
assert.strictEqual(result.found, true);
assert.strictEqual(result.phase_number, 'MANIFOLD-117');
assert.strictEqual(result.phase_name, 'Prefixed Heading');
assert.strictEqual(result.goal, 'Exact prefixed lookup works on init resolver');
});
test('prefers canonical bare heading before prefixed drift fallback (#1455)', () => {
writeRoadmap(tmpDir, [
'## v1.0: Current',
'### Phase MANIFOLD-117: Prefixed Heading',
'**Goal:** Drift fallback',
'',
'### Phase 117: Bare Heading',
'**Goal:** Canonical bare',
].join('\n'));
const result = getRoadmapPhaseInternal(tmpDir, '117');
assert.ok(result !== null, 'bare lookup should resolve');
assert.strictEqual(result.phase_name, 'Bare Heading');
assert.strictEqual(result.goal, 'Canonical bare');
});
test('returns null for missing phase number', () => {
writeRoadmap(tmpDir, '### Phase 1: Foo\n**Goal:** bar\n');
const result = getRoadmapPhaseInternal(tmpDir, '99');
assert.strictEqual(result, null);
});
test('finds milestone-prefixed phase ID (e.g. 2-01)', () => {
writeState(tmpDir, { milestone: 'v2.0' });
writeRoadmap(tmpDir, [
'## v2.0: Current',
'### Phase 2-01: Alpha',
'**Goal:** first alpha phase',
'',
'### Phase 2-02: Beta',
'**Goal:** beta phase',
].join('\n'));
const result = getRoadmapPhaseInternal(tmpDir, '2-01');
assert.ok(result !== null);
assert.strictEqual(result.found, true);
assert.strictEqual(result.phase_name, 'Alpha');
assert.strictEqual(result.goal, 'first alpha phase');
});
test('decimal phase ID (e.g. 1.5)', () => {
writeRoadmap(tmpDir, [
'## v1.0: Current',
'### Phase 1.5: Intermediate',
'**Goal:** interstitial step',
].join('\n'));
const result = getRoadmapPhaseInternal(tmpDir, '1.5');
assert.ok(result !== null);
assert.strictEqual(result.phase_name, 'Intermediate');
});
});
// ─── getMilestoneInfo ─────────────────────────────────────────────────────────
describe('roadmap-parser: getMilestoneInfo', () => {
let tmpDir;
beforeEach(() => { tmpDir = createTempProject(); });
afterEach(() => { cleanup(tmpDir); });
test('returns default when ROADMAP.md missing', () => {
const info = getMilestoneInfo(tmpDir);
assert.strictEqual(info.version, 'v1.0');
assert.strictEqual(info.name, 'milestone');
});
test('reads version from STATE.md and heading name', () => {
writeState(tmpDir, { milestone: 'v2.0' });
writeRoadmap(tmpDir, '## v2.0: The Big Launch\n### Phase 1: Setup\n');
const info = getMilestoneInfo(tmpDir);
assert.strictEqual(info.version, 'v2.0');
assert.match(info.name, /Big Launch/);
});
test('falls back to 🚧 WIP marker when STATE.md has no milestone', () => {
writeRoadmap(tmpDir, '## 🚧 **v1.5 Work In Progress**\n### Phase 1: Do stuff\n');
const info = getMilestoneInfo(tmpDir);
assert.strictEqual(info.version, 'v1.5');
assert.match(info.name, /Work In Progress/i);
});
test('extracts from heading when no STATE.md and no WIP marker', () => {
writeRoadmap(tmpDir, [
'## v3.0: Future Milestone',
'### Phase 1: Not started',
].join('\n'));
const info = getMilestoneInfo(tmpDir);
assert.strictEqual(info.version, 'v3.0');
assert.match(info.name, /Future Milestone/);
});
test('skips completed ✅ milestones', () => {
writeRoadmap(tmpDir, [
'## ✅ v1.0: Shipped Already',
'## v2.0: Next Up',
].join('\n'));
const info = getMilestoneInfo(tmpDir);
// Should not use the ✅-prefixed version as the current milestone
assert.strictEqual(info.version, 'v2.0');
});
});
// ─── getMilestonePhaseFilter ──────────────────────────────────────────────────
describe('roadmap-parser: getMilestonePhaseFilter', () => {
let tmpDir;
beforeEach(() => { tmpDir = createTempProject(); });
afterEach(() => { cleanup(tmpDir); });
test('returns passAll (phaseCount=0) when ROADMAP.md missing', () => {
const filter = getMilestonePhaseFilter(tmpDir);
assert.strictEqual(filter.phaseCount, 0);
assert.strictEqual(filter('anything'), true);
});
test('basic milestone phase filter — matches dirs by phase number', () => {
writeRoadmap(tmpDir, [
'## v1.0: Launch',
'### Phase 1: Setup',
'**Goal:** setup',
'',
'### Phase 2: Build',
'**Goal:** build',
].join('\n'));
const filter = getMilestonePhaseFilter(tmpDir);
assert.strictEqual(filter.phaseCount, 2);
assert.strictEqual(filter('01-setup'), true, '01-setup matches Phase 1');
assert.strictEqual(filter('02-build'), true, '02-build matches Phase 2');
assert.strictEqual(filter('03-deploy'), false, '03-deploy not in milestone');
});
test('milestone-prefixed phase IDs (e.g. 2-01)', () => {
writeState(tmpDir, { milestone: 'v2.0' });
writeRoadmap(tmpDir, [
'## v2.0: Current',
'### Phase 2-01: Alpha',
'### Phase 2-02: Beta',
].join('\n'));
const filter = getMilestonePhaseFilter(tmpDir);
assert.strictEqual(filter('02-01-alpha'), true, '02-01 matches Phase 2-01');
assert.strictEqual(filter('02-02-beta'), true, '02-02 matches Phase 2-02');
assert.strictEqual(filter('02-03-other'), false, '02-03 not in milestone');
});
test('versionOverride uses specified version slice', () => {
writeRoadmap(tmpDir, [
'## v1.0: Old',
'### Phase 1: Old Phase',
'',
'## v2.0: Current',
'### Phase 2: New Phase',
].join('\n'));
const filter = getMilestonePhaseFilter(tmpDir, 'v2.0');
assert.strictEqual(filter('02-new-phase'), true, 'phase 2 in v2.0 slice');
assert.strictEqual(filter('01-old-phase'), false, 'phase 1 not in v2.0 slice');
});
test('missingExplicitVersion set when version not found in versioned roadmap', () => {
writeRoadmap(tmpDir, [
'## v1.0: Only Milestone',
'### Phase 1: Foo',
].join('\n'));
const filter = getMilestonePhaseFilter(tmpDir, 'v9.9');
assert.strictEqual(filter.missingExplicitVersion, true, 'missingExplicitVersion should be true');
assert.strictEqual(filter.phaseCount, 0);
});
test('zero-padded phase IDs match unpadded dirs and vice versa', () => {
writeRoadmap(tmpDir, [
'## v1.0: Padded Test',
'### Phase 01: Setup',
'### Phase 02: Build',
].join('\n'));
const filter = getMilestonePhaseFilter(tmpDir);
assert.strictEqual(filter('1-setup'), true, 'unpadded dir matches padded Phase 01');
assert.strictEqual(filter('02-build'), true, 'padded dir matches padded Phase 02');
});
test('decimal phase IDs in ROADMAP filter correctly', () => {
writeRoadmap(tmpDir, [
'## v1.0: Decimal Test',
'### Phase 1.5: Interstitial',
'### Phase 2: Normal',
].join('\n'));
const filter = getMilestonePhaseFilter(tmpDir);
assert.ok(filter.phaseCount >= 1, 'at least one phase found');
// Decimal phase IDs are non-numeric so filter should handle them
assert.strictEqual(filter('1.5-interstitial'), true, 'decimal phase dir matches');
});
test('repeated phase IDs — deduplication (no double count)', () => {
writeRoadmap(tmpDir, [
'## v1.0: Repeated',
'### Phase 1: First',
'### Phase 1: Duplicate heading',
].join('\n'));
const filter = getMilestonePhaseFilter(tmpDir);
// Phase 1 appears twice but should only count once
assert.strictEqual(filter.phaseCount, 1, 'deduplication: only 1 unique phase');
});
test('adversarial: phase heading inside backtick fence is excluded (fix #875)', () => {
writeRoadmap(tmpDir, [
'## v1.0: Real',
'```',
'### Phase 999: Fake Phase Inside Fence',
'```',
'### Phase 1: Real Phase',
'**Goal:** real',
].join('\n'));
const filter = getMilestonePhaseFilter(tmpDir);
// Phase headings inside fenced code blocks must NOT be counted as real phases.
// getMilestonePhaseFilter is fence-aware (fix #875).
assert.strictEqual(filter('01-real'), true, 'real phase matches');
assert.strictEqual(filter('999-fake'), false, 'fenced phase heading is correctly excluded');
});
test('adversarial: unclosed fence block — does not crash', () => {
writeRoadmap(tmpDir, [
'## v1.0: Unclosed',
'```',
'### Phase 1: Inside unclosed fence',
'**Goal:** unreachable',
// Intentionally no closing ``` — adversarial fixture
].join('\n'));
// Should not throw regardless of fence parsing behavior
let filter;
assert.doesNotThrow(() => {
filter = getMilestonePhaseFilter(tmpDir);
}, 'unclosed fence should not throw');
assert.ok(typeof filter === 'function', 'filter is a function');
});
test('adversarial: phase heading inside tilde fence is excluded (fix #875)', () => {
writeRoadmap(tmpDir, [
'## v1.0: Tilde',
'~~~',
'### Phase 999: Fake',
'~~~',
'### Phase 1: Real',
].join('\n'));
const filter = getMilestonePhaseFilter(tmpDir);
// Phase headings inside tilde-fenced code blocks must NOT be counted as real phases.
// getMilestonePhaseFilter is fence-aware (fix #875).
assert.strictEqual(filter('01-real'), true, 'real phase matches despite tilde fence');
assert.strictEqual(filter('999-fake'), false, 'tilde-fenced phase heading is correctly excluded');
});
test('adversarial: phase heading inside fence is excluded with CRLF endings (fix #875)', () => {
const crlf = '## v1.0: CRLF Fence\r\n```\r\n### Phase 999: Fake\r\n```\r\n### Phase 1: Real\r\n';
writeRoadmap(tmpDir, crlf);
const filter = getMilestonePhaseFilter(tmpDir);
assert.strictEqual(filter('01-real'), true, 'real phase matches in CRLF file');
assert.strictEqual(filter('999-fake'), false, 'fenced phase excluded in CRLF file');
});
test('adversarial: phase headings in back-to-back fences are excluded (fix #875)', () => {
writeRoadmap(tmpDir, [
'## v1.0: Adjacent',
'```',
'### Phase 998: Fake A',
'```',
'```',
'### Phase 999: Fake B',
'```',
'### Phase 1: Real',
].join('\n'));
const filter = getMilestonePhaseFilter(tmpDir);
assert.strictEqual(filter('01-real'), true, 'real phase matches');
assert.strictEqual(filter('998-fake'), false, 'first fenced phase excluded');
assert.strictEqual(filter('999-fake'), false, 'second fenced phase excluded');
});
test('adversarial: CRLF line endings in roadmap', () => {
const crlf = '## v1.0: CRLF\r\n### Phase 1: Setup\r\n### Phase 2: Build\r\n';
writeRoadmap(tmpDir, crlf);
let filter;
assert.doesNotThrow(() => { filter = getMilestonePhaseFilter(tmpDir); });
assert.ok(filter.phaseCount >= 1, 'phases found despite CRLF');
});
test('adversarial: mixed CRLF and LF in same file', () => {
const mixed = '## v1.0: Mixed\r\n### Phase 1: A\n### Phase 2: B\r\n### Phase 3: C\n';
writeRoadmap(tmpDir, mixed);
let filter;
assert.doesNotThrow(() => { filter = getMilestonePhaseFilter(tmpDir); });
assert.ok(filter.phaseCount >= 1, 'phases found in mixed CRLF/LF');
});
test('adversarial: unicode headings', () => {
writeState(tmpDir, { milestone: 'v1.0' });
writeRoadmap(tmpDir, [
'## v1.0: 日本語マイルストーン',
'### Phase 1: Héros Réalité',
'### Phase 2: Тест',
].join('\n'));
let filter;
assert.doesNotThrow(() => { filter = getMilestonePhaseFilter(tmpDir); });
assert.strictEqual(filter.phaseCount, 2, '2 unicode phases found');
assert.strictEqual(filter('01-setup'), true, 'phase 1 dir matches');
});
test('adversarial: bracket-prefixed phase heading ### [GSD] Phase 2-01:', () => {
writeState(tmpDir, { milestone: 'v2.0' });
writeRoadmap(tmpDir, [
'## v2.0: Bracket',
'### [GSD] Phase 2-01: Setup',
'### [GSD] Phase 2-02: Build',
].join('\n'));
const filter = getMilestonePhaseFilter(tmpDir);
assert.strictEqual(filter('02-01-setup'), true, 'bracket-prefixed phase 2-01 matched');
assert.strictEqual(filter('02-02-build'), true, 'bracket-prefixed phase 2-02 matched');
});
});