fix(#3982): strip closed-milestone details from the current window (#4177)

* test(#3982): archived details must not leak into the current-milestone window

Parser-level regression (newest-first layout, two archived details
blocks) plus the issue's end-to-end phase.complete fixture: completing
phase 20 must advance to 21, never backwards into the archived range.

* fix(#3982): strip closed-milestone details from the current window

The heading-located window stripped <details> archives from the
preamble but not from currentSection; on newest-first roadmaps the
archived titles sit in summary tags rather than headings, so the
section walk reached end-of-document and the window swallowed every
collapsed archive below the active milestone. The strip is gated on
isClosedMilestoneHeading over each block's summary — the issue's
prescribed narrow form — so the active milestone's own collapsed
blocks (#1341) survive instead of trading this bug for the
phase_count: 0 class (#557/#2947).

* chore(#3982): backfill changeset pr number

---------

Co-authored-by: sim <sim@local>
This commit is contained in:
Tom Boucher
2026-09-02 02:33:54 -04:00
committed by GitHub
parent 9eef1b791b
commit 383c2f6b34
4 changed files with 149 additions and 1 deletions

View File

@@ -111,6 +111,72 @@ describe('roadmap-parser: extractCurrentMilestone', () => {
assert.ok(result.includes('v2.0'), 'version heading preserved');
});
test('newest-first layout: archived details below the active milestone do not leak into the window (#3982)', () => {
// The archived milestone's title lives in the <summary> TAG, not a
// heading, so no milestone-shaped heading bounds the section walk and the
// raw currentSection used to swallow the whole <details> block — feeding
// archived phases to phase.complete's lowest-outstanding scan.
writeState(tmpDir, { milestone: 'v0.3' });
const content = [
'# Roadmap',
'',
'### 🚧 v0.3 — Third Milestone (Phases 20-22) — ACTIVE',
'',
'- [x] **Phase 20: First Thing** - does the first thing.',
'- [ ] **Phase 21: Second Thing** - does the second thing.',
'- [ ] **Phase 22: Third Thing** - does the third thing.',
'',
'<details>',
'<summary>✅ v0.2 Second Milestone (Phases 10-12) — ARCHIVED</summary>',
'',
'- [ ] **Phase 10: Never Finished** - was left unchecked when v0.2 closed.',
'- [x] **Phase 11: Done Thing** - completed.',
'',
'</details>',
'',
'<details>',
'<summary>✅ v0.1 First Milestone (Phases 1-2) — ARCHIVED</summary>',
'',
'- [x] **Phase 1: Done** - completed.',
'',
'</details>',
].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('Phase 21'), 'the real next phase stays in the window');
assert.ok(result.includes('Phase 22'), 'the last current phase stays in the window');
assert.ok(!result.includes('Phase 10'), 'an archived milestone\'s unchecked phase must not leak into the current window (#3982)');
assert.ok(!result.includes('Never Finished'), 'archived milestone content must not leak (#3982)');
assert.ok(!result.includes('Phase 1: Done'), 'a second archived details block must not leak either');
});
test('active milestone own collapsed details are preserved by the closed-only strip (#3982)', () => {
// The issue's adversarial fixture: the ACTIVE milestone holds its own
// collapsed <details> (deferred scope). A blanket strip would delete
// phases 21/22 and reproduce the phase_count: 0 class (#557/#2947).
writeState(tmpDir, { milestone: 'v0.3' });
const content = [
'# Roadmap', '',
'### 🚧 v0.3 — Third Milestone (Phases 20-22) — ACTIVE', '',
'- [x] **Phase 20: First Thing** - done.',
'',
'<details>',
'<summary>Deferred scope for v0.3</summary>', '',
'- [ ] **Phase 21: Second Thing** - deferred.',
'- [ ] **Phase 22: Third Thing** - deferred.',
'',
'</details>', '',
].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('Phase 21'), 'the active milestone\'s own collapsed phases must survive (#3982)');
assert.ok(result.includes('Phase 22'), 'the active milestone\'s own collapsed phases must survive (#3982)');
});
test('reads milestone from STATE.md and extracts that section', () => {
writeState(tmpDir, { milestone: 'v2.0' });
const content = [