Files
get-shit-done/tests/roadmap-phase-fallback.test.cjs
Tom Boucher 74da61fb4a fix(#2619): prevent extractCurrentMilestone from truncating on phase-vX.Y headings (#2624)
* fix(#2619): prevent extractCurrentMilestone from truncating on phase-vX.Y headings

extractCurrentMilestone sliced ROADMAP.md to the current milestone by
looking for the next milestone heading with a greedy regex:

    ^#{1,N}\s+(?:.*v\d+\.\d+||📋|🚧)

Any heading that mentioned a version literal matched — including phase
headings like "### Phase 12: v1.0 Tech-Debt Closure". When the current
milestone was at the same heading level as the phases (### 🚧 v1.1 …),
the slice terminated at the first such phase, hiding every phase that
followed from phase.insert, validate.health W007, and other SDK commands.

Fix: add a `(?!Phase\s+\S)` negative lookahead so phase headings can
never be treated as milestone boundaries. Phase headings always start
with the literal `Phase `, so this is a clean exclusion.

Applied to:
- get-shit-done/bin/lib/core.cjs (extractCurrentMilestone)
- sdk/src/query/roadmap.ts (extractCurrentMilestone + extractNextMilestoneSection)

Regression tests:
- tests/roadmap-phase-fallback.test.cjs: extractCurrentMilestone does not
  truncate on phase heading containing vX.Y (#2619)
- sdk/src/query/roadmap.test.ts: extractCurrentMilestone bug-2619: does
  not truncate at a phase heading containing vX.Y

Closes #2619

* fix(#2619): make milestone-boundary Phase lookahead case-insensitive

CodeRabbit follow-up on #2619: the negative lookahead `(?!Phase\s+\S)`
in the SDK milestone-boundary regex was case-sensitive, so headings like
`### PHASE 12: v1.0 Tech-Debt` or `### phase 12: …` still truncated the
milestone slice. Add the `i` flag (now `gmi`).

The sibling CJS regex in get-shit-done/bin/lib/core.cjs already uses the
`mi` flag, so it is already case-insensitive; added a regression test to
lock that in.

- sdk/src/query/roadmap.ts: change flags from `gm` → `gmi`
- sdk/src/query/roadmap.test.ts: add PHASE/phase regression test
- tests/roadmap-phase-fallback.test.cjs: add PHASE/phase regression test

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 11:53:20 -04:00

313 lines
9.2 KiB
JavaScript

/**
* GSD Tools Tests - roadmap get-phase fallback to full ROADMAP.md
*
* Covers issue #1634: phases outside the current milestone slice should still
* resolve by falling back to the full ROADMAP.md content.
*/
const { test, describe, beforeEach, afterEach } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const path = require('path');
const { runGsdTools, createTempProject, cleanup } = require('./helpers.cjs');
/**
* Helper: write STATE.md with a milestone version so extractCurrentMilestone
* will slice the roadmap to only that milestone's section.
*/
function writeState(tmpDir, version) {
fs.writeFileSync(
path.join(tmpDir, '.planning', 'STATE.md'),
`---\nmilestone: ${version}\n---\n`
);
}
describe('roadmap get-phase fallback to full ROADMAP.md (#1634)', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
});
afterEach(() => {
cleanup(tmpDir);
});
test('active milestone phase still resolves correctly', () => {
writeState(tmpDir, 'v1.0');
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
`# Roadmap
## v1.0 Current Release
### Phase 1: Foundation
**Goal:** Set up project infrastructure
### Phase 2: API
**Goal:** Build REST API
## v2.0 Next Release
### Phase 3: Frontend
**Goal:** Build UI layer
`
);
const result = runGsdTools('roadmap get-phase 1', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.equal(output.found, true, 'active milestone phase should be found');
assert.equal(output.phase_number, '1');
assert.equal(output.phase_name, 'Foundation');
assert.equal(output.goal, 'Set up project infrastructure');
});
test('backlog phase outside current milestone resolves via fallback', () => {
writeState(tmpDir, 'v1.0');
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
`# Roadmap
## v1.0 Current Release
### Phase 1: Foundation
**Goal:** Set up project infrastructure
## v2.0 Future Release
### Phase 999.60: Backlog Cleanup
**Goal:** Clean up technical debt from backlog
`
);
const result = runGsdTools('roadmap get-phase 999.60', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.equal(output.found, true, 'backlog phase should be found via fallback');
assert.equal(output.phase_number, '999.60');
assert.equal(output.phase_name, 'Backlog Cleanup');
assert.equal(output.goal, 'Clean up technical debt from backlog');
});
test('future planned milestone phase resolves via fallback', () => {
writeState(tmpDir, 'v1.0');
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
`# Roadmap
## v1.0 Current Release
### Phase 1: Foundation
**Goal:** Set up project infrastructure
## v3.0 Planned Milestone
### Phase 1025: Advanced Analytics
**Goal:** Build analytics dashboard for enterprise customers
**Success Criteria** (what must be TRUE):
1. Dashboard renders in under 2s
2. Supports 10k concurrent users
`
);
const result = runGsdTools('roadmap get-phase 1025', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.equal(output.found, true, 'future milestone phase should be found via fallback');
assert.equal(output.phase_number, '1025');
assert.equal(output.phase_name, 'Advanced Analytics');
assert.equal(output.goal, 'Build analytics dashboard for enterprise customers');
assert.ok(Array.isArray(output.success_criteria), 'success_criteria should be extracted');
assert.equal(output.success_criteria.length, 2, 'should have 2 criteria');
});
test('truly missing phase still returns found: false', () => {
writeState(tmpDir, 'v1.0');
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
`# Roadmap
## v1.0 Current Release
### Phase 1: Foundation
**Goal:** Set up project infrastructure
## v2.0 Future Release
### Phase 5: Mobile
**Goal:** Build mobile app
`
);
const result = runGsdTools('roadmap get-phase 9999', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.equal(output.found, false, 'truly missing phase should return found: false');
assert.equal(output.phase_number, '9999');
});
test('backlog checklist-only phase triggers malformed_roadmap via fallback', () => {
writeState(tmpDir, 'v1.0');
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
`# Roadmap
## v1.0 Current Release
### Phase 1: Foundation
**Goal:** Set up project infrastructure
## v2.0 Backlog
- [ ] **Phase 50: Cleanup** - Remove old code
`
);
const result = runGsdTools('roadmap get-phase 50', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.equal(output.found, false, 'checklist-only phase should not be "found"');
assert.equal(output.error, 'malformed_roadmap', 'should identify malformed roadmap via fallback');
assert.ok(output.message.includes('missing'), 'should explain the issue');
});
test('checklist in milestone does not block full header match in wider roadmap', () => {
writeState(tmpDir, 'v1.0');
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
`# Roadmap
## v1.0 Current Release
### Phase 1: Foundation
**Goal:** Set up project infrastructure
- [ ] **Phase 50: Cleanup** - referenced in checklist
## v2.0 Future Release
### Phase 50: Cleanup
**Goal:** Remove deprecated modules
`
);
const result = runGsdTools('roadmap get-phase 50', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.equal(output.found, true, 'full header in v2.0 should win over checklist in v1.0');
assert.equal(output.phase_name, 'Cleanup');
assert.equal(output.goal, 'Remove deprecated modules');
});
test('extractCurrentMilestone does not truncate on phase heading containing vX.Y (#2619)', () => {
// Regression: phase heading like "### Phase 12: v1.0 Tech-Debt Closure"
// was incorrectly treated as a milestone boundary because the greedy
// `.*v\d+\.\d+` subpattern in nextMilestonePattern matched it.
const core = require('../get-shit-done/bin/lib/core.cjs');
writeState(tmpDir, 'v1.1');
const roadmap = `# Roadmap
## Phases
### 🚧 v1.1 Launch-Ready (In Progress)
### Phase 11: Structured Logging
**Goal:** Add structured logging
### Phase 12: v1.0 Tech-Debt Closure
**Goal:** Close out v1.0 debt
### Phase 19: Security Audit
**Goal:** Full security audit
`;
const slice = core.extractCurrentMilestone(roadmap, tmpDir);
assert.ok(
slice.includes('### Phase 12: v1.0 Tech-Debt Closure'),
'slice must include Phase 12 (it lives inside the active milestone)'
);
assert.ok(
slice.includes('### Phase 19: Security Audit'),
'slice must include Phase 19 (truncation at Phase 12 would hide it)'
);
});
test('extractCurrentMilestone handles PHASE/phase (case-insensitive) containing vX.Y (#2619 follow-up)', () => {
// CodeRabbit follow-up: the negative lookahead `(?!Phase\s+\S)` must be
// case-insensitive so PHASE/phase variants are also excluded.
const core = require('../get-shit-done/bin/lib/core.cjs');
writeState(tmpDir, 'v1.1');
const roadmap = `# Roadmap
## Phases
### 🚧 v1.1 Launch-Ready (In Progress)
### PHASE 11: Structured Logging
**Goal:** Add structured logging
### phase 12: v1.0 Tech-Debt Closure
**Goal:** Close out v1.0 debt
### Phase 19: Security Audit
**Goal:** Full security audit
`;
const slice = core.extractCurrentMilestone(roadmap, tmpDir);
assert.ok(
slice.includes('### PHASE 11: Structured Logging'),
'slice must include PHASE 11 (uppercase)'
);
assert.ok(
slice.includes('### phase 12: v1.0 Tech-Debt Closure'),
'slice must include phase 12 (lowercase with vX.Y)'
);
assert.ok(
slice.includes('### Phase 19: Security Audit'),
'slice must include Phase 19 (truncation at phase 12 would hide it)'
);
});
test('section extraction from fallback includes correct content boundaries', () => {
writeState(tmpDir, 'v1.0');
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
`# Roadmap
## v1.0 Current Release
### Phase 1: Foundation
**Goal:** Set up project infrastructure
## v2.0 Future Release
### Phase 10: Database
**Goal:** Schema design and migrations
This phase covers:
- Schema modeling
- Migration tooling
- Seed data
### Phase 11: Caching
**Goal:** Add Redis caching layer
`
);
const result = runGsdTools('roadmap get-phase 10', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.equal(output.found, true, 'phase 10 should be found via fallback');
assert.ok(output.section.includes('Schema modeling'), 'section includes description');
assert.ok(output.section.includes('Seed data'), 'section includes all bullets');
assert.ok(!output.section.includes('Phase 11'), 'section does not include next phase');
});
});