Files
msd-core/tests/fix-1446-total-phases-corrects-downward.test.cjs
Tom Boucher 4719b36d41 fix(#1445,#1446): exclude 999.x backlog from milestone totals; allow total_phases downward correction (#1490)
* fix(#1445,#1446): exclude 999.x backlog phases from milestone totals; allow total_phases downward correction

#1445: deriveProgressFromRoadmap (phase-lifecycle.cts), the roadmapPhaseCount
loop (state.cts), and getMilestonePhaseFilter (roadmap-parser.cts) all now
skip phase tokens matching /^999\b/ — consistent with the existing init.cts
filter. 999.x backlog dirs are consequently excluded from phaseDirs too.

#1446: shouldPreserveExistingProgress (state-document.cts) no longer includes
total_phases in its ratchet check. total_phases always takes the freshly
derived value; only completed_phases, total_plans, and completed_plans retain
ratchet behaviour.

Regression tests added for both bugs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore: add changeset for #1445/#1446 progress-backlog-exclusion-and-ratchet

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(#1445,#1446): rename test files to fix-NNN convention; fix changeset pr: null

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 13:37:32 -04:00

157 lines
5.9 KiB
JavaScript

'use strict';
/**
* Regression test for bug #1446:
* total_phases must correct downward when re-derived; shouldPreserveExistingProgress
* must NOT include total_phases in its ratchet check.
*
* Root cause:
* shouldPreserveExistingProgress (state-document.cts) returned true when
* existingProgress.total_phases > derivedProgress.total_phases, making the
* stored value sticky even when it was wrong (e.g. counted backlog phases).
*
* Fix:
* total_phases is removed from the "existing exceeds derived" check.
* Only completed_phases, total_plans, and completed_plans keep ratchet behaviour.
*
* Scenarios:
* A. shouldPreserveExistingProgress unit test — returns false when only total_phases differs.
* B. state sync re-derives a lower total_phases and writes the new value.
*/
const { describe, test, beforeEach, afterEach } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const { runGsdTools, createTempProject, cleanup } = require('./helpers.cjs');
const { shouldPreserveExistingProgress } = require('../gsd-core/bin/lib/state-document.cjs');
// ─── Scenario A: unit test ───────────────────────────────────────────────────
describe('bug #1446 — shouldPreserveExistingProgress does not ratchet total_phases', () => {
test('existing total_phases:10 > derived total_phases:7 → returns false (no ratchet)', () => {
const existing = { total_phases: 10, completed_phases: 3, total_plans: 6, completed_plans: 3 };
const derived = { total_phases: 7, completed_phases: 3, total_plans: 6, completed_plans: 3 };
assert.equal(
shouldPreserveExistingProgress(existing, derived),
false,
'total_phases downward correction must NOT trigger shouldPreserveExistingProgress',
);
});
test('existing completed_phases:5 > derived completed_phases:2 → returns true (ratchet still active)', () => {
const existing = { total_phases: 7, completed_phases: 5, total_plans: 6, completed_plans: 3 };
const derived = { total_phases: 7, completed_phases: 2, total_plans: 6, completed_plans: 3 };
assert.equal(
shouldPreserveExistingProgress(existing, derived),
true,
'completed_phases ratchet must still work',
);
});
test('existing total_phases:10 > derived:7 AND completed_phases matches → false (total_phases alone does not preserve)', () => {
const existing = { total_phases: 10, completed_phases: 3 };
const derived = { total_phases: 7, completed_phases: 3 };
assert.equal(
shouldPreserveExistingProgress(existing, derived),
false,
'only-total_phases discrepancy must not trigger preservation',
);
});
test('all derived values equal existing → returns false', () => {
const existing = { total_phases: 7, completed_phases: 3, total_plans: 6, completed_plans: 3 };
const derived = { total_phases: 7, completed_phases: 3, total_plans: 6, completed_plans: 3 };
assert.equal(shouldPreserveExistingProgress(existing, derived), false);
});
});
// ─── Scenario B: end-to-end state sync overwrites inflated total_phases ──────
describe('bug #1446 — state sync writes corrected (lower) total_phases', () => {
let tmpDir;
// ROADMAP has 3 real phases only (no 999.x).
const ROADMAP = [
'## Milestone v1.0: Test',
'',
'### Phase 01: Alpha',
'**Goal:** alpha',
'',
'### Phase 02: Beta',
'**Goal:** beta',
'',
'### Phase 03: Gamma',
'**Goal:** gamma',
].join('\n');
beforeEach(() => {
tmpDir = createTempProject('bug-1446-');
const planning = path.join(tmpDir, '.planning');
fs.writeFileSync(path.join(planning, 'ROADMAP.md'), ROADMAP, 'utf-8');
// STATE.md has a stale inflated total_phases:10 in frontmatter.
fs.writeFileSync(
path.join(planning, 'STATE.md'),
[
'---',
'gsd_state_version: 1.0',
'milestone: v1.0',
'status: executing',
'progress:',
' total_phases: 10',
' completed_phases: 2',
' total_plans: 6',
' completed_plans: 4',
' percent: 40',
'---',
'',
'# GSD State',
'',
'## Configuration',
'Current Phase: 3',
'Status: Executing Phase 3',
'Last Activity: 2026-01-01',
'Progress: [████░░░░░░] 40%',
].join('\n'),
'utf-8',
);
fs.writeFileSync(path.join(planning, 'config.json'), '{}', 'utf-8');
for (const d of ['01-alpha', '02-beta', '03-gamma']) {
const dir = path.join(planning, 'phases', d);
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(path.join(dir, 'PLAN.md'), '# Plan\n', 'utf-8');
// Mark 01 and 02 as complete (2 summaries)
if (d !== '03-gamma') {
fs.writeFileSync(path.join(dir, 'PLAN-SUMMARY.md'), '# Summary\n', 'utf-8');
}
}
});
afterEach(() => {
cleanup(tmpDir);
});
test('state sync corrects total_phases from 10 to 3', () => {
const syncResult = runGsdTools(['state', 'sync'], tmpDir);
assert.ok(syncResult.success, `state sync failed: ${syncResult.error}`);
const jsonResult = runGsdTools(['state', 'json'], tmpDir);
assert.ok(jsonResult.success, `state json failed: ${jsonResult.error}`);
const state = JSON.parse(jsonResult.output);
assert.ok(state.progress, 'state json must return a progress block');
assert.equal(
state.progress.total_phases,
3,
`total_phases must be corrected to 3 (derived), not kept at 10 (stale). Got ${state.progress.total_phases}`,
);
// completed_phases ratchet still works: existing 2 ≥ disk-derived → keep 2
assert.ok(
state.progress.completed_phases >= 2,
`completed_phases must be at least 2 (ratchet). Got ${state.progress.completed_phases}`,
);
});
});