fix: scope ROADMAP.md searches to current milestone (#1059)
Phase numbers reset per milestone (v1.0 Phase 1-26, v1.1 Phase 1-6, v1.2 Phase 1-6). Functions that searched ROADMAP.md for phase headings or checkboxes would match archived milestone entries inside <details> blocks before reaching the current milestone's entries. This caused: - roadmap_complete: true for phases that aren't complete (checkbox regex matched the archived [x] Phase N from a previous milestone) - phase-complete marking the wrong checkbox (archived instead of current) - phase-add computing wrong maxPhase from archived headings - progress not showing phases defined in ROADMAP but not yet on disk
This commit is contained in:
@@ -331,6 +331,34 @@ function getArchivedPhaseDirs(cwd) {
|
||||
return results;
|
||||
}
|
||||
|
||||
// ─── Roadmap milestone scoping ───────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Strip shipped milestone content wrapped in <details> blocks.
|
||||
* Used to isolate current milestone phases when searching ROADMAP.md
|
||||
* for phase headings or checkboxes — prevents matching archived milestone
|
||||
* phases that share the same numbers as current milestone phases.
|
||||
*/
|
||||
function stripShippedMilestones(content) {
|
||||
return content.replace(/<details>[\s\S]*?<\/details>/gi, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace a pattern only in the current milestone section of ROADMAP.md
|
||||
* (everything after the last </details> close tag). Used for write operations
|
||||
* that must not accidentally modify archived milestone checkboxes/tables.
|
||||
*/
|
||||
function replaceInCurrentMilestone(content, pattern, replacement) {
|
||||
const lastDetailsClose = content.lastIndexOf('</details>');
|
||||
if (lastDetailsClose === -1) {
|
||||
return content.replace(pattern, replacement);
|
||||
}
|
||||
const offset = lastDetailsClose + '</details>'.length;
|
||||
const before = content.slice(0, offset);
|
||||
const after = content.slice(offset);
|
||||
return before + after.replace(pattern, replacement);
|
||||
}
|
||||
|
||||
// ─── Roadmap & model utilities ────────────────────────────────────────────────
|
||||
|
||||
function getRoadmapPhaseInternal(cwd, phaseNum) {
|
||||
@@ -339,7 +367,7 @@ function getRoadmapPhaseInternal(cwd, phaseNum) {
|
||||
if (!fs.existsSync(roadmapPath)) return null;
|
||||
|
||||
try {
|
||||
const content = fs.readFileSync(roadmapPath, 'utf-8');
|
||||
const content = stripShippedMilestones(fs.readFileSync(roadmapPath, 'utf-8'));
|
||||
const escapedPhase = escapeRegex(phaseNum.toString());
|
||||
const phasePattern = new RegExp(`#{2,4}\\s*Phase\\s+${escapedPhase}:\\s*([^\\n]+)`, 'i');
|
||||
const headerMatch = content.match(phasePattern);
|
||||
@@ -352,7 +380,7 @@ function getRoadmapPhaseInternal(cwd, phaseNum) {
|
||||
const sectionEnd = nextHeaderMatch ? headerIndex + nextHeaderMatch.index : content.length;
|
||||
const section = content.slice(headerIndex, sectionEnd).trim();
|
||||
|
||||
const goalMatch = section.match(/\*\*Goal:\*\*\s*([^\n]+)/i);
|
||||
const goalMatch = section.match(/\*\*Goal(?:\*\*:|\*?\*?:\*\*)\s*([^\n]+)/i);
|
||||
const goal = goalMatch ? goalMatch[1].trim() : null;
|
||||
|
||||
return {
|
||||
@@ -416,7 +444,7 @@ function getMilestoneInfo(cwd) {
|
||||
}
|
||||
|
||||
// Second: heading-format roadmaps — strip shipped milestones in <details> blocks
|
||||
const cleaned = roadmap.replace(/<details>[\s\S]*?<\/details>/gi, '');
|
||||
const cleaned = stripShippedMilestones(roadmap);
|
||||
// Extract version and name from the same ## heading for consistency
|
||||
const headingMatch = cleaned.match(/## .*v(\d+\.\d+)[:\s]+([^\n(]+)/);
|
||||
if (headingMatch) {
|
||||
@@ -444,7 +472,7 @@ function getMilestoneInfo(cwd) {
|
||||
function getMilestonePhaseFilter(cwd) {
|
||||
const milestonePhaseNums = new Set();
|
||||
try {
|
||||
const roadmap = fs.readFileSync(path.join(cwd, '.planning', 'ROADMAP.md'), 'utf-8');
|
||||
const roadmap = stripShippedMilestones(fs.readFileSync(path.join(cwd, '.planning', 'ROADMAP.md'), 'utf-8'));
|
||||
const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:/gi;
|
||||
let m;
|
||||
while ((m = phasePattern.exec(roadmap)) !== null) {
|
||||
@@ -491,5 +519,7 @@ module.exports = {
|
||||
generateSlugInternal,
|
||||
getMilestoneInfo,
|
||||
getMilestonePhaseFilter,
|
||||
stripShippedMilestones,
|
||||
replaceInCurrentMilestone,
|
||||
toPosixPath,
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
const { loadConfig, resolveModelInternal, findPhaseInternal, getRoadmapPhaseInternal, pathExistsInternal, generateSlugInternal, getMilestoneInfo, normalizePhaseName, toPosixPath, output, error } = require('./core.cjs');
|
||||
const { loadConfig, resolveModelInternal, findPhaseInternal, getRoadmapPhaseInternal, pathExistsInternal, generateSlugInternal, getMilestoneInfo, getMilestonePhaseFilter, stripShippedMilestones, normalizePhaseName, toPosixPath, output, error } = require('./core.cjs');
|
||||
|
||||
function cmdInitExecutePhase(cwd, phase, raw) {
|
||||
if (!phase) {
|
||||
@@ -600,20 +600,46 @@ function cmdInitProgress(cwd, raw) {
|
||||
const config = loadConfig(cwd);
|
||||
const milestone = getMilestoneInfo(cwd);
|
||||
|
||||
// Analyze phases
|
||||
// Analyze phases — filter to current milestone and include ROADMAP-only phases
|
||||
const phasesDir = path.join(cwd, '.planning', 'phases');
|
||||
const phases = [];
|
||||
let currentPhase = null;
|
||||
let nextPhase = null;
|
||||
|
||||
// Build set of phases defined in ROADMAP for the current milestone
|
||||
const roadmapPhaseNums = new Set();
|
||||
const roadmapPhaseNames = new Map();
|
||||
try {
|
||||
const roadmapContent = stripShippedMilestones(
|
||||
fs.readFileSync(path.join(cwd, '.planning', 'ROADMAP.md'), 'utf-8')
|
||||
);
|
||||
const headingPattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:\s*([^\n]+)/gi;
|
||||
let hm;
|
||||
while ((hm = headingPattern.exec(roadmapContent)) !== null) {
|
||||
roadmapPhaseNums.add(hm[1]);
|
||||
roadmapPhaseNames.set(hm[1], hm[2].replace(/\(INSERTED\)/i, '').trim());
|
||||
}
|
||||
} catch {}
|
||||
|
||||
const isDirInMilestone = getMilestonePhaseFilter(cwd);
|
||||
const seenPhaseNums = new Set();
|
||||
|
||||
try {
|
||||
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
||||
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort();
|
||||
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name)
|
||||
.filter(isDirInMilestone)
|
||||
.sort((a, b) => {
|
||||
const pa = a.match(/^(\d+[A-Z]?(?:\.\d+)*)/i);
|
||||
const pb = b.match(/^(\d+[A-Z]?(?:\.\d+)*)/i);
|
||||
if (!pa || !pb) return a.localeCompare(b);
|
||||
return parseInt(pa[1], 10) - parseInt(pb[1], 10);
|
||||
});
|
||||
|
||||
for (const dir of dirs) {
|
||||
const match = dir.match(/^(\d+(?:\.\d+)*)-?(.*)/);
|
||||
const match = dir.match(/^(\d+[A-Z]?(?:\.\d+)*)-?(.*)/i);
|
||||
const phaseNumber = match ? match[1] : dir;
|
||||
const phaseName = match && match[2] ? match[2] : null;
|
||||
seenPhaseNums.add(phaseNumber.replace(/^0+/, '') || '0');
|
||||
|
||||
const phasePath = path.join(phasesDir, dir);
|
||||
const phaseFiles = fs.readdirSync(phasePath);
|
||||
@@ -648,6 +674,29 @@ function cmdInitProgress(cwd, raw) {
|
||||
}
|
||||
} catch {}
|
||||
|
||||
// Add phases defined in ROADMAP but not yet scaffolded to disk
|
||||
for (const [num, name] of roadmapPhaseNames) {
|
||||
const stripped = num.replace(/^0+/, '') || '0';
|
||||
if (!seenPhaseNums.has(stripped)) {
|
||||
const phaseInfo = {
|
||||
number: num,
|
||||
name: name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, ''),
|
||||
directory: null,
|
||||
status: 'not_started',
|
||||
plan_count: 0,
|
||||
summary_count: 0,
|
||||
has_research: false,
|
||||
};
|
||||
phases.push(phaseInfo);
|
||||
if (!nextPhase && !currentPhase) {
|
||||
nextPhase = phaseInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Re-sort phases by number after adding ROADMAP-only phases
|
||||
phases.sort((a, b) => parseInt(a.number, 10) - parseInt(b.number, 10));
|
||||
|
||||
// Check for paused work
|
||||
let pausedAt = null;
|
||||
try {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { escapeRegex, normalizePhaseName, comparePhaseNum, findPhaseInternal, getArchivedPhaseDirs, generateSlugInternal, getMilestonePhaseFilter, toPosixPath, output, error } = require('./core.cjs');
|
||||
const { escapeRegex, normalizePhaseName, comparePhaseNum, findPhaseInternal, getArchivedPhaseDirs, generateSlugInternal, getMilestonePhaseFilter, stripShippedMilestones, replaceInCurrentMilestone, toPosixPath, output, error } = require('./core.cjs');
|
||||
const { extractFrontmatter } = require('./frontmatter.cjs');
|
||||
const { writeStateMd } = require('./state.cjs');
|
||||
|
||||
@@ -318,10 +318,11 @@ function cmdPhaseAdd(cwd, description, raw) {
|
||||
error('ROADMAP.md not found');
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(roadmapPath, 'utf-8');
|
||||
const rawContent = fs.readFileSync(roadmapPath, 'utf-8');
|
||||
const content = stripShippedMilestones(rawContent);
|
||||
const slug = generateSlugInternal(description);
|
||||
|
||||
// Find highest integer phase number
|
||||
// Find highest integer phase number (in current milestone only)
|
||||
const phasePattern = /#{2,4}\s*Phase\s+(\d+)[A-Z]?(?:\.\d+)*:/gi;
|
||||
let maxPhase = 0;
|
||||
let m;
|
||||
@@ -344,11 +345,11 @@ function cmdPhaseAdd(cwd, description, raw) {
|
||||
|
||||
// Find insertion point: before last "---" or at end
|
||||
let updatedContent;
|
||||
const lastSeparator = content.lastIndexOf('\n---');
|
||||
const lastSeparator = rawContent.lastIndexOf('\n---');
|
||||
if (lastSeparator > 0) {
|
||||
updatedContent = content.slice(0, lastSeparator) + phaseEntry + content.slice(lastSeparator);
|
||||
updatedContent = rawContent.slice(0, lastSeparator) + phaseEntry + rawContent.slice(lastSeparator);
|
||||
} else {
|
||||
updatedContent = content + phaseEntry;
|
||||
updatedContent = rawContent + phaseEntry;
|
||||
}
|
||||
|
||||
fs.writeFileSync(roadmapPath, updatedContent, 'utf-8');
|
||||
@@ -374,7 +375,8 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
|
||||
error('ROADMAP.md not found');
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(roadmapPath, 'utf-8');
|
||||
const rawContent = fs.readFileSync(roadmapPath, 'utf-8');
|
||||
const content = stripShippedMilestones(rawContent);
|
||||
const slug = generateSlugInternal(description);
|
||||
|
||||
// Normalize input then strip leading zeros for flexible matching
|
||||
@@ -415,23 +417,23 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
|
||||
|
||||
// Insert after the target phase section
|
||||
const headerPattern = new RegExp(`(#{2,4}\\s*Phase\\s+0*${afterPhaseEscaped}:[^\\n]*\\n)`, 'i');
|
||||
const headerMatch = content.match(headerPattern);
|
||||
const headerMatch = rawContent.match(headerPattern);
|
||||
if (!headerMatch) {
|
||||
error(`Could not find Phase ${afterPhase} header`);
|
||||
}
|
||||
|
||||
const headerIdx = content.indexOf(headerMatch[0]);
|
||||
const afterHeader = content.slice(headerIdx + headerMatch[0].length);
|
||||
const headerIdx = rawContent.indexOf(headerMatch[0]);
|
||||
const afterHeader = rawContent.slice(headerIdx + headerMatch[0].length);
|
||||
const nextPhaseMatch = afterHeader.match(/\n#{2,4}\s+Phase\s+\d/i);
|
||||
|
||||
let insertIdx;
|
||||
if (nextPhaseMatch) {
|
||||
insertIdx = headerIdx + headerMatch[0].length + nextPhaseMatch.index;
|
||||
} else {
|
||||
insertIdx = content.length;
|
||||
insertIdx = rawContent.length;
|
||||
}
|
||||
|
||||
const updatedContent = content.slice(0, insertIdx) + phaseEntry + content.slice(insertIdx);
|
||||
const updatedContent = rawContent.slice(0, insertIdx) + phaseEntry + rawContent.slice(insertIdx);
|
||||
fs.writeFileSync(roadmapPath, updatedContent, 'utf-8');
|
||||
|
||||
const result = {
|
||||
@@ -728,7 +730,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
||||
`(-\\s*\\[)[ ](\\]\\s*.*Phase\\s+${escapeRegex(phaseNum)}[:\\s][^\\n]*)`,
|
||||
'i'
|
||||
);
|
||||
roadmapContent = roadmapContent.replace(checkboxPattern, `$1x$2 (completed ${today})`);
|
||||
roadmapContent = replaceInCurrentMilestone(roadmapContent, checkboxPattern, `$1x$2 (completed ${today})`);
|
||||
|
||||
// Progress table: update Status to Complete, add date
|
||||
const phaseEscaped = escapeRegex(phaseNum);
|
||||
@@ -736,8 +738,8 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
||||
`(\\|\\s*${phaseEscaped}\\.?\\s[^|]*\\|[^|]*\\|)\\s*[^|]*(\\|)\\s*[^|]*(\\|)`,
|
||||
'i'
|
||||
);
|
||||
roadmapContent = roadmapContent.replace(
|
||||
tablePattern,
|
||||
roadmapContent = replaceInCurrentMilestone(
|
||||
roadmapContent, tablePattern,
|
||||
`$1 Complete $2 ${today} $3`
|
||||
);
|
||||
|
||||
@@ -746,8 +748,8 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
||||
`(#{2,4}\\s*Phase\\s+${phaseEscaped}[\\s\\S]*?\\*\\*Plans:\\*\\*\\s*)[^\\n]+`,
|
||||
'i'
|
||||
);
|
||||
roadmapContent = roadmapContent.replace(
|
||||
planCountPattern,
|
||||
roadmapContent = replaceInCurrentMilestone(
|
||||
roadmapContent, planCountPattern,
|
||||
`$1${summaryCount}/${planCount} plans complete`
|
||||
);
|
||||
|
||||
@@ -758,7 +760,8 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
||||
if (fs.existsSync(reqPath)) {
|
||||
// Extract the current phase section from roadmap (scoped to avoid cross-phase matching)
|
||||
const phaseEsc = escapeRegex(phaseNum);
|
||||
const phaseSectionMatch = roadmapContent.match(
|
||||
const currentMilestoneRoadmap = stripShippedMilestones(roadmapContent);
|
||||
const phaseSectionMatch = currentMilestoneRoadmap.match(
|
||||
new RegExp(`(#{2,4}\\s*Phase\\s+${phaseEsc}[:\\s][\\s\\S]*?)(?=#{2,4}\\s*Phase\\s+|$)`, 'i')
|
||||
);
|
||||
|
||||
@@ -821,7 +824,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
||||
// for phases that are defined but not yet planned (no directory on disk)
|
||||
if (isLastPhase && fs.existsSync(roadmapPath)) {
|
||||
try {
|
||||
const roadmapForPhases = fs.readFileSync(roadmapPath, 'utf-8');
|
||||
const roadmapForPhases = stripShippedMilestones(fs.readFileSync(roadmapPath, 'utf-8'));
|
||||
const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:\s*([^\n]+)/gi;
|
||||
let pm;
|
||||
while ((pm = phasePattern.exec(roadmapForPhases)) !== null) {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { escapeRegex, normalizePhaseName, output, error, findPhaseInternal } = require('./core.cjs');
|
||||
const { escapeRegex, normalizePhaseName, output, error, findPhaseInternal, stripShippedMilestones, replaceInCurrentMilestone } = require('./core.cjs');
|
||||
|
||||
function cmdRoadmapGetPhase(cwd, phaseNum, raw) {
|
||||
const roadmapPath = path.join(cwd, '.planning', 'ROADMAP.md');
|
||||
@@ -15,7 +15,7 @@ function cmdRoadmapGetPhase(cwd, phaseNum, raw) {
|
||||
}
|
||||
|
||||
try {
|
||||
const content = fs.readFileSync(roadmapPath, 'utf-8');
|
||||
const content = stripShippedMilestones(fs.readFileSync(roadmapPath, 'utf-8'));
|
||||
|
||||
// Escape special regex chars in phase number, handle decimal
|
||||
const escapedPhase = escapeRegex(phaseNum);
|
||||
@@ -98,7 +98,8 @@ function cmdRoadmapAnalyze(cwd, raw) {
|
||||
return;
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(roadmapPath, 'utf-8');
|
||||
const rawContent = fs.readFileSync(roadmapPath, 'utf-8');
|
||||
const content = stripShippedMilestones(rawContent);
|
||||
const phasesDir = path.join(cwd, '.planning', 'phases');
|
||||
|
||||
// Extract all phase headings: ## Phase N: Name or ### Phase N: Name
|
||||
@@ -153,7 +154,7 @@ function cmdRoadmapAnalyze(cwd, raw) {
|
||||
} catch {}
|
||||
|
||||
// Check ROADMAP checkbox status
|
||||
const checkboxPattern = new RegExp(`-\\s*\\[(x| )\\]\\s*.*Phase\\s+${escapeRegex(phaseNum)}`, 'i');
|
||||
const checkboxPattern = new RegExp(`-\\s*\\[(x| )\\]\\s*.*Phase\\s+${escapeRegex(phaseNum)}[:\\s]`, 'i');
|
||||
const checkboxMatch = content.match(checkboxPattern);
|
||||
const roadmapComplete = checkboxMatch ? checkboxMatch[1] === 'x' : false;
|
||||
|
||||
@@ -262,8 +263,8 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
|
||||
'i'
|
||||
);
|
||||
const dateField = isComplete ? ` ${today} ` : ' ';
|
||||
roadmapContent = roadmapContent.replace(
|
||||
tablePattern,
|
||||
roadmapContent = replaceInCurrentMilestone(
|
||||
roadmapContent, tablePattern,
|
||||
`$1 ${summaryCount}/${planCount} $2 ${status.padEnd(11)}$3${dateField}$4`
|
||||
);
|
||||
|
||||
@@ -275,7 +276,7 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
|
||||
const planCountText = isComplete
|
||||
? `${summaryCount}/${planCount} plans complete`
|
||||
: `${summaryCount}/${planCount} plans executed`;
|
||||
roadmapContent = roadmapContent.replace(planCountPattern, `$1${planCountText}`);
|
||||
roadmapContent = replaceInCurrentMilestone(roadmapContent, planCountPattern, `$1${planCountText}`);
|
||||
|
||||
// If complete: check checkbox
|
||||
if (isComplete) {
|
||||
@@ -283,7 +284,7 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
|
||||
`(-\\s*\\[)[ ](\\]\\s*.*Phase\\s+${phaseEscaped}[:\\s][^\\n]*)`,
|
||||
'i'
|
||||
);
|
||||
roadmapContent = roadmapContent.replace(checkboxPattern, `$1x$2 (completed ${today})`);
|
||||
roadmapContent = replaceInCurrentMilestone(roadmapContent, checkboxPattern, `$1x$2 (completed ${today})`);
|
||||
}
|
||||
|
||||
fs.writeFileSync(roadmapPath, roadmapContent, 'utf-8');
|
||||
|
||||
@@ -276,14 +276,16 @@ function cmdStateUpdateProgress(cwd, raw) {
|
||||
|
||||
let content = fs.readFileSync(statePath, 'utf-8');
|
||||
|
||||
// Count summaries across all phases
|
||||
// Count summaries across current milestone phases only
|
||||
const phasesDir = path.join(cwd, '.planning', 'phases');
|
||||
let totalPlans = 0;
|
||||
let totalSummaries = 0;
|
||||
|
||||
if (fs.existsSync(phasesDir)) {
|
||||
const isDirInMilestone = getMilestonePhaseFilter(cwd);
|
||||
const phaseDirs = fs.readdirSync(phasesDir, { withFileTypes: true })
|
||||
.filter(e => e.isDirectory()).map(e => e.name);
|
||||
.filter(e => e.isDirectory()).map(e => e.name)
|
||||
.filter(isDirInMilestone);
|
||||
for (const dir of phaseDirs) {
|
||||
const files = fs.readdirSync(path.join(phasesDir, dir));
|
||||
totalPlans += files.filter(f => f.match(/-PLAN\.md$/i)).length;
|
||||
|
||||
@@ -624,8 +624,8 @@ describe('getRoadmapPhaseInternal', () => {
|
||||
assert.strictEqual(result.goal, 'Create REST endpoints');
|
||||
});
|
||||
|
||||
test('returns null goal when Goal uses colon-outside-bold format', () => {
|
||||
// Actual ROADMAP.md uses **Goal**: (colon outside bold) which the regex does not match
|
||||
test('returns goal when Goal uses colon-outside-bold format', () => {
|
||||
// **Goal**: (colon outside bold) is now supported alongside **Goal:**
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
||||
'### Phase 1: Foundation\n**Goal**: Build the base\n'
|
||||
@@ -633,7 +633,7 @@ describe('getRoadmapPhaseInternal', () => {
|
||||
const result = getRoadmapPhaseInternal(tmpDir, '1');
|
||||
assert.strictEqual(result.found, true);
|
||||
assert.strictEqual(result.phase_name, 'Foundation');
|
||||
assert.strictEqual(result.goal, null);
|
||||
assert.strictEqual(result.goal, 'Build the base');
|
||||
});
|
||||
|
||||
test('returns null when roadmap missing', () => {
|
||||
|
||||
Reference in New Issue
Block a user