test(#2641): lockdown tests from self-adversarial pass
Self-run adversarial pass on PR #3046 before next reviewer round-trip. Three lockdown tests added — none uncovered new bugs, all lock current behavior so a future change doesn't silently flip a convention. 1. Single-quote YAML version (`milestone: 'v0.9'`) Parity with the existing double-quote test. The strip pattern `/^["']|["']$/g` handles both — locked here so a future change to either character class doesn't silently regress one form. 2. Heading-anchor wins over <details> fallback (precedence lock) When a ROADMAP has BOTH `### v0.9` heading AND `<details><summary>v0.9</summary>` block, the heading-level lookup matches first and the fallback never fires. Test asserts the heading slice is returned starting at offset 0 AND the synthesized `## v0.9 ... details-anchored` heading is NOT prepended (proves fallback didn't run). Also documented in-test that the heading-anchor slice naturally includes downstream <details> blocks verbatim — a property of the heading path, not of this PR's fallback. 3. Multiple <details> blocks for same version → first match wins `content.match(detailsPattern)` (non-`g`) returns first match in document order. Locked so a future change to the matcher (e.g. switching to `matchAll` and picking last) doesn't silently change which block is treated as active. Adversarial-checklist coverage on commit 781cc6f8: - Boundary cases: empty / whitespace / single-char / single-quote / double-quote / digit-suffix (`v0.91`) / dot-suffix (`v0.9.0`) / hyphen-suffix (`v0.9-rc.1`, intentional same-milestone match per existing currentVersionStr convention) — all covered. - Sibling consistency: parseMilestoneFromState, getMilestoneInfo, extractCurrentMilestone all strip quotes identically. - Comment-vs-behavior: walked nested-guard, empty-guard, lookahead, tag-strip by hand against the regex; all comments accurate. - Downstream consumers: roadmapAnalyze + roadmapGetPhase both verified end-to-end via tests + FAMP smoke. - Failure-mode locality: all fall-through paths produce loud failures (empty arrays, `{found:false}`); no silent confident-wrong outputs. 48/48 roadmap.test.ts tests pass.
This commit is contained in:
@@ -622,6 +622,107 @@ Detail
|
||||
expect(result).not.toMatch(/^##.*<em>/m);
|
||||
});
|
||||
|
||||
// ─── Bug #2641 (lockdown): single-quote YAML version ───
|
||||
it('bug-2641: handles single-quote YAML version (milestone: \'v0.9\') in STATE.md', async () => {
|
||||
// Parity coverage with the double-quote test. The strip pattern
|
||||
// `/^["']|["']$/g` handles both — locked here so a future change to
|
||||
// either character class doesn't silently regress one form.
|
||||
const roadmap = `# Roadmap
|
||||
|
||||
<details>
|
||||
<summary>v0.9 Local-First Bus — Phase Details</summary>
|
||||
|
||||
### Phase 3: Polish
|
||||
**Goal:** Add polish.
|
||||
</details>
|
||||
`;
|
||||
const stateSingle = `---\nmilestone: 'v0.9'\n---\n# State\n`;
|
||||
await writeFile(join(tmpDir, '.planning', 'STATE.md'), stateSingle);
|
||||
await writeFile(join(tmpDir, '.planning', 'ROADMAP.md'), roadmap);
|
||||
|
||||
const result = await extractCurrentMilestone(roadmap, tmpDir);
|
||||
|
||||
expect(result).toContain('### Phase 3: Polish');
|
||||
expect(result).toMatch(/^##\s+v0\.9 Local-First Bus/m);
|
||||
});
|
||||
|
||||
// ─── Bug #2641 (lockdown): heading wins when BOTH heading and <details> match ───
|
||||
it('bug-2641: markdown heading anchor wins over <details><summary> fallback', async () => {
|
||||
// The <details> fallback only fires when the heading-level lookup MISSES.
|
||||
// If a ROADMAP has both `### v0.9 …` heading AND `<details><summary>v0.9 …</summary>`
|
||||
// for the same version, the heading anchor must win. Locks precedence so a
|
||||
// future refactor doesn't accidentally flip the order and silently change
|
||||
// which slice gets returned.
|
||||
const roadmap = `# Roadmap
|
||||
|
||||
### v0.9 Local-First Bus (heading-anchored)
|
||||
|
||||
### Phase 1: Heading-anchored Phase
|
||||
**Goal:** From the heading slice.
|
||||
|
||||
<details>
|
||||
<summary>v0.9 Local-First Bus — Phase Details (details-anchored)</summary>
|
||||
|
||||
### Phase 99: Details-anchored Phase
|
||||
**Goal:** From the details slice.
|
||||
</details>
|
||||
`;
|
||||
const state = `---\nmilestone: v0.9\n---\n# State\n`;
|
||||
await writeFile(join(tmpDir, '.planning', 'STATE.md'), state);
|
||||
await writeFile(join(tmpDir, '.planning', 'ROADMAP.md'), roadmap);
|
||||
|
||||
const result = await extractCurrentMilestone(roadmap, tmpDir);
|
||||
|
||||
// Heading slice is what got returned — original `### v0.9` heading
|
||||
// present, Phase 1 from the heading slice present.
|
||||
expect(result).toContain('### v0.9 Local-First Bus (heading-anchored)');
|
||||
expect(result).toContain('### Phase 1: Heading-anchored Phase');
|
||||
// Critical: the <details> fallback did NOT fire, so no synthesized
|
||||
// `## ` heading is prepended. (The heading-anchor slice extends to the
|
||||
// next milestone boundary and includes the downstream <details> block
|
||||
// verbatim — that's a property of the heading-anchor path, not the
|
||||
// fallback. We're locking which CODE PATH ran, not how its output looks.)
|
||||
expect(result).not.toMatch(/^##\s+v0\.9 Local-First Bus.*details-anchored/im);
|
||||
// The original heading must appear at the START of the slice (the
|
||||
// heading-anchor path returns content starting at the matched heading).
|
||||
expect(result.indexOf('### v0.9 Local-First Bus (heading-anchored)')).toBe(0);
|
||||
});
|
||||
|
||||
// ─── Bug #2641 (lockdown): multiple <details> blocks for same version ───
|
||||
it('bug-2641: when multiple <details> match the version, the FIRST is returned', async () => {
|
||||
// `content.match(detailsPattern)` (non-`g`) returns the first match in
|
||||
// document order. Lock this so a future change to the matcher (e.g.
|
||||
// switching to `matchAll` and picking the last) doesn't silently change
|
||||
// which block is treated as the active milestone. Document-order-first is
|
||||
// intentional: in real ROADMAPs, the active milestone is conventionally
|
||||
// listed before any duplicates (e.g. retro-active or branch-merge artefacts).
|
||||
const roadmap = `# Roadmap
|
||||
|
||||
<details>
|
||||
<summary>v0.9 Local-First Bus — Phase Details (FIRST)</summary>
|
||||
|
||||
### Phase 1: First-block Phase
|
||||
**Goal:** Should be returned.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>v0.9 Local-First Bus — Phase Details (SECOND)</summary>
|
||||
|
||||
### Phase 99: Second-block Phase
|
||||
**Goal:** Should NOT be returned.
|
||||
</details>
|
||||
`;
|
||||
const state = `---\nmilestone: v0.9\n---\n# State\n`;
|
||||
await writeFile(join(tmpDir, '.planning', 'STATE.md'), state);
|
||||
await writeFile(join(tmpDir, '.planning', 'ROADMAP.md'), roadmap);
|
||||
|
||||
const result = await extractCurrentMilestone(roadmap, tmpDir);
|
||||
|
||||
expect(result).toContain('### Phase 1: First-block Phase');
|
||||
expect(result).not.toContain('### Phase 99: Second-block Phase');
|
||||
expect(result).toMatch(/^##\s+v0\.9 Local-First Bus.*FIRST/m);
|
||||
});
|
||||
|
||||
// ─── Bug #2422: same-version sub-heading truncation ───────────────────
|
||||
it('bug-2422: does not truncate at same-version sub-heading (## v2.0 Phase Details)', async () => {
|
||||
const roadmapWithDetails = `# ROADMAP
|
||||
|
||||
Reference in New Issue
Block a user