Merge branch 'next' into fix/2056-plan-phase-foreign-prefix

This commit is contained in:
Tom Boucher
2026-07-09 19:37:15 -04:00
committed by GitHub
4 changed files with 85 additions and 23 deletions

View File

@@ -16,7 +16,7 @@ const { test, describe, before, 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');
const { runGsdTools, createTempProject, cleanup, parseFrontmatter } = require('./helpers.cjs');
// ─── helpers ─────────────────────────────────────────────────────────────────
@@ -54,6 +54,34 @@ describe('milestone complete command', () => {
beforeEach(() => { tmpDir = createTempProject(); });
afterEach(() => { cleanup(tmpDir); });
test('preserves current_phase frontmatter through milestone complete (#2111)', () => {
// Seed STATE.md mid-phase-19: the real current_phase lives in frontmatter
// and the only body phase source is the `Phase:` prose line (no explicit
// `Current Phase:` field — matching what milestoneCompleteCore writes).
fs.writeFileSync(
path.join(tmpDir, '.planning', 'STATE.md'),
`---\ncurrent_phase: "19"\n---\n# State\n\n**Status:** In progress\n` +
`**Last Activity:** 2025-01-01\n**Last Activity Description:** Working\n\n` +
`## Current Position\n\nPhase: 19 — EXECUTING\nPlan: 1 of 1\n` +
`Status: Executing\nLast activity: 2025-01-01 — Running phase\n`,
);
// No ROADMAP.md — mirrors 'handles missing ROADMAP.md gracefully' so the
// milestone-phase-filter guard never fires.
const result = runGsdTools('milestone complete v0.5 --name Test', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const state = fs.readFileSync(path.join(tmpDir, '.planning', 'STATE.md'), 'utf-8');
const fm = parseFrontmatter(state);
// Fails pre-migration: the unanchored parser mined "5" from
// "Phase: Milestone v0.5 complete" and clobbered current_phase.
assert.strictEqual(
fm.current_phase, '19',
`current_phase must be preserved across milestone complete, not mined from ` +
`the version string (#2111); got ${JSON.stringify(fm.current_phase)}`,
);
});
test('archives roadmap, requirements, creates MILESTONES.md', () => {
writeRoadmap(tmpDir, `# Roadmap v1.0 MVP\n\n### Phase 1: Foundation\n**Goal:** Setup\n`);
fs.writeFileSync(

View File

@@ -3055,6 +3055,39 @@ describe('state complete-phase: decorated Phase fallback (#2761 nitpick)', () =>
assert.ok(!after.includes('Status: Phase Phase complete'));
});
test('rejects a milestone-closure Phase line, never mines the version token (#2111 / #2125)', () => {
// After `milestone complete v0.5`, the only phase signal is the narrative
// `Phase: Milestone v0.5 complete`. The old unanchored resolver mined "0.5"
// and rewrote Status as "Phase 0.5 complete"; the anchored parser yields no
// token, so complete-phase must reject rather than corrupt STATE.md.
const stateMd = [
'---',
'milestone: v0.5',
'---',
'',
'# State',
'',
'**Status:** Awaiting next milestone',
'**Last Activity:** 2024-01-15',
'',
'## Current Position',
'',
'Phase: Milestone v0.5 complete',
'',
].join('\n');
const statePath = path.join(tmpDir, '.planning', 'STATE.md');
fs.writeFileSync(statePath, stateMd);
const result = runGsdTools('state complete-phase', tmpDir);
assert.ok(result.success, 'command should return JSON error payload, not crash');
const output = JSON.parse(result.output);
assert.ok(output.error, 'expected a resolution error, not a phase mined from the version string');
const after = fs.readFileSync(statePath, 'utf-8');
assert.ok(!after.includes('Phase 0.5 complete'), `must not mine "0.5" from the version: ${after}`);
assert.ok(!after.includes('Phase: 0.5'), `must not rewrite Current Position to Phase 0.5: ${after}`);
});
test('supports explicit phase override for complete-phase disambiguation (#3063)', () => {
const stateMd = [
'---',