Files
msd-core/tests/health-diagnostic-rules/consistency.test.cjs

347 lines
14 KiB
JavaScript

'use strict';
/**
* Tests for `src/health-diagnostic-rules/consistency.cts` (Phase 12, #3310,
* ADR-3180 §8.4) — the "consistency" (`C0NN`) rule group: C001-C004, plus
* `health-diagnostic.cts`'s `CONSISTENCY_RULES`/`evaluateConsistencyRules`
* composition (W006/W007 reuse proof).
*
* Design: .gsd/phase/feat-3310-enhance-3180-the-sibling-validators-shar/40-design.md
* Test matrix: .gsd/phase/feat-3310-enhance-3180-the-sibling-validators-shar/50-test-matrix.md
* section 2 (rows 9-11), section 3 (C001-C004)
*
* Fixture provenance (§8.5 + CONTRIBUTING "Fixture provenance (#2371)"):
* - C001/C002/C004 are MECHANICAL MUTATION — a numbering gap / a missing
* `wave:` line, mirroring the exact fixture shapes
* `tests/planning-snapshot.test.cjs`'s Phase-12 field tests already use
* for the same underlying `PlanningSnapshot` fields.
* - C003 REUSES `tests/health-diagnostic-rules/phase-structure.test.cjs`'s
* I001 fixture family (a live PLAN.md with no matching SUMMARY.md),
* inverted: a SUMMARY.md with no matching live PLAN.md.
*
* Every case calls the REAL `buildPlanningSnapshot(cwd)` against a real
* temp `.planning/` tree, then the REAL rule `check` functions from the
* compiled module under test — no hand-built in-memory snapshot mocks,
* mirroring `tests/health-diagnostic-rules/roadmap-disk-consistency.test.cjs`.
*/
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const { createTempDir, cleanup } = require('../helpers.cjs');
const { buildPlanningSnapshot } = require('../../gsd-core/bin/lib/planning-snapshot.cjs');
const consistencyMod = require('../../gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs');
const { RULES } = consistencyMod;
const {
SEVERITY,
REMEDY_ACTION,
REMEDY_RISK,
CONSISTENCY_RULES,
evaluateConsistencyRules,
evaluateRules,
} = require('../../gsd-core/bin/lib/health-diagnostic.cjs');
function ruleFor(code) {
const rule = RULES.find((r) => r.code === code);
assert.ok(rule, `rule ${code} not found in RULES`);
return rule;
}
// ─── Fixture helpers (mirrors tests/planning-snapshot.test.cjs) ────────────
function planningDirOf(cwd) {
return path.join(cwd, '.planning');
}
function writeRoadmap(cwd, content) {
fs.mkdirSync(planningDirOf(cwd), { recursive: true });
fs.writeFileSync(path.join(planningDirOf(cwd), 'ROADMAP.md'), content);
}
function writeFile(cwd, relPath, content) {
const full = path.join(cwd, relPath);
fs.mkdirSync(path.dirname(full), { recursive: true });
fs.writeFileSync(full, content);
}
function makePhaseDir(cwd, dirName) {
fs.mkdirSync(path.join(planningDirOf(cwd), 'phases', dirName), { recursive: true });
}
function writePlan(cwd, relPhasePath, planName, frontmatterLines) {
const lines = frontmatterLines ? ['---', ...frontmatterLines, '---', '', '# Plan', ''] : ['# Plan', ''];
writeFile(cwd, `${relPhasePath}/${planName}`, lines.join('\n'));
}
// ─── RULES shape ────────────────────────────────────────────────────────────
describe('RULES (consistency group)', () => {
test('exports exactly 4 rules: C001, C002, C003, C004', () => {
assert.deepEqual(RULES.map((r) => r.code).sort(), ['C001', 'C002', 'C003', 'C004']);
});
test('all four rules are severity WARNING with an ADVISE remedy shape', () => {
for (const code of ['C001', 'C002', 'C003', 'C004']) {
assert.equal(ruleFor(code).severity, SEVERITY.WARNING);
assert.equal(ruleFor(code).repairable, false);
}
});
});
// ─── C001 — gap in disk phase numbering ────────────────────────────────────
describe('C001 — gap in disk phase numbering', () => {
test('MECHANICAL MUTATION: phases 1, 2, 4 on disk (3 missing) fires C001', (t) => {
const cwd = createTempDir('gsd-3310-c001-');
t.after(() => cleanup(cwd));
writeRoadmap(cwd, ['## v1.0 Current 🚧', ''].join('\n'));
makePhaseDir(cwd, '01-foo');
makePhaseDir(cwd, '02-bar');
makePhaseDir(cwd, '04-baz');
const snapshot = buildPlanningSnapshot(cwd);
const diagnostics = ruleFor('C001').check(snapshot);
assert.equal(diagnostics.length, 1);
assert.deepEqual(diagnostics[0], {
code: 'C001',
severity: SEVERITY.WARNING,
message: 'Gap in phase numbering: 2 → 4',
remedy: {
action: REMEDY_ACTION.ADVISE,
risk: REMEDY_RISK.NONE,
args: { command: 'Create the missing phase directory or renumber to close the gap' },
},
});
});
test('baseline: a sequential integer run produces no diagnostics', (t) => {
const cwd = createTempDir('gsd-3310-c001-neg-');
t.after(() => cleanup(cwd));
makePhaseDir(cwd, '01-foo');
makePhaseDir(cwd, '02-bar');
makePhaseDir(cwd, '03-baz');
const snapshot = buildPlanningSnapshot(cwd);
assert.deepEqual(ruleFor('C001').check(snapshot), []);
});
test('boundary: phase_naming "custom" skips the integer-sequence gap check entirely', (t) => {
const cwd = createTempDir('gsd-3310-c001-custom-');
t.after(() => cleanup(cwd));
fs.mkdirSync(planningDirOf(cwd), { recursive: true });
fs.writeFileSync(
path.join(planningDirOf(cwd), 'config.json'),
JSON.stringify({ phase_naming: 'custom' }, null, 2),
);
makePhaseDir(cwd, '01-foo');
makePhaseDir(cwd, '04-baz');
const snapshot = buildPlanningSnapshot(cwd);
assert.equal(snapshot.config.value?.['phase_naming'], 'custom');
assert.deepEqual(ruleFor('C001').check(snapshot), []);
});
test('a sentinel phase dir (999-interim) does not produce a spurious gap', (t) => {
const cwd = createTempDir('gsd-3310-c001-sentinel-');
t.after(() => cleanup(cwd));
makePhaseDir(cwd, '01-foo');
makePhaseDir(cwd, '02-bar');
makePhaseDir(cwd, '999-interim');
const snapshot = buildPlanningSnapshot(cwd);
assert.deepEqual(ruleFor('C001').check(snapshot), []);
});
});
// ─── C002 — gap in plan numbering within a phase ───────────────────────────
describe('C002 — gap in plan numbering within a phase', () => {
test('MECHANICAL MUTATION: 01-PLAN.md, 03-PLAN.md in one phase dir fires C002', (t) => {
const cwd = createTempDir('gsd-3310-c002-');
t.after(() => cleanup(cwd));
writePlan(cwd, '.planning/phases/01-foo', '01-01-PLAN.md');
writePlan(cwd, '.planning/phases/01-foo', '01-03-PLAN.md');
const snapshot = buildPlanningSnapshot(cwd);
const diagnostics = ruleFor('C002').check(snapshot);
assert.equal(diagnostics.length, 1);
assert.deepEqual(diagnostics[0], {
code: 'C002',
severity: SEVERITY.WARNING,
message: 'Gap in plan numbering in 01-foo: plan 1 → 3',
remedy: {
action: REMEDY_ACTION.ADVISE,
risk: REMEDY_RISK.NONE,
args: { command: 'Create the missing plan or renumber to close the gap' },
},
});
});
test('boundary: a gap of exactly one (01, 02) still reports the same C002 subject', (t) => {
const cwd = createTempDir('gsd-3310-c002-gap1-');
t.after(() => cleanup(cwd));
writePlan(cwd, '.planning/phases/01-foo', '01-01-PLAN.md');
writePlan(cwd, '.planning/phases/01-foo', '01-02-PLAN.md');
const snapshot = buildPlanningSnapshot(cwd);
// Sequential (01, 02) is NOT a gap — sanity baseline for the boundary
// pairing below (a real gap must be > 1, not merely non-empty).
assert.deepEqual(ruleFor('C002').check(snapshot), []);
});
test('baseline: sequential plans (01, 02, 03) produce no diagnostics', (t) => {
const cwd = createTempDir('gsd-3310-c002-neg-');
t.after(() => cleanup(cwd));
writePlan(cwd, '.planning/phases/01-foo', '01-01-PLAN.md');
writePlan(cwd, '.planning/phases/01-foo', '01-02-PLAN.md');
writePlan(cwd, '.planning/phases/01-foo', '01-03-PLAN.md');
const snapshot = buildPlanningSnapshot(cwd);
assert.deepEqual(ruleFor('C002').check(snapshot), []);
});
});
// ─── C003 — orphan SUMMARY with no matching live PLAN ──────────────────────
// Reuses phase-structure.test.cjs's I001 fixture family, inverted.
describe('C003 — orphan SUMMARY with no matching live PLAN', () => {
test('REUSED (I001 family, inverted): a SUMMARY.md with no matching PLAN.md fires C003', (t) => {
const cwd = createTempDir('gsd-3310-c003-');
t.after(() => cleanup(cwd));
writeFile(cwd, '.planning/phases/01-foo/01-01-SUMMARY.md', '# Summary\n');
const snapshot = buildPlanningSnapshot(cwd);
const diagnostics = ruleFor('C003').check(snapshot);
assert.equal(diagnostics.length, 1);
assert.deepEqual(diagnostics[0], {
code: 'C003',
severity: SEVERITY.WARNING,
message: 'Summary 01-01-SUMMARY.md in 01-foo has no matching PLAN.md',
remedy: {
action: REMEDY_ACTION.ADVISE,
risk: REMEDY_RISK.NONE,
args: { command: 'Create the matching PLAN.md or remove the orphan summary' },
},
});
});
test('boundary: a summary paired only to a superseded plan is still an orphan', (t) => {
const cwd = createTempDir('gsd-3310-c003-superseded-');
t.after(() => cleanup(cwd));
writePlan(cwd, '.planning/phases/01-foo', '01-01-PLAN.md', ['status: superseded']);
writeFile(cwd, '.planning/phases/01-foo/01-01-SUMMARY.md', '# Summary\n');
const snapshot = buildPlanningSnapshot(cwd);
const diagnostics = ruleFor('C003').check(snapshot);
assert.equal(diagnostics.length, 1);
assert.equal(diagnostics[0].code, 'C003', 'a summary paired only to a superseded plan must still fire C003');
});
test('baseline: a paired plan+summary produces no diagnostics', (t) => {
const cwd = createTempDir('gsd-3310-c003-neg-');
t.after(() => cleanup(cwd));
writePlan(cwd, '.planning/phases/01-foo', '01-01-PLAN.md');
writeFile(cwd, '.planning/phases/01-foo/01-01-SUMMARY.md', '# Summary\n');
const snapshot = buildPlanningSnapshot(cwd);
assert.deepEqual(ruleFor('C003').check(snapshot), []);
});
});
// ─── C004 — PLAN missing 'wave' in frontmatter ─────────────────────────────
describe("C004 — PLAN missing 'wave' in frontmatter", () => {
test('MECHANICAL MUTATION: a shipped PLAN template with wave: stripped fires C004', (t) => {
const cwd = createTempDir('gsd-3310-c004-');
t.after(() => cleanup(cwd));
writePlan(cwd, '.planning/phases/01-foo', '01-01-PLAN.md');
const snapshot = buildPlanningSnapshot(cwd);
const diagnostics = ruleFor('C004').check(snapshot);
assert.equal(diagnostics.length, 1);
assert.deepEqual(diagnostics[0], {
code: 'C004',
severity: SEVERITY.WARNING,
message: "01-foo/01-01-PLAN.md: missing 'wave' in frontmatter",
remedy: {
action: REMEDY_ACTION.ADVISE,
risk: REMEDY_RISK.NONE,
args: { command: "Add a 'wave' key to the plan's frontmatter" },
},
});
});
test('baseline: a plan with wave: in frontmatter produces no diagnostics', (t) => {
const cwd = createTempDir('gsd-3310-c004-neg-');
t.after(() => cleanup(cwd));
writePlan(cwd, '.planning/phases/01-foo', '01-01-PLAN.md', ['wave: 1']);
const snapshot = buildPlanningSnapshot(cwd);
assert.deepEqual(ruleFor('C004').check(snapshot), []);
});
test('a superseded plan with no wave is not flagged (superseded plans are excluded from the live set)', (t) => {
const cwd = createTempDir('gsd-3310-c004-superseded-');
t.after(() => cleanup(cwd));
writePlan(cwd, '.planning/phases/01-foo', '01-01-PLAN.md', ['status: superseded']);
const snapshot = buildPlanningSnapshot(cwd);
assert.deepEqual(ruleFor('C004').check(snapshot), []);
});
});
// ─── CONSISTENCY_RULES composition (health-diagnostic.cts) ────────────────
describe('CONSISTENCY_RULES composition (matrix row 9)', () => {
test('exactly W006, W007, C001-C004 — 6 entries, no duplicates', () => {
const codes = CONSISTENCY_RULES.map((r) => r.code).sort();
assert.deepEqual(codes, ['C001', 'C002', 'C003', 'C004', 'W006', 'W007']);
assert.equal(new Set(codes).size, 6);
});
});
describe('evaluateConsistencyRules', () => {
test('matrix row 10: an all-clean snapshot produces zero diagnostics', (t) => {
const cwd = createTempDir('gsd-3310-ecr-clean-');
t.after(() => cleanup(cwd));
writeRoadmap(cwd, ['## v1.0 Current 🚧', '', '### Phase 1: Foo'].join('\n'));
writePlan(cwd, '.planning/phases/01-foo', '01-01-PLAN.md', ['wave: 1']);
writeFile(cwd, '.planning/phases/01-foo/01-01-SUMMARY.md', '# Summary\n');
const snapshot = buildPlanningSnapshot(cwd);
assert.deepEqual(evaluateConsistencyRules(snapshot), []);
});
test('matrix row 11 (W006 reuse proof): evaluateConsistencyRules produces the IDENTICAL Diagnostic evaluateRules would for the same W006 fixture', (t) => {
const cwd = createTempDir('gsd-3310-ecr-w006-');
t.after(() => cleanup(cwd));
writeRoadmap(
cwd,
['## v1.0 Current 🚧', '', '### Phase 1: Foo', '', '### Phase 2: Bar'].join('\n'),
);
makePhaseDir(cwd, '01-foo');
// Phase 2 deliberately has no matching directory -> W006.
const snapshot = buildPlanningSnapshot(cwd);
const fromConsistency = evaluateConsistencyRules(snapshot).filter((d) => d.code === 'W006');
const fromHealth = evaluateRules(snapshot).filter((d) => d.code === 'W006');
assert.equal(fromConsistency.length, 1);
assert.equal(fromHealth.length, 1);
assert.deepEqual(fromConsistency[0], fromHealth[0]);
assert.deepEqual(
JSON.parse(JSON.stringify(fromConsistency)),
JSON.parse(JSON.stringify(fromHealth)),
'evaluateConsistencyRules must produce a byte-identical Diagnostic to evaluateRules for the same fixture — proof of reuse, not reimplementation',
);
});
});