mirror of
https://github.com/glittercowboy/get-shit-done
synced 2026-04-25 17:25:23 +02:00
* 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>
313 lines
9.2 KiB
JavaScript
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');
|
|
});
|
|
});
|