Files
msd-core/tests/state-contract.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

449 lines
21 KiB
JavaScript

'use strict';
/**
* Integration tests for `src/state-contract.cts` — the v1
* `.planning/state.json` best-effort publisher (#3227).
*
* This file owns the CLI / call-site wiring band only: it spawns the real
* `msd-tools` binary via `runMsdTools` for each of the 11 documented
* step-boundary commands (plus the non-boundary / no-planning / idempotent /
* error-path guard cases) and asserts state.json actually lands on disk
* end-to-end through the real dispatch path. The spawn-free, in-process
* parsing/mapping/degradation/property-based surface — everything that does
* NOT need a child process — lives in the `.unit.` sibling
* `tests/state-contract.unit.test.cjs`, which is also the Stryker mutation
* shard target (`scripts/mutation-matrix.cjs`, `state-contract` entry):
* Stryker's command runner treats one `node --test <file>` invocation as a
* single test costing whatever its slowest case costs, re-run once per
* mutant, so a suite that spawns a child process per case cannot finish
* inside the 15-minute shard cap (#2790 precedent).
*
* Design: .msd/phase/feat-3227-state-contract/40-design.md
* Test matrix: .msd/phase/feat-3227-state-contract/50-test-matrix.md
*
* Fixture provenance (CONTRIBUTING.md "Fixture provenance (#2371)"): every
* `.planning/` document shape written by this file's fixture builders is
* derived from the SHIPPED templates the product author wrote —
* `msd-core/templates/roadmap.md` (`## Phases` checkbox bullets,
* `### Phase N: Name` details with `Plans:` lists, the 4-column and
* milestone-grouped 5-column `## Progress` tables, and the
* `Not started | In progress | Complete | Deferred` status vocabulary) and
* `msd-core/templates/state.md` (`## Current Position`, `Phase: X of Y
* (Name)`) — never from `state-contract.cts`'s own parsing model.
*
* This module is a NEW leaf; the fixture builders below are local to this
* file rather than reused from `tests/planning-inspect.test.cjs` (a sibling
* document-shape consumer) because the shapes this suite needs diverge
* enough from that file's `## Phase Details` + `Plans:` fixtures that
* sharing would couple two independent test suites to one mutable helper.
*
* These same fixture helpers are also byte-duplicated (not shared) into the
* `.unit.` sibling above, deliberately: that file is the Stryker mutation
* shard target and must stay spawn-free and self-contained, so a `require`
* of this integration file would drag `runMsdTools` and its subprocess seam
* into the shard. The duplication is isolation, not drift.
*/
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const { createTempProject, createTempDir, cleanup, runMsdTools } = require('./helpers.cjs');
const { STATE_CONTRACT_VERSION } = require('../msd-core/bin/lib/state-contract.cjs');
// ─── Fixture helpers ──────────────────────────────────────────────────────────
function planningDirOf(cwd) {
return path.join(cwd, '.planning');
}
function statePathOf(cwd) {
return path.join(planningDirOf(cwd), 'state.json');
}
function writeAbs(fullPath, content) {
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
fs.writeFileSync(fullPath, content);
}
function writeFile(cwd, relPath, content) {
writeAbs(path.join(cwd, relPath), content);
}
function writeRoadmapRaw(cwd, content) {
writeFile(cwd, '.planning/ROADMAP.md', content);
}
function writeRoadmap(cwd, lines, eol = '\n') {
writeRoadmapRaw(cwd, lines.join(eol));
}
function writeState(cwd, frontmatterLines, bodyLines = [], eol = '\n') {
writeFile(cwd, '.planning/STATE.md', ['---', ...frontmatterLines, '---', '', ...bodyLines].join(eol));
}
function readStateJsonRaw(cwd) {
return fs.readFileSync(statePathOf(cwd), 'utf8');
}
// ─── 14. Call-site wiring (integration, real CLI) ──────────────────────────────
function writePassedVerification(tmpDir, phaseDirName, phaseToken) {
const phaseDir = path.join(tmpDir, '.planning', 'phases', phaseDirName);
fs.mkdirSync(phaseDir, { recursive: true });
fs.writeFileSync(
path.join(phaseDir, `${phaseToken}-VERIFICATION.md`),
['---', 'status: passed', '---', '', '# Verification', ''].join('\n'),
);
return phaseDir;
}
const BOUNDARY_COMMANDS = [
{
label: 'state begin-phase',
argv: ['state', 'begin-phase', '--phase', '2', '--name', 'Hardening', '--plans', '2'],
setup: (tmpDir) => {
// Body-only STATE.md (no frontmatter block) — begin-phase reads its
// preconditions from the `**Current Phase:**`-style body fields.
fs.writeFileSync(path.join(planningDirOf(tmpDir), 'STATE.md'), [
'# Project State', '',
'**Current Phase:** 2',
'**Current Phase Name:** Hardening',
'**Total Phases:** 3',
'**Current Plan:** 0',
'**Total Plans in Phase:** 0',
'**Status:** Ready to plan',
'**Last Activity:** 2025-01-01',
'**Last Activity Description:** setup',
'',
].join('\n'));
},
},
{
label: 'state planned-phase',
argv: ['state', 'planned-phase', '--phase', '3', '--name', 'Polish', '--plans', '1'],
setup: (tmpDir) => {
fs.writeFileSync(path.join(planningDirOf(tmpDir), 'STATE.md'),
'# Project State\n\n**Status:** Planning\n**Total Plans in Phase:** 0\n**Last Activity:** 2024-01-01\n**Current Phase:** 3\n');
},
},
{
label: 'state advance-plan',
argv: ['state', 'advance-plan'],
setup: (tmpDir) => {
fs.writeFileSync(path.join(planningDirOf(tmpDir), 'STATE.md'), [
'# Project State', '',
'**Current Plan:** 1',
'**Total Plans in Phase:** 3',
'**Status:** Executing',
'**Last Activity:** 2024-01-10',
'',
].join('\n'));
},
},
{
label: 'state complete-phase',
argv: ['state', 'complete-phase', '--phase', '2'],
setup: (tmpDir) => {
writeState(tmpDir, ['milestone: v1.0', 'current_phase: 2'], [
'# State', '',
'**Status:** Executing',
'**Last Activity:** 2024-01-15',
'',
]);
},
},
{
label: 'state milestone-switch',
argv: ['state', 'milestone-switch', '--milestone', 'v2.0', '--name', 'Next'],
setup: (tmpDir) => {
writeState(tmpDir, [
"msd_state_version: '1.0'", 'milestone: v1.0', 'milestone_name: Foundation', 'status: completed',
], [
'# Project State', '', '## Current Position', '',
'Phase: 5 (Foundation) — COMPLETED', 'Plan: 3 of 3',
'Status: v1.0 milestone complete', 'Last activity: 2025-01-01 -- v1.0 shipped', '',
]);
writeRoadmap(tmpDir, ['# Roadmap', '', '## v1.0 Foundation', '', '### Phase 5: Notify', '']);
fs.writeFileSync(path.join(planningDirOf(tmpDir), 'config.json'), '{}');
},
},
{
label: 'phase add',
argv: ['phase', 'add', 'A new phase'],
setup: (tmpDir) => {
writeRoadmap(tmpDir, ['# Roadmap v1.0', '', '### Phase 1: Foundation', '**Goal:** Setup', '', '---', '']);
},
},
{
label: 'phase add-batch',
argv: ['phase', 'add-batch', '--descriptions', '["One","Two"]'],
setup: (tmpDir) => {
writeRoadmap(tmpDir, ['# Roadmap v1.0', '', '### Phase 1: Foundation', '**Goal:** Setup', '', '---', '']);
},
},
{
label: 'phase insert',
argv: ['phase', 'insert', '2', 'An inserted phase'],
setup: (tmpDir) => {
writeRoadmap(tmpDir, [
'# Roadmap', '',
'### Phase 1: Foundation', '**Goal:** Setup', '**Depends on:** Nothing', '',
'### Phase 2: Auth', '**Goal:** Authentication', '**Depends on:** Phase 1', '',
]);
},
},
{
label: 'phase remove',
argv: ['phase', 'remove', '3', '--force'],
setup: (tmpDir) => {
writeRoadmap(tmpDir, [
'# Roadmap', '',
'### Phase 1: Foundation', '**Goal:** Setup', '**Depends on:** Nothing', '',
'### Phase 2: Auth', '**Goal:** Authentication', '**Depends on:** Phase 1', '',
'### Phase 3: Features', '**Goal:** Core features', '**Depends on:** Phase 2', '',
]);
fs.mkdirSync(path.join(planningDirOf(tmpDir), 'phases', '01-foundation'), { recursive: true });
fs.mkdirSync(path.join(planningDirOf(tmpDir), 'phases', '02-auth'), { recursive: true });
fs.mkdirSync(path.join(planningDirOf(tmpDir), 'phases', '03-features'), { recursive: true });
},
},
{
label: 'phase complete',
argv: ['phase', 'complete', '1'],
setup: (tmpDir) => {
writeRoadmap(tmpDir, [
'# Roadmap', '',
'- [ ] Phase 1: Foundation', '- [ ] Phase 2: API', '',
'### Phase 1: Foundation', '**Goal:** Setup', '**Plans:** 1 plans', '',
'### Phase 2: API', '**Goal:** Build API', '',
]);
fs.writeFileSync(path.join(planningDirOf(tmpDir), 'STATE.md'),
'# State\n\n**Current Phase:** 01\n**Current Phase Name:** Foundation\n**Status:** In progress\n**Current Plan:** 01-01\n**Last Activity:** 2025-01-01\n**Last Activity Description:** Working on phase 1\n');
const p1 = path.join(planningDirOf(tmpDir), 'phases', '01-foundation');
fs.mkdirSync(p1, { recursive: true });
fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan');
fs.writeFileSync(path.join(p1, '01-01-SUMMARY.md'), '# Summary');
fs.mkdirSync(path.join(planningDirOf(tmpDir), 'phases', '02-api'), { recursive: true });
writePassedVerification(tmpDir, '01-foundation', '01');
},
},
{
label: 'milestone complete',
// --confirm is the mutation opt-in (#3726) — without it the command
// refuses before touching anything, and this fixture must genuinely
// succeed. --force alone does not imply it.
argv: ['milestone', 'complete', 'v1.1', '--force', '--confirm'],
setup: (tmpDir) => {
// --force bypasses both the TRUNCATED-scope guard and the
// unstarted-phase (no_directory) guard, so this fixture only needs a
// resolvable v1.1 milestone window with at least one phase entry —
// the one precondition `getMilestonePhaseFilter`'s
// `missingExplicitVersion` check enforces unconditionally, before
// --force is ever consulted.
writeRoadmap(tmpDir, ['# Roadmap', '', '## v1.1 Hardening', '', '### Phase 1: Foo', '**Goal:** Ship it', '']);
fs.writeFileSync(path.join(planningDirOf(tmpDir), 'STATE.md'),
'# Project State\n\n**Status:** Executing\n**Current Phase:** 1\n**Last Activity:** 2025-01-01\n');
},
},
];
describe('state contract — call-site wiring (integration, real CLI)', () => {
test('eachBoundaryCommandPublishesTheSnapshot', () => {
for (const { label, argv, setup } of BOUNDARY_COMMANDS) {
const tmpDir = createTempProject();
setup(tmpDir);
const result = runMsdTools(argv, tmpDir);
assert.strictEqual(result.success, true, `${label} fixture must genuinely succeed: ${result.error}`);
const jsonPath = statePathOf(tmpDir);
assert.strictEqual(fs.existsSync(jsonPath), true, `${label} must publish state.json`);
const onDisk = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
assert.strictEqual(onDisk.contract, STATE_CONTRACT_VERSION, `${label}: state.json contract must equal STATE_CONTRACT_VERSION`);
cleanup(tmpDir);
}
});
test('nonBoundaryCommandDoesNotPublish', (t) => {
const tmpDir = createTempProject();
t.after(() => cleanup(tmpDir));
fs.writeFileSync(path.join(planningDirOf(tmpDir), 'STATE.md'), '# State\n\n**Status:** Planning\n');
runMsdTools(['state', 'get', 'status'], tmpDir);
assert.strictEqual(fs.existsSync(statePathOf(tmpDir)), false);
});
test('boundaryCommandUnaffectedWithoutPlanning', (t) => {
const dirA = createTempDir();
t.after(() => cleanup(dirA));
const dirB = createTempDir();
t.after(() => cleanup(dirB));
const argv = ['state', 'begin-phase', '--phase', '1', '--name', 'X', '--plans', '1'];
const resultA = runMsdTools(argv, dirA);
const resultB = runMsdTools(argv, dirB);
assert.strictEqual(resultA.success, resultB.success);
assert.strictEqual(resultA.output, resultB.output);
});
test('idempotentNoOpDoesNotRepublish', (t) => {
const tmpDir = createTempProject();
t.after(() => cleanup(tmpDir));
fs.writeFileSync(path.join(planningDirOf(tmpDir), 'STATE.md'),
'# Project State\n\n**Status:** Executing\n**Current Phase:** 1\n**Last Activity:** 2024-01-01\n');
const first = runMsdTools(['state', 'complete-phase', '--phase', '1'], tmpDir);
assert.ok(first.success, `first complete-phase call failed: ${first.error}`);
assert.ok(fs.existsSync(statePathOf(tmpDir)), 'first (genuine) completion must publish');
const beforeSecondCall = readStateJsonRaw(tmpDir);
const second = runMsdTools(['state', 'complete-phase', '--phase', '1'], tmpDir);
assert.ok(second.success, `second (idempotent) complete-phase call failed: ${second.error}`);
const afterSecondCall = readStateJsonRaw(tmpDir);
assert.strictEqual(afterSecondCall, beforeSecondCall, 'idempotent no-op must not republish (updated_at must not move)');
});
test('errorPathDoesNotPublish', (t) => {
const tmpDir = createTempProject();
t.after(() => cleanup(tmpDir));
// No STATE.md at all — complete-phase's own precondition check fails.
const result = runMsdTools(['state', 'complete-phase', '--phase', '3'], tmpDir);
assert.ok(result.success, `command should exit 0 with a JSON error envelope: ${result.error}`);
const output = JSON.parse(result.output);
assert.ok(output.error, 'expected a structured error envelope');
assert.strictEqual(fs.existsSync(statePathOf(tmpDir)), false);
});
// #3227 blocker fix: `updated: []` genuine no-op transitions must not
// publish state.json — a refreshed `updated_at` must always mean something
// on disk actually moved (design doc §40 row 26). Each case below is a
// reproducer confirmed (via direct CLI probing) to reach
// `publishStateContract` on the unfixed code with nothing genuinely written.
test('plannedPhaseNoOpDoesNotPublish', (t) => {
const tmpDir = createTempProject();
t.after(() => cleanup(tmpDir));
writeRoadmap(tmpDir, [
'# Roadmap', '',
'## Progress', '',
'| Phase | Plans Complete | Status | Completed |',
'|---|---|---|---|',
'| 1. A | 0/1 | Not started | - |',
'',
]);
// ADR-3473 §8.7 (#3872) / test-matrix row 17 (D15): `msd_state_version`
// is pre-seeded here so this fixture isolates the ONE thing it is meant
// to prove (zero recognized Current Position labels is a genuine no-op)
// from an UNRELATED, already-correct §8.7 behavior — a frontmatter block
// synthesizing `msd_state_version` for the first time on a document that
// never had one IS a real, reportable addition (row 17/D15, pinned
// separately by "state patch accepts JSON object input from workflows"
// above). Omitting it here would make THIS no-op test fail for a reason
// that has nothing to do with recognized labels.
writeState(tmpDir, ['msd_state_version: 1.0', 'status: planning'], [
'# Project State', '',
'(no recognized labels here)',
]);
const result = runMsdTools(['state', 'planned-phase', '--phase', '3', '--name', 'Polish', '--plans', '1'], tmpDir);
assert.ok(result.success, `planned-phase fixture must genuinely succeed: ${result.error}`);
const output = JSON.parse(result.output);
assert.deepStrictEqual(output.updated, [], 'expected a genuine no-op (zero recognized Current Position labels)');
assert.strictEqual(fs.existsSync(statePathOf(tmpDir)), false, 'a no-op transition must not publish state.json');
});
test('beginPhaseNoOpDoesNotPublish', (t) => {
const tmpDir = createTempProject();
t.after(() => cleanup(tmpDir));
writeRoadmap(tmpDir, ['# Roadmap', '']);
fs.writeFileSync(path.join(planningDirOf(tmpDir), 'STATE.md'), [
'# Project State', '',
'(no recognized labels here)',
'',
].join('\n'));
const result = runMsdTools(['state', 'begin-phase', '--phase', '3', '--name', 'Polish', '--plans', '1'], tmpDir);
assert.ok(result.success, `begin-phase fixture must genuinely succeed: ${result.error}`);
const output = JSON.parse(result.output);
assert.deepStrictEqual(output.updated, [], 'expected a genuine no-op (zero recognized body fields)');
assert.strictEqual(fs.existsSync(statePathOf(tmpDir)), false, 'a no-op transition must not publish state.json');
});
// Additional no-op path found while auditing `state advance-plan`
// (cmdStateAdvancePlan, src/state.cts): re-invoking advance-plan while
// already parked at "last plan, ready for verification" reproduces
// byte-identical STATE.md content on the SECOND call. MOVED under
// ADR-3473 §8.7 (#3872): before that change, `reconcileReportedFields`
// compared the transform's OWN reported fields against the FINAL
// persisted bytes, so `Status`/`Last Activity` (values the transform
// always names, which trivially still matched the byte-identical file)
// came back non-empty even though nothing was written. §8.7 replaced
// that with a diff against the transaction's PRE-WRITE snapshot, so a
// genuinely byte-identical second call now correctly reconciles to
// `updated: []` — a true no-op is reported as a true no-op. Publishing
// still does not fire on this call: `cmdStateAdvancePlan` gates on
// `readModifyWriteStateMd`'s own write-happened return value, never on
// `updated.length`, so this remains a no-op-does-not-publish regression
// test — only the expected shape of the (now-correct) `updated` moved.
test('advancePlanNoOpDoesNotPublish', (t) => {
const tmpDir = createTempProject();
t.after(() => cleanup(tmpDir));
writeRoadmap(tmpDir, ['# Roadmap', '']);
writeState(tmpDir, ['status: planning'], [
'# Project State', '',
'Current Plan: 2', 'Total Plans in Phase: 2', 'Status: Ready to execute', 'Last Activity: 2020-01-01',
'',
'## Current Position',
'Phase: 5', 'Plan: 2 of 2', 'Status: Ready to execute',
'',
]);
const first = runMsdTools(['state', 'advance-plan'], tmpDir);
assert.ok(first.success, `first advance-plan call must genuinely succeed: ${first.error}`);
assert.strictEqual(fs.existsSync(statePathOf(tmpDir)), true, 'first (genuine) advance must publish');
fs.unlinkSync(statePathOf(tmpDir));
const second = runMsdTools(['state', 'advance-plan'], tmpDir);
assert.ok(second.success, `second (no-op) advance-plan call must genuinely succeed: ${second.error}`);
const output = JSON.parse(second.output);
assert.deepStrictEqual(output.updated, [], 'ADR-3473 §8.7: a byte-identical second call is a genuine no-op — `updated` must reconcile to empty, not merely reflect fields the transform always names');
assert.strictEqual(fs.existsSync(statePathOf(tmpDir)), false, 'the second, no-op advance-plan call must not publish state.json');
});
// Additional no-op path found while auditing `phase complete`
// (cmdPhaseComplete, src/phase.cts): with no STATE.md present, a re-run of
// `phase complete <N>` against an already-completed phase produces a
// `writes[]` whose only entry (ROADMAP.md) is byte-identical to what's
// already on disk — `writePlanningFileSet` applies zero writes on the
// second call, so the fix gates on that applied count rather than
// publishing unconditionally once the verification-gated transaction runs.
test('phaseCompleteReRunWithoutStateNoOpDoesNotPublish', (t) => {
const tmpDir = createTempProject();
t.after(() => cleanup(tmpDir));
writeRoadmap(tmpDir, [
'# Roadmap', '',
'### Phase 1: Foo', '',
'**Goal:** g', '**Plans:** 1 plans', '',
'Plans:', '- [ ] 01 TBD', '',
'## Progress', '',
'| Phase | Plans Complete | Status | Completed |',
'|---|---|---|---|',
'| 1 | 0/1 | Not started | - |',
'',
]);
if (fs.existsSync(path.join(planningDirOf(tmpDir), 'STATE.md'))) fs.unlinkSync(path.join(planningDirOf(tmpDir), 'STATE.md'));
const phaseDir = path.join(planningDirOf(tmpDir), 'phases', '01-foo');
fs.mkdirSync(phaseDir, { recursive: true });
fs.writeFileSync(path.join(phaseDir, '01-PLAN.md'), '# Plan');
fs.writeFileSync(path.join(phaseDir, '01-SUMMARY.md'), '# Summary');
writePassedVerification(tmpDir, '01-foo', '01');
const first = runMsdTools(['phase', 'complete', '1'], tmpDir);
assert.ok(first.success, `first phase complete call must genuinely succeed: ${first.error}`);
assert.strictEqual(fs.existsSync(statePathOf(tmpDir)), true, 'first (genuine) completion must publish');
fs.unlinkSync(statePathOf(tmpDir));
const second = runMsdTools(['phase', 'complete', '1'], tmpDir);
assert.ok(second.success, `second (no-op) phase complete call must genuinely succeed: ${second.error}`);
assert.strictEqual(fs.existsSync(statePathOf(tmpDir)), false, 'the second, no-op phase complete call must not publish state.json');
});
});