Files
msd-core/tests/phase-completion-single-owner.test.cjs
Jakub Zych a9a7a328e6 refactor: hard-fork GSD -> MSD (Make Software Done)
Mechanical rename produced by scripts/msd-rename.cjs: gsd/Gsd/GSD -> msd/Msd/MSD
across contents and paths, upstream package/repo coordinates -> @golem15/msd-core
and golem15com/msd-core. Deep links into upstream history, sibling upstream
packages, the GSD-2 import feature, CHANGELOG.md and .changeset/ are kept as-is.

Hand edits on top: MSD block-letter banner and logos, LICENSE copyright line,
package/plugin identity, regenerated lockfile, install-tree fixtures, derived
registries and benchmark baseline; migration checksum baseline re-locked
(MSD keeps its own install state, so no install had applied the old sums);
sort-order and regex-escaped expectations in tests adjusted.
2026-10-06 01:47:40 +02:00

742 lines
36 KiB
JavaScript

'use strict';
process.env.MSD_TEST_MODE = '1';
// F2 below reads msd-core/workflows/mvp-phase.md and regex-tests its shell
// content for the disk-strict OR removal (Decision 4(d)). A workflow .md
// file's text IS the deployed prompt-layer artifact the runtime executes —
// there is no runtime API that "runs" mvp-phase.md to observe its shell
// logic behaviorally, so asserting on its source text tests the actual
// deployed contract. #3186 review finding 6(b).
/**
* Phase-completion single-owner tests (epic #3180, issue #3186, ADR-3180
* §7.4, disk-strict per #2957). Covers `.msd/phase/refactor-3186-phase-
* completion-predicate/50-test-matrix.md` sections A-F:
*
* A — the predicate itself (`src/verification.cts` · `isPhaseComplete`)
* B — disk-strict: the ROADMAP checkbox has no machine authority
* C — identity at each CONSUMER's observable output (Decision 4c)
* D — the 0.x-split: sites answering a DIFFERENT question keep answering it
* F — Tier-2 regression surface
*
* Section E (the drift guard itself) lives in
* tests/completion-predicate-drift-guard.test.cjs.
*
* A1 is the #3168 regression (zero plans + passing `*-VERIFICATION.md` must
* read complete). Verified RED-before/GREEN-after manually against this
* change (git stash the src/ edits, rebuild, and probe `init manager` on
* the exact A1 fixture below): pre-fix it reported
* `{ phase_complete: false, verification_status: 'not_required',
* disk_status: 'empty' }`; post-fix it reports
* `{ phase_complete: true, verification_status: 'passed', disk_status:
* 'complete' }`. That evidence is reported in the implementation PR/summary
* rather than re-run here (a stash/rebuild inside a test body would not be
* hermetic); the assertions below pin the GREEN (post-fix) behavior as a
* permanent regression net.
*/
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const { isPhaseComplete } = require('../msd-core/bin/lib/verification.cjs');
const { SCOPE } = require('../msd-core/bin/lib/planning-scope.cjs');
const { scanPhasePlans } = require('../msd-core/bin/lib/plan-scan.cjs');
const { buildWorkstreamInventory } = require('../msd-core/bin/lib/workstream-inventory-builder.cjs');
const { runMsdTools, createTempDir, createTempProject, cleanup } = require('./helpers.cjs');
// ─── Fixture helpers ────────────────────────────────────────────────────────
function writeRoadmap(tmpDir, phases) {
const sections = phases.map((p) => {
let section = `### Phase ${p.number}: ${p.name}\n\n**Goal:** ${p.goal || 'Do the thing'}\n`;
if (p.depends_on) section += `**Depends on:** ${p.depends_on}\n`;
return section;
}).join('\n');
const checklist = phases.map((p) => {
const mark = p.complete ? 'x' : ' ';
return `- [${mark}] **Phase ${p.number}: ${p.name}**`;
}).join('\n');
// A real Progress TABLE (msd-core/templates/roadmap.md shape), not just the
// checklist — cmdRoadmapUpdatePlanProgress writes into this table via the
// markdown-table seam (editProgressTableSlice/updateTableCell), which no-ops
// when no table with these columns exists. G2 (#3186) needs a real table to
// observe the "Plans Complete / Status cells still update, only the
// completion checkbox is withheld" behavior.
const table = [
'| Phase | Plans Complete | Status | Completed |',
'|-------|-----------------|--------|-----------|',
...phases.map((p) => `| ${p.number}. ${p.name} | 0/0 | Not started | - |`),
].join('\n');
const content = `# Roadmap\n\n## Progress\n\n${checklist}\n\n${table}\n\n${sections}`;
fs.writeFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), content);
}
function writeState(tmpDir) {
fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), '---\nstatus: active\n---\n# State\n');
}
function scaffoldPhase(tmpDir, num, opts = {}) {
const padded = String(num).padStart(2, '0');
const slug = opts.slug || 'test-phase';
const dir = path.join(tmpDir, '.planning', 'phases', `${padded}-${slug}`);
fs.mkdirSync(dir, { recursive: true });
if (opts.plans) {
for (let i = 1; i <= opts.plans; i++) {
fs.writeFileSync(path.join(dir, `${padded}-${String(i).padStart(2, '0')}-PLAN.md`), `# Plan ${i}`);
}
}
if (opts.summaries) {
for (let i = 1; i <= opts.summaries; i++) {
fs.writeFileSync(path.join(dir, `${padded}-${String(i).padStart(2, '0')}-SUMMARY.md`), `# Summary ${i}`);
}
}
return dir;
}
function writeVerification(phaseDir, padded, status, filenameOverride) {
const filename = filenameOverride || `${padded}-VERIFICATION.md`;
fs.writeFileSync(path.join(phaseDir, filename), `---\nstatus: ${status}\n---\n# Verification\n`);
}
// ═════════════════════════════════════════════════════════════════════════
// A — isPhaseComplete: the predicate itself
// ═════════════════════════════════════════════════════════════════════════
describe('A — isPhaseComplete: disk-strict, unconditional readVerificationStatus', () => {
test('A1 (#3168 regression): zero plans + passing *-VERIFICATION.md -> complete', (t) => {
const dir = createTempDir('msd-phase-complete-a1-');
t.after(() => cleanup(dir));
writeVerification(dir, '01', 'passed');
const result = isPhaseComplete(dir);
assert.strictEqual(result.value.complete, true);
assert.strictEqual(result.value.verification.status, 'passed');
assert.strictEqual(result.scope, SCOPE.COMPLETE);
});
test('A2: plans present, all summarized, passing verification -> complete', (t) => {
const dir = createTempDir('msd-phase-complete-a2-');
t.after(() => cleanup(dir));
fs.writeFileSync(path.join(dir, '01-01-PLAN.md'), '# Plan');
fs.writeFileSync(path.join(dir, '01-01-SUMMARY.md'), '# Summary');
writeVerification(dir, '01', 'passed');
const result = isPhaseComplete(dir);
assert.strictEqual(result.value.complete, true);
});
test('A3: plans present, all summarized, NO verification -> not complete', (t) => {
const dir = createTempDir('msd-phase-complete-a3-');
t.after(() => cleanup(dir));
fs.writeFileSync(path.join(dir, '01-01-PLAN.md'), '# Plan');
fs.writeFileSync(path.join(dir, '01-01-SUMMARY.md'), '# Summary');
const result = isPhaseComplete(dir);
assert.strictEqual(result.value.complete, false);
assert.strictEqual(result.value.verification.status, 'missing');
});
test('A4: verification present but FAILING -> not complete, distinguishable from absent', (t) => {
const dir = createTempDir('msd-phase-complete-a4-');
t.after(() => cleanup(dir));
writeVerification(dir, '01', 'gaps_found');
const result = isPhaseComplete(dir);
assert.strictEqual(result.value.complete, false);
assert.strictEqual(result.value.verification.status, 'gaps_found');
assert.notStrictEqual(result.value.verification.status, 'missing');
});
test('A5: zero plans, NO verification -> not complete', (t) => {
const dir = createTempDir('msd-phase-complete-a5-');
t.after(() => cleanup(dir));
const result = isPhaseComplete(dir);
assert.strictEqual(result.value.complete, false);
assert.strictEqual(result.value.verification.status, 'missing');
});
test('A6: plan count boundary 0/1/2 with passing verification -> complete at every count', (t) => {
for (const planCount of [0, 1, 2]) {
const dir = createTempDir(`msd-phase-complete-a6-${planCount}-`);
t.after(() => cleanup(dir));
for (let i = 1; i <= planCount; i++) {
fs.writeFileSync(path.join(dir, `01-0${i}-PLAN.md`), `# Plan ${i}`);
fs.writeFileSync(path.join(dir, `01-0${i}-SUMMARY.md`), `# Summary ${i}`);
}
writeVerification(dir, '01', 'passed');
const result = isPhaseComplete(dir);
assert.strictEqual(result.value.complete, true, `planCount=${planCount} must be complete`);
}
});
test('A7: phase dir unreadable -> non-COMPLETE scope, not a false "incomplete"', (t) => {
const dir = createTempDir('msd-phase-complete-a7-');
t.after(() => cleanup(dir));
// Injected via a fake `deps.fs` (method monkeypatching through the
// function's own dependency-injection seam) rather than chmod 0o000 —
// chmod is bypassed by root/Docker CI and does not exercise the code
// path deterministically.
const fakeFs = {
readdirSync: () => {
throw new Error('EACCES: permission denied, scandir');
},
readFileSync: fs.readFileSync,
statSync: fs.statSync,
};
const result = isPhaseComplete(dir, { fs: fakeFs });
assert.notStrictEqual(result.scope, SCOPE.COMPLETE);
assert.strictEqual(result.scope, SCOPE.UNREADABLE);
});
test('A8: multiple *-VERIFICATION.md files, one passing one failing -> defined, documented verdict', (t) => {
const dir = createTempDir('msd-phase-complete-a8-');
t.after(() => cleanup(dir));
// readVerificationStatus (which isPhaseComplete wraps) takes the
// lexicographically-FIRST matching filename (`.sort()[0]`) — pin that
// contract here rather than leaving "multiple files" undefined.
writeVerification(dir, '01', 'gaps_found', '01-A-VERIFICATION.md');
writeVerification(dir, '01', 'passed', '01-B-VERIFICATION.md');
const result = isPhaseComplete(dir);
assert.strictEqual(result.value.verification.status, 'gaps_found', '01-A- sorts before 01-B-');
assert.strictEqual(result.value.complete, false);
});
test('A9: CRLF in the verification file -> identical to LF', (t) => {
const dirLf = createTempDir('msd-phase-complete-a9-lf-');
const dirCrlf = createTempDir('msd-phase-complete-a9-crlf-');
t.after(() => {
cleanup(dirLf);
cleanup(dirCrlf);
});
fs.writeFileSync(path.join(dirLf, '01-VERIFICATION.md'), '---\nstatus: passed\n---\n# Verification\n');
fs.writeFileSync(path.join(dirCrlf, '01-VERIFICATION.md'), '---\r\nstatus: passed\r\n---\r\n# Verification\r\n');
const lfResult = isPhaseComplete(dirLf);
const crlfResult = isPhaseComplete(dirCrlf);
assert.strictEqual(lfResult.value.complete, true);
assert.strictEqual(crlfResult.value.complete, true);
assert.strictEqual(crlfResult.value.verification.status, lfResult.value.verification.status);
});
});
// ═════════════════════════════════════════════════════════════════════════
// B — Disk-strict: the checkbox has no machine authority
// ═════════════════════════════════════════════════════════════════════════
describe('B — disk-strict: ROADMAP checkbox carries no machine authority', () => {
function fixture(tmpDir, { checked, plans, summaries, verificationStatus }) {
writeState(tmpDir);
writeRoadmap(tmpDir, [{ number: '1', name: 'Foo', complete: checked }]);
const dir = scaffoldPhase(tmpDir, 1, { slug: 'foo', plans, summaries });
if (verificationStatus) writeVerification(dir, '01', verificationStatus);
return dir;
}
test('B1: checkbox ticked, plans outstanding, no verification -> NOT complete (the Tier-2 break)', () => {
const tmpDir = createTempProject();
try {
fixture(tmpDir, { checked: true, plans: 2, summaries: 0 });
const result = runMsdTools('roadmap analyze --raw', tmpDir);
assert.ok(result.success, result.error);
const analyzed = JSON.parse(result.output);
// 2 plans, 0 summaries -> 'planned' (disk_status's own taxonomy: no
// summaries yet means "planned", not "partial" — 'partial' requires
// summaryCount > 0). The load-bearing assertion for the Tier-2 break is
// the notStrictEqual below: the checkbox alone must not read 'complete'.
assert.strictEqual(analyzed.phases[0].disk_status, 'planned');
assert.notStrictEqual(analyzed.phases[0].disk_status, 'complete');
} finally {
cleanup(tmpDir);
}
});
test('B2: checkbox ticked AND verification passing -> complete (checkbox contributed nothing)', () => {
const tmpDir = createTempProject();
try {
fixture(tmpDir, { checked: true, plans: 1, summaries: 1, verificationStatus: 'passed' });
const result = runMsdTools('roadmap analyze --raw', tmpDir);
assert.ok(result.success, result.error);
const analyzed = JSON.parse(result.output);
assert.strictEqual(analyzed.phases[0].disk_status, 'complete');
} finally {
cleanup(tmpDir);
}
});
test('B3: checkbox UNTICKED, verification passing -> complete (disk wins in both directions)', () => {
const tmpDir = createTempProject();
try {
fixture(tmpDir, { checked: false, plans: 1, summaries: 1, verificationStatus: 'passed' });
const result = runMsdTools('roadmap analyze --raw', tmpDir);
assert.ok(result.success, result.error);
const analyzed = JSON.parse(result.output);
assert.strictEqual(analyzed.phases[0].disk_status, 'complete');
assert.strictEqual(analyzed.phases[0].roadmap_complete, false, 'checkbox itself stays unticked/reported');
} finally {
cleanup(tmpDir);
}
});
test('B4: checkbox ticked, verification FAILING -> not complete', () => {
const tmpDir = createTempProject();
try {
fixture(tmpDir, { checked: true, plans: 1, summaries: 1, verificationStatus: 'gaps_found' });
const result = runMsdTools('roadmap analyze --raw', tmpDir);
assert.ok(result.success, result.error);
const analyzed = JSON.parse(result.output);
assert.notStrictEqual(analyzed.phases[0].disk_status, 'complete');
} finally {
cleanup(tmpDir);
}
});
test('B5: ROADMAP.md absent entirely -> the predicate itself is unaffected (isPhaseComplete never reads it)', (t) => {
const dir = createTempDir('msd-phase-complete-b5-');
t.after(() => cleanup(dir));
writeVerification(dir, '01', 'passed');
// No ROADMAP.md anywhere near `dir` — isPhaseComplete takes a phase
// directory, not a project root, and never touches ROADMAP.md.
const result = isPhaseComplete(dir);
assert.strictEqual(result.value.complete, true);
});
test('B6: a ticked checkbox is NOT deleted from ROADMAP.md — only its authority is removed', () => {
const tmpDir = createTempProject();
try {
fixture(tmpDir, { checked: true, plans: 2, summaries: 0 });
const before = fs.readFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), 'utf-8');
assert.ok(before.includes('[x] **Phase 1'), 'fixture sanity: checkbox starts ticked');
const result = runMsdTools('roadmap analyze --raw', tmpDir);
assert.ok(result.success, result.error);
const after = fs.readFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), 'utf-8');
assert.strictEqual(after, before, 'roadmap analyze is read-only: the human annotation survives untouched');
assert.ok(after.includes('[x] **Phase 1'), 'the ticked checkbox itself is still present');
} finally {
cleanup(tmpDir);
}
});
});
// ═════════════════════════════════════════════════════════════════════════
// C — Identity at each CONSUMER's observable output (Decision 4c)
// ═════════════════════════════════════════════════════════════════════════
describe('C — consumer identity: every reader of "is phase P complete?" agrees', () => {
test('C1: init manager reports complete for the A1 fixture (the #3168 symptom)', () => {
const tmpDir = createTempProject();
try {
writeState(tmpDir);
writeRoadmap(tmpDir, [{ number: '1', name: 'Foo' }]);
const dir = scaffoldPhase(tmpDir, 1, { slug: 'foo' }); // zero plans
writeVerification(dir, '01', 'passed');
const result = runMsdTools('init manager --raw', tmpDir);
assert.ok(result.success, result.error);
const output = JSON.parse(result.output);
const phase1 = output.phases.find((p) => p.number === '1' || p.number === '01');
assert.strictEqual(phase1.phase_complete, true);
assert.strictEqual(phase1.verification_status, 'passed');
assert.notStrictEqual(phase1.verification_status, 'not_required');
} finally {
cleanup(tmpDir);
}
});
test('C2: roadmap analyze disk_status matches the owner, with no checkbox arm', () => {
const tmpDir = createTempProject();
try {
writeState(tmpDir);
writeRoadmap(tmpDir, [{ number: '1', name: 'Foo' }]); // checkbox UNTICKED
const dir = scaffoldPhase(tmpDir, 1, { slug: 'foo' });
writeVerification(dir, '01', 'passed');
const owner = isPhaseComplete(dir);
const result = runMsdTools('roadmap analyze --raw', tmpDir);
assert.ok(result.success, result.error);
const analyzed = JSON.parse(result.output);
assert.strictEqual(analyzed.phases[0].disk_status === 'complete', owner.value.complete);
} finally {
cleanup(tmpDir);
}
});
test('C3: phase complete is unchanged — still succeeds exactly when the owner says complete', () => {
const tmpDir = createTempProject();
try {
writeState(tmpDir);
writeRoadmap(tmpDir, [{ number: '1', name: 'Foo' }]);
const dir = scaffoldPhase(tmpDir, 1, { slug: 'foo' }); // zero plans, A1 shape
writeVerification(dir, '01', 'passed');
const owner = isPhaseComplete(dir);
assert.strictEqual(owner.value.complete, true);
const result = runMsdTools('phase complete 1 --raw', tmpDir);
assert.ok(result.success, `phase complete must succeed when the owner reports complete: ${result.error}`);
} finally {
cleanup(tmpDir);
}
});
test('C4: roadmap update-plan-progress "complete" field matches the owner', () => {
const tmpDir = createTempProject();
try {
writeState(tmpDir);
writeRoadmap(tmpDir, [{ number: '1', name: 'Foo' }]);
const dir = scaffoldPhase(tmpDir, 1, { slug: 'foo', plans: 1, summaries: 1 });
writeVerification(dir, '01', 'gaps_found'); // failing -> owner says not complete
const owner = isPhaseComplete(dir);
assert.strictEqual(owner.value.complete, false);
// No --raw: cmdRoadmapUpdatePlanProgress's output() call carries a
// non-undefined rawValue (a "N/N Status" text fallback), so --raw
// would switch this to plain text instead of JSON (unlike roadmap
// analyze / init manager, whose output() calls pass rawValue:
// undefined and always emit JSON regardless of --raw).
const result = runMsdTools('roadmap update-plan-progress 1', tmpDir);
assert.ok(result.success, result.error);
const output = JSON.parse(result.output);
assert.strictEqual(output.complete, owner.value.complete);
} finally {
cleanup(tmpDir);
}
});
test('C5: cross-consumer — one fixture, init manager AND roadmap analyze report the SAME verdict', () => {
const tmpDir = createTempProject();
try {
writeState(tmpDir);
writeRoadmap(tmpDir, [{ number: '1', name: 'Foo' }]);
const dir = scaffoldPhase(tmpDir, 1, { slug: 'foo' }); // zero plans
writeVerification(dir, '01', 'passed');
const initResult = runMsdTools('init manager --raw', tmpDir);
const roadmapResult = runMsdTools('roadmap analyze --raw', tmpDir);
assert.ok(initResult.success, initResult.error);
assert.ok(roadmapResult.success, roadmapResult.error);
const initPhase = JSON.parse(initResult.output).phases.find((p) => p.number === '1' || p.number === '01');
const roadmapPhase = JSON.parse(roadmapResult.output).phases[0];
assert.strictEqual(initPhase.phase_complete, true);
assert.strictEqual(roadmapPhase.disk_status, 'complete');
} finally {
cleanup(tmpDir);
}
});
describe('C6: the §7.4 headline — "phase complete succeeds while init manager reports incomplete" is unrepresentable', () => {
test('agreement case: zero plans + passing verification -> BOTH succeed/report complete', () => {
const tmpDir = createTempProject();
try {
writeState(tmpDir);
writeRoadmap(tmpDir, [{ number: '1', name: 'Foo' }]);
const dir = scaffoldPhase(tmpDir, 1, { slug: 'foo' });
writeVerification(dir, '01', 'passed');
const initResult = runMsdTools('init manager --raw', tmpDir);
assert.ok(initResult.success, initResult.error);
const initPhase = JSON.parse(initResult.output).phases.find((p) => p.number === '1' || p.number === '01');
assert.strictEqual(initPhase.phase_complete, true);
const completeResult = runMsdTools('phase complete 1 --raw', tmpDir);
assert.ok(completeResult.success, `phase complete must succeed to agree with init manager: ${completeResult.error}`);
} finally {
cleanup(tmpDir);
}
});
test('agreement case: plans outstanding, no verification -> BOTH report/refuse incomplete', () => {
const tmpDir = createTempProject();
try {
writeState(tmpDir);
writeRoadmap(tmpDir, [{ number: '1', name: 'Foo' }]);
scaffoldPhase(tmpDir, 1, { slug: 'foo', plans: 1, summaries: 1 }); // no *-VERIFICATION.md written
const initResult = runMsdTools('init manager --raw', tmpDir);
assert.ok(initResult.success, initResult.error);
const initPhase = JSON.parse(initResult.output).phases.find((p) => p.number === '1' || p.number === '01');
assert.strictEqual(initPhase.phase_complete, false);
const completeResult = runMsdTools('phase complete 1 --raw', tmpDir);
assert.strictEqual(completeResult.success, false, 'phase complete must be BLOCKED to agree with init manager reporting incomplete');
} finally {
cleanup(tmpDir);
}
});
});
});
// ═════════════════════════════════════════════════════════════════════════
// D — the 0.x-split: sites asking a DIFFERENT question keep answering it
// ═════════════════════════════════════════════════════════════════════════
describe('D — the 0.x split: "are plans summarized" stays a different, legitimate answer', () => {
function buildD1Fixture(t) {
// All plans summarized, but NO *-VERIFICATION.md — the exact fixture
// the design's "0.x split" section names as the trap.
const dir = createTempDir('msd-phase-completion-d1-');
t.after(() => cleanup(dir));
fs.writeFileSync(path.join(dir, '01-01-PLAN.md'), '# Plan');
fs.writeFileSync(path.join(dir, '01-01-SUMMARY.md'), '# Summary');
return dir;
}
test('D1: scanPhasePlans.completed still reports true — "are plans summarized" is a different question', (t) => {
const dir = buildD1Fixture(t);
const scan = scanPhasePlans(dir);
assert.strictEqual(scan.completed, true, 'plan-scan.cts answers "are all plans summarized", not "is the phase complete"');
});
test('D2: the SAME fixture through isPhaseComplete -> NOT complete (the two answers legitimately differ)', (t) => {
const dir = buildD1Fixture(t);
const owner = isPhaseComplete(dir);
assert.strictEqual(owner.value.complete, false);
const scan = scanPhasePlans(dir);
assert.notStrictEqual(scan.completed, owner.value.complete, 'the two derivations must legitimately disagree on this exact fixture');
});
// ADR-3180 §7.4 (#3186 review finding 3, corrected from this phase's own
// design doc): `buildWorkstreamInventory` was ORIGINALLY (wrongly)
// classified as staying on the "different question" side of the 0.x
// split, the same way `scanPhasePlans.completed` legitimately does. Review
// found it reproduced the §7.4 headline case (#3168) in a third surface —
// it combined a local summaries-met derivation with caller-supplied
// verification data to decide the SAME "is phase P complete?" question,
// not a different one. Corrected: the module is a pure, I/O-free
// projection (module header: "No I/O. No async.") that cannot call
// `isPhaseComplete` itself, so per Decision 4(c) the CALLER computes the
// owner's real verdict and passes it in via `PhaseFilesCount.complete` —
// `workstream-inventory.cts` does this with a real `isPhaseComplete` call
// in production. D4 now pins that routing directly.
test('D4: buildWorkstreamInventory on the D1 fixture is NOT complete when the caller supplies the owner\'s real (false) verdict', (t) => {
const dir = buildD1Fixture(t);
const scan = scanPhasePlans(dir);
const owner = isPhaseComplete(dir);
assert.strictEqual(owner.value.complete, false, 'fixture sanity: no *-VERIFICATION.md -> the owner says not complete');
const inventory = buildWorkstreamInventory({
name: 'default',
projectDir: path.dirname(dir),
workstreamDir: path.dirname(dir),
phaseDirNames: ['01-fixture'],
activeWorkstreamName: 'default',
phaseFilesCounts: [
{
directory: '01-fixture',
planCount: scan.planCount,
summaryCount: scan.summaryCount,
// ADR-3180 §7.4 (#3186): the caller-computed owner verdict, NOT a
// local re-derivation from planCount/summaryCount.
complete: owner.value.complete,
},
],
roadmapPhaseCount: 1,
stateProjection: { status: 'in_progress', current_phase: '01', last_activity: null },
filesExist: { roadmap: true, state: true, requirements: false },
});
assert.strictEqual(
inventory.phases[0].status,
'in_progress',
'summaries-met alone no longer resolves complete — the builder routes through the caller-supplied owner verdict (#3186 fix)',
);
});
test('D5: buildWorkstreamInventory reports complete for a zero-plan phase when the caller supplies a true owner verdict (#3168 parity)', (t) => {
const dir = createTempDir('msd-phase-completion-d5-');
t.after(() => cleanup(dir));
writeVerification(dir, '01', 'passed');
const owner = isPhaseComplete(dir);
assert.strictEqual(owner.value.complete, true);
const inventory = buildWorkstreamInventory({
name: 'default',
projectDir: path.dirname(dir),
workstreamDir: path.dirname(dir),
phaseDirNames: ['01-fixture'],
activeWorkstreamName: 'default',
phaseFilesCounts: [
{ directory: '01-fixture', planCount: 0, summaryCount: 0, complete: owner.value.complete },
],
roadmapPhaseCount: 1,
stateProjection: { status: 'in_progress', current_phase: '01', last_activity: null },
filesExist: { roadmap: true, state: true, requirements: false },
});
assert.strictEqual(
inventory.phases[0].status,
'complete',
'zero plans + a passing verification -> complete, matching isPhaseComplete (#3168) even with planCount 0',
);
});
});
// ═════════════════════════════════════════════════════════════════════════
// F — Tier-2 regression surface
// ═════════════════════════════════════════════════════════════════════════
describe('F — Tier-2 regression surface', () => {
test('F1: a project relying on checkbox-only completion — roadmap analyze stops reporting complete (documented break)', () => {
// Same fixture shape as B1, framed as the Tier-2 regression this phase
// ships deliberately: a downstream project that was relying on a ticked
// checkbox alone (no passing verification, plans outstanding) now sees
// `disk_status` flip away from 'complete'.
const tmpDir = createTempProject();
try {
writeState(tmpDir);
writeRoadmap(tmpDir, [{ number: '1', name: 'Foo', complete: true }]);
scaffoldPhase(tmpDir, 1, { slug: 'foo', plans: 3, summaries: 0 });
const result = runMsdTools('roadmap analyze --raw', tmpDir);
assert.ok(result.success, result.error);
const analyzed = JSON.parse(result.output);
assert.notStrictEqual(analyzed.phases[0].disk_status, 'complete');
} finally {
cleanup(tmpDir);
}
});
test('F2: msd-core/workflows/mvp-phase.md no longer ORs PHASE_COMPLETE with disk status', () => {
const content = fs.readFileSync(
path.join(__dirname, '..', 'msd-core', 'workflows', 'mvp-phase.md'),
'utf-8',
);
assert.ok(
!/"\$DISK_STATUS"\s*==\s*"complete"\s*\|\|\s*"\$PHASE_COMPLETE"/.test(content),
'the disk-strict OR must be gone from mvp-phase.md',
);
assert.ok(
/if \[\[ "\$DISK_STATUS" == "complete" \]\]; then/.test(content),
'DISK_STATUS alone must decide completion',
);
});
test('F3: init manager never emits the old not_required sentinel for a zero-plan phase (regression net replacing it)', () => {
const tmpDir = createTempProject();
try {
writeState(tmpDir);
writeRoadmap(tmpDir, [{ number: '1', name: 'Foo' }]);
scaffoldPhase(tmpDir, 1, { slug: 'foo' }); // zero plans, no verification either
const result = runMsdTools('init manager --raw', tmpDir);
assert.ok(result.success, result.error);
const output = JSON.parse(result.output);
const phase1 = output.phases.find((p) => p.number === '1' || p.number === '01');
assert.notStrictEqual(phase1.verification_status, 'not_required', 'not_required is retired — the owner always reports a real readVerificationStatus verdict');
assert.strictEqual(phase1.verification_status, 'missing');
assert.strictEqual(phase1.phase_complete, false);
} finally {
cleanup(tmpDir);
}
});
});
// ═════════════════════════════════════════════════════════════════════════
// G — write-path plan-coverage gate (#3186 review finding 1, #2648
// precedent). `isPhaseComplete` deliberately has NO plan-count precondition
// (the owner), but `roadmap update-plan-progress` WRITES a checkbox +
// completion date into ROADMAP.md — a stronger claim than "verification
// passed" — and must additionally refuse when a plan has no completion
// record, exactly like `phase complete`'s own #2648 gate. Matrix rows A-F
// never covered "plans added AFTER a still-fresh passing verification";
// this is that row.
// ═════════════════════════════════════════════════════════════════════════
describe('G — write-path plan-coverage gate: a plan added after a still-fresh passing verification', () => {
test('G1: the OWNER (isPhaseComplete) reports complete — no plan-count precondition, by design', () => {
const dir = createTempDir('msd-phase-completion-g1-');
try {
fs.writeFileSync(path.join(dir, '01-01-PLAN.md'), '# Plan 1');
fs.writeFileSync(path.join(dir, '01-01-SUMMARY.md'), '# Summary 1');
writeVerification(dir, '01', 'passed');
// A second plan lands AFTER verification passed — never summarized.
// The verification file is untouched, so its status stays 'passed'
// (readVerificationStatus's staleness check compares only SUMMARY
// mtimes against the verification file, never plan count).
fs.writeFileSync(path.join(dir, '01-02-PLAN.md'), '# Plan 2');
const owner = isPhaseComplete(dir);
assert.strictEqual(owner.value.verification.status, 'passed', 'fixture sanity: verification reads as still-fresh passed');
assert.strictEqual(owner.value.complete, true, 'the owner has no plan-count precondition by design (ADR-3180 §7.4 hard constraint) — this is not the bug');
} finally {
cleanup(dir);
}
});
test('G2: roadmap update-plan-progress REFUSES to write completion for the identical fixture', () => {
const tmpDir = createTempProject();
try {
writeState(tmpDir);
writeRoadmap(tmpDir, [{ number: '1', name: 'Foo' }]);
const dir = scaffoldPhase(tmpDir, 1, { slug: 'foo', plans: 1, summaries: 1 });
writeVerification(dir, '01', 'passed');
fs.writeFileSync(path.join(dir, '01-02-PLAN.md'), '# Plan 2'); // outstanding, no summary
const roadmapBefore = fs.readFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), 'utf-8');
const result = runMsdTools('roadmap update-plan-progress 1', tmpDir);
assert.ok(result.success, result.error);
const output = JSON.parse(result.output);
assert.strictEqual(output.complete, false, 'the write site must refuse — an outstanding plan has no completion record (#2648 precedent)');
const roadmapAfter = fs.readFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), 'utf-8');
assert.ok(!roadmapAfter.includes('[x] **Phase 1'), 'the checkbox must NOT be checked');
assert.ok(!/\(completed \d{4}-\d{2}-\d{2}\)/.test(roadmapAfter), 'no completion date must be stamped');
assert.notStrictEqual(roadmapAfter, roadmapBefore, 'the command still updates the Plans Complete / Status cells — only the completion checkbox is withheld');
} finally {
cleanup(tmpDir);
}
});
test('G3: phase complete agrees — refuses for the identical fixture, same reason class', () => {
const tmpDir = createTempProject();
try {
writeState(tmpDir);
writeRoadmap(tmpDir, [{ number: '1', name: 'Foo' }]);
const dir = scaffoldPhase(tmpDir, 1, { slug: 'foo', plans: 1, summaries: 1 });
writeVerification(dir, '01', 'passed');
fs.writeFileSync(path.join(dir, '01-02-PLAN.md'), '# Plan 2');
const updateResult = runMsdTools('roadmap update-plan-progress 1', tmpDir);
assert.ok(updateResult.success, updateResult.error);
const updateOutput = JSON.parse(updateResult.output);
const completeResult = runMsdTools('phase complete 1 --raw', tmpDir);
assert.strictEqual(completeResult.success, false, 'phase complete must refuse (its own #2648 gate)');
assert.strictEqual(updateOutput.complete, false, 'both write paths agree: incomplete');
} finally {
cleanup(tmpDir);
}
});
test('G4: once the outstanding plan gets its summary, both write paths agree completion proceeds', () => {
const tmpDir = createTempProject();
try {
writeState(tmpDir);
writeRoadmap(tmpDir, [{ number: '1', name: 'Foo' }]);
const dir = scaffoldPhase(tmpDir, 1, { slug: 'foo', plans: 2, summaries: 2 });
writeVerification(dir, '01', 'passed');
const result = runMsdTools('roadmap update-plan-progress 1', tmpDir);
assert.ok(result.success, result.error);
const output = JSON.parse(result.output);
assert.strictEqual(output.complete, true, 'no outstanding plan — the gate does not withhold completion');
const roadmapAfter = fs.readFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), 'utf-8');
assert.ok(/\(completed \d{4}-\d{2}-\d{2}\)/.test(roadmapAfter), 'completion date IS stamped once plan coverage is satisfied');
} finally {
cleanup(tmpDir);
}
});
});