Files
msd-core/tests/gen-section-manifest.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

781 lines
32 KiB
JavaScript

'use strict';
/**
* gen-section-manifest.cjs drift-guard tests — 50-test-matrix.md rows 29-41
* (issue #2932, epic #1671 Phase 5,
* `.msd/phase/chore-2932-init-section-manifest/50-test-matrix.md` section D).
*
* Every test spawns the real CLI (execFileSync) against a temp fixture tree
* shaped like the real repo (`<root>/msd-core/workflows/<name>.md` +
* `<root>/msd-core/workflows/<name>/steps/<id>.md`), using the generator's
* `--workflows-dir`/`--manifest-path`/`--repo-root` overrides — no fs
* monkeypatching except rows 40/41, which inject the exact fault
* `writeManifestAtomically` cannot otherwise be made to hit (CONTRIBUTING.md
* / CLAUDE.md cross-platform IO-failure rule: monkeypatch the `fs` method,
* restore via `t.after()`, never `chmod 0o000`).
*/
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const { execFileSync } = require('node:child_process');
const { createTempDir, cleanup } = require('./helpers.cjs');
const { GENERATOR_SCRIPT_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
const {
buildFreshManifest,
writeManifestAtomically,
loadWorkflowFragmentsLib,
checkReport,
ManifestBuildError,
REASON,
WORKFLOW_FRAGMENTS_LIB_PATH,
} = require('../scripts/gen-section-manifest.cjs');
const ROOT = path.resolve(__dirname, '..');
const SCRIPT = path.join(ROOT, 'scripts', 'gen-section-manifest.cjs');
const STACK_FRAME_RE = /\n\s+at\s+\S+\s+\(.*:\d+:\d+\)/;
// ─── Fixture builder ───────────────────────────────────────────────────────
/**
* Build a `<tmpRoot>/msd-core/workflows/` tree containing one workflow file
* plus (optionally) its `steps/` directory, matching the real repo's
* relative shape exactly so `--repo-root <tmpRoot>` produces the same
* `msd-core/workflows/...` POSIX `read` paths the real generator emits.
*
* @param {string} tmpRoot
* @param {string} workflowName - e.g. "sample" -> msd-core/workflows/sample.md
* @param {string} sourceContent
* @param {{[stepFileName: string]: string}} [stepFiles]
* @returns {{ workflowsDir: string, manifestPath: string }}
*/
function buildFixture(tmpRoot, workflowName, sourceContent, stepFiles = {}) {
const workflowsDir = path.join(tmpRoot, 'msd-core', 'workflows');
fs.mkdirSync(workflowsDir, { recursive: true });
fs.writeFileSync(path.join(workflowsDir, `${workflowName}.md`), sourceContent, 'utf8');
if (Object.keys(stepFiles).length > 0) {
const stepsDir = path.join(workflowsDir, workflowName, 'steps');
fs.mkdirSync(stepsDir, { recursive: true });
for (const [name, content] of Object.entries(stepFiles)) {
fs.writeFileSync(path.join(stepsDir, name), content, 'utf8');
}
}
return { workflowsDir, manifestPath: path.join(workflowsDir, 'section-manifest.json') };
}
/** A single well-formed marker + matching step-file body. */
function markerSource(id, when, refFile) {
return [
'# Sample workflow',
'',
`<!-- msd:section id="${id}" when="${when}" -->`,
`Read and execute \`msd-core/workflows/sample/steps/${refFile}\`.`,
'<!-- /msd:section -->',
'',
].join('\n');
}
/**
* @param {string[]} args
* @param {string} cwd
* @returns {{code: number, stdout: string, stderr: string}}
*/
function runGenSectionManifest(args, cwd = ROOT) {
try {
const stdout = execFileSync(process.execPath, [SCRIPT, ...args], {
cwd,
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
timeout: GENERATOR_SCRIPT_TIMEOUT_MS,
});
return { code: 0, stdout, stderr: '' };
} catch (err) {
return {
code: err.status ?? 1,
stdout: err.stdout ? err.stdout.toString() : '',
stderr: err.stderr ? err.stderr.toString() : '',
};
}
}
function parseJsonReport(stdout) {
return JSON.parse(stdout.trim());
}
// ─── D. Generator drift-guard (rows 29-41) ─────────────────────────────────
describe('gen-section-manifest.cjs --check / --write (matrix D)', () => {
test('checkExitsZeroWhenManifestMatchesSource (row 29)', (t) => {
const tmpRoot = createTempDir('gen-section-manifest-');
t.after(() => cleanup(tmpRoot));
const { workflowsDir, manifestPath } = buildFixture(
tmpRoot,
'sample',
markerSource('handle-x', 'always', 'handle-x.md'),
{ 'handle-x.md': '<step name="handle_x">\nbody\n</step>\n' },
);
const fresh = buildFreshManifest(workflowsDir, tmpRoot);
fs.writeFileSync(manifestPath, JSON.stringify(fresh, null, 2) + '\n', 'utf8');
const r = runGenSectionManifest([
'--check', '--workflows-dir', workflowsDir, '--manifest-path', manifestPath, '--repo-root', tmpRoot,
]);
assert.equal(r.code, 0, `stderr: ${r.stderr}`);
});
test('checkExitsOneAndNamesFileWhenManifestIsStale (row 30)', (t) => {
const tmpRoot = createTempDir('gen-section-manifest-');
t.after(() => cleanup(tmpRoot));
const { workflowsDir, manifestPath } = buildFixture(
tmpRoot,
'sample',
markerSource('handle-x', 'always', 'handle-x.md'),
{ 'handle-x.md': '<step name="handle_x">\nbody\n</step>\n' },
);
// Stale: valid {workflows:{...}} shape, but "when" no longer matches the
// source's marker (must reach FAIL_STALE, not trip the shape check).
fs.writeFileSync(
manifestPath,
JSON.stringify({ workflows: { sample: [{ id: 'handle-x', when: 'flag:--wave', read: 'msd-core/workflows/sample/steps/handle-x.md' }] } }, null, 2) + '\n',
'utf8',
);
const r = runGenSectionManifest([
'--check', '--json', '--workflows-dir', workflowsDir, '--manifest-path', manifestPath, '--repo-root', tmpRoot,
]);
assert.equal(r.code, 1);
assert.doesNotMatch(r.stderr, STACK_FRAME_RE, 'no bare stack trace for a stale manifest');
const report = parseJsonReport(r.stdout);
assert.equal(report.ok, false);
assert.equal(report.reason, REASON.FAIL_STALE);
assert.equal(report.subject, manifestPath, 'report must name the stale manifest file');
});
test('writeThenCheckRoundTripsClean (row 31)', (t) => {
const tmpRoot = createTempDir('gen-section-manifest-');
t.after(() => cleanup(tmpRoot));
const { workflowsDir, manifestPath } = buildFixture(
tmpRoot,
'sample',
markerSource('handle-x', 'always', 'handle-x.md'),
{ 'handle-x.md': '<step name="handle_x">\nbody\n</step>\n' },
);
const w = runGenSectionManifest([
'--write', '--workflows-dir', workflowsDir, '--manifest-path', manifestPath, '--repo-root', tmpRoot,
]);
assert.equal(w.code, 0, `stderr: ${w.stderr}`);
assert.ok(fs.existsSync(manifestPath));
const c = runGenSectionManifest([
'--check', '--workflows-dir', workflowsDir, '--manifest-path', manifestPath, '--repo-root', tmpRoot,
]);
assert.equal(c.code, 0, '--check must be clean immediately after --write');
});
test('checkFailsCleanlyWhenManifestAbsent (row 32)', (t) => {
const tmpRoot = createTempDir('gen-section-manifest-');
t.after(() => cleanup(tmpRoot));
const { workflowsDir, manifestPath } = buildFixture(
tmpRoot,
'sample',
markerSource('handle-x', 'always', 'handle-x.md'),
{ 'handle-x.md': '<step name="handle_x">\nbody\n</step>\n' },
);
assert.equal(fs.existsSync(manifestPath), false, 'sanity: manifest must not exist yet');
const r = runGenSectionManifest([
'--check', '--json', '--workflows-dir', workflowsDir, '--manifest-path', manifestPath, '--repo-root', tmpRoot,
]);
assert.equal(r.code, 1);
assert.doesNotMatch(r.stderr, STACK_FRAME_RE, 'an absent manifest must not crash, just fail closed');
const report = parseJsonReport(r.stdout);
assert.equal(report.ok, false);
assert.equal(report.reason, REASON.FAIL_MANIFEST_MISSING);
assert.equal(report.subject, manifestPath);
});
test('checkFailsCleanlyOnEmptyManifestFile (row 33)', (t) => {
const tmpRoot = createTempDir('gen-section-manifest-');
t.after(() => cleanup(tmpRoot));
const { workflowsDir, manifestPath } = buildFixture(
tmpRoot,
'sample',
markerSource('handle-x', 'always', 'handle-x.md'),
{ 'handle-x.md': '<step name="handle_x">\nbody\n</step>\n' },
);
fs.writeFileSync(manifestPath, '', 'utf8');
const r = runGenSectionManifest([
'--check', '--json', '--workflows-dir', workflowsDir, '--manifest-path', manifestPath, '--repo-root', tmpRoot,
]);
assert.equal(r.code, 1);
assert.doesNotMatch(r.stderr, STACK_FRAME_RE, 'no stack trace for an empty manifest file');
const report = parseJsonReport(r.stdout);
assert.equal(report.ok, false);
assert.equal(report.reason, REASON.FAIL_MANIFEST_UNPARSEABLE);
});
test('rejectsValidJsonThatIsNotTheExpectedShape (row 34)', (t) => {
const tmpRoot = createTempDir('gen-section-manifest-');
t.after(() => cleanup(tmpRoot));
const { workflowsDir, manifestPath } = buildFixture(
tmpRoot,
'sample',
markerSource('handle-x', 'always', 'handle-x.md'),
{ 'handle-x.md': '<step name="handle_x">\nbody\n</step>\n' },
);
for (const hostileJson of ['0', '"s"', '[]', 'null', 'true']) {
fs.writeFileSync(manifestPath, hostileJson, 'utf8');
const r = runGenSectionManifest([
'--check', '--json', '--workflows-dir', workflowsDir, '--manifest-path', manifestPath, '--repo-root', tmpRoot,
]);
assert.equal(r.code, 1, `hostile JSON ${hostileJson} must fail --check`);
assert.doesNotMatch(r.stderr, STACK_FRAME_RE, `hostile JSON ${hostileJson} must not crash`);
const report = parseJsonReport(r.stdout);
assert.equal(report.ok, false, `hostile JSON ${hostileJson}`);
assert.equal(report.reason, REASON.FAIL_MANIFEST_MALFORMED_SHAPE, `hostile JSON ${hostileJson}`);
}
});
test('rejectsPre6_1FlatSectionsShapeAsMalformed (upgrade-path supplemental: an installed tree still carrying the old flat {sections:[...]} artifact)', (t) => {
const tmpRoot = createTempDir('gen-section-manifest-');
t.after(() => cleanup(tmpRoot));
const { workflowsDir, manifestPath } = buildFixture(
tmpRoot,
'sample',
markerSource('handle-x', 'always', 'handle-x.md'),
{ 'handle-x.md': '<step name="handle_x">\nbody\n</step>\n' },
);
// Pre-6.1 committed artifact shape: a flat `{sections:[...]}` array with
// no `workflows` key at all. Must be rejected as malformed, never
// silently attributed to whichever workflow asks first (design row C4).
fs.writeFileSync(
manifestPath,
JSON.stringify({ sections: [{ id: 'handle-x', when: 'always', read: 'msd-core/workflows/sample/steps/handle-x.md' }] }, null, 2) + '\n',
'utf8',
);
const r = runGenSectionManifest([
'--check', '--json', '--workflows-dir', workflowsDir, '--manifest-path', manifestPath, '--repo-root', tmpRoot,
]);
assert.equal(r.code, 1);
assert.doesNotMatch(r.stderr, STACK_FRAME_RE, 'a pre-6.1 flat manifest must not crash the generator');
const report = parseJsonReport(r.stdout);
assert.equal(report.ok, false);
assert.equal(report.reason, REASON.FAIL_MANIFEST_MALFORMED_SHAPE, 'pre-6.1 flat {sections:[...]} shape must be rejected, not mistaken for up-to-date or stale');
});
test('checkFailsWhenMarkerReferencesMissingStepFile (row 35)', (t) => {
const tmpRoot = createTempDir('gen-section-manifest-');
t.after(() => cleanup(tmpRoot));
// Marker present, but the step file it names is never created.
const { workflowsDir, manifestPath } = buildFixture(
tmpRoot,
'sample',
markerSource('handle-x', 'always', 'handle-x.md'),
{},
);
const r = runGenSectionManifest([
'--check', '--json', '--workflows-dir', workflowsDir, '--manifest-path', manifestPath, '--repo-root', tmpRoot,
]);
assert.equal(r.code, 1);
assert.doesNotMatch(r.stderr, STACK_FRAME_RE, 'a missing step file must not crash the generator');
const report = parseJsonReport(r.stdout);
assert.equal(report.ok, false);
assert.equal(report.reason, REASON.FAIL_MISSING_STEP_FILE);
assert.equal(
report.subject,
'msd-core/workflows/sample/steps/handle-x.md',
'report must name the missing step file path',
);
});
test('checkFailsOnOrphanStepFile (row 36)', (t) => {
const tmpRoot = createTempDir('gen-section-manifest-');
t.after(() => cleanup(tmpRoot));
// handle-x.md is the legitimate marker target; stray.md is referenced by
// nothing (not the marker, not any prose in the parent or in handle-x.md).
const { workflowsDir, manifestPath } = buildFixture(
tmpRoot,
'sample',
markerSource('handle-x', 'always', 'handle-x.md'),
{
'handle-x.md': '<step name="handle_x">\nbody\n</step>\n',
'stray.md': '<step name="unreachable">\nnever referenced anywhere\n</step>\n',
},
);
const r = runGenSectionManifest([
'--check', '--json', '--workflows-dir', workflowsDir, '--manifest-path', manifestPath, '--repo-root', tmpRoot,
]);
assert.equal(r.code, 1);
assert.doesNotMatch(r.stderr, STACK_FRAME_RE, 'an orphan step file must not crash the generator');
const report = parseJsonReport(r.stdout);
assert.equal(report.ok, false);
assert.equal(report.reason, REASON.FAIL_ORPHAN_STEP_FILE);
assert.equal(
report.subject,
'msd-core/workflows/sample/steps/stray.md',
'report must name the orphan step file path',
);
});
test('nestedStepReferenceIsNotFlaggedAsOrphan (supplemental: proves the reachability fixed-point, not just the negative case)', (t) => {
const tmpRoot = createTempDir('gen-section-manifest-');
t.after(() => cleanup(tmpRoot));
// handle-x.md is the marker target; handle-x.md's OWN prose delegates to
// handle-x-run.md, which is referenced by NO marker directly — mirrors
// the real repo's regression-gate.md -> regression-gate-run.md shape.
const { workflowsDir, manifestPath } = buildFixture(
tmpRoot,
'sample',
markerSource('handle-x', 'always', 'handle-x.md'),
{
'handle-x.md': 'Read and execute `msd-core/workflows/sample/steps/handle-x-run.md`.\n',
'handle-x-run.md': 'the nested run body\n',
},
);
const fresh = buildFreshManifest(workflowsDir, tmpRoot);
assert.equal(fresh.workflows.sample.length, 1);
fs.writeFileSync(manifestPath, JSON.stringify(fresh, null, 2) + '\n', 'utf8');
const r = runGenSectionManifest([
'--check', '--workflows-dir', workflowsDir, '--manifest-path', manifestPath, '--repo-root', tmpRoot,
]);
assert.equal(r.code, 0, `handle-x-run.md must resolve as reachable, not orphan; stderr: ${r.stderr}`);
});
test('producesIdenticalManifestForCrlfAndLfSources (row 37)', (t) => {
const lfRoot = createTempDir('gen-section-manifest-lf-');
const crlfRoot = createTempDir('gen-section-manifest-crlf-');
t.after(() => {
cleanup(lfRoot);
cleanup(crlfRoot);
});
const lfSource = markerSource('handle-x', 'state:has-prior-phases', 'handle-x.md');
const lfStep = '<step name="handle_x">\nbody\n</step>\n';
const { workflowsDir: lfDir } = buildFixture(lfRoot, 'sample', lfSource, { 'handle-x.md': lfStep });
const { workflowsDir: crlfDir } = buildFixture(
crlfRoot,
'sample',
lfSource.replace(/\n/g, '\r\n'),
{ 'handle-x.md': lfStep.replace(/\n/g, '\r\n') },
);
const lfManifest = buildFreshManifest(lfDir, lfRoot);
const crlfManifest = buildFreshManifest(crlfDir, crlfRoot);
assert.deepEqual(crlfManifest, lfManifest, 'CRLF and LF sources must produce an identical manifest');
});
test('ignoresSectionShapedLineInsideFencedBlock (row 38)', (t) => {
const tmpRoot = createTempDir('gen-section-manifest-');
t.after(() => cleanup(tmpRoot));
const source = [
'# Sample workflow',
'',
'<!-- msd:section id="handle-x" when="always" -->',
'Read and execute `msd-core/workflows/sample/steps/handle-x.md`.',
'<!-- /msd:section -->',
'',
'Documentation of the marker grammar (must NOT be treated as real):',
'```',
'<!-- msd:section id="fake" when="always" -->',
'never a real section',
'<!-- /msd:section -->',
'```',
'',
].join('\n');
const { workflowsDir, manifestPath } = buildFixture(tmpRoot, 'sample', source, {
'handle-x.md': '<step name="handle_x">\nbody\n</step>\n',
});
const fresh = buildFreshManifest(workflowsDir, tmpRoot);
assert.equal(fresh.workflows.sample.length, 1, 'the fenced marker-shaped lines must not produce a section');
assert.equal(fresh.workflows.sample[0].id, 'handle-x');
fs.writeFileSync(manifestPath, JSON.stringify(fresh, null, 2) + '\n', 'utf8');
const r = runGenSectionManifest([
'--check', '--workflows-dir', workflowsDir, '--manifest-path', manifestPath, '--repo-root', tmpRoot,
]);
assert.equal(r.code, 0, `stderr: ${r.stderr}`);
});
test('leavesLoopHostMarkerUntouched (row 39)', (t) => {
const tmpRoot = createTempDir('gen-section-manifest-');
t.after(() => cleanup(tmpRoot));
const source = [
'<!-- msd:loop-host name="example" -->',
'',
'# Sample workflow',
'',
'<!-- msd:section id="handle-x" when="always" -->',
'Read and execute `msd-core/workflows/sample/steps/handle-x.md`.',
'<!-- /msd:section -->',
'',
].join('\n');
const { workflowsDir, manifestPath } = buildFixture(tmpRoot, 'sample', source, {
'handle-x.md': '<step name="handle_x">\nbody\n</step>\n',
});
const fresh = buildFreshManifest(workflowsDir, tmpRoot);
assert.equal(fresh.workflows.sample.length, 1, 'msd:loop-host must never be treated as a msd:section marker');
assert.equal(fresh.workflows.sample[0].id, 'handle-x');
fs.writeFileSync(manifestPath, JSON.stringify(fresh, null, 2) + '\n', 'utf8');
const r = runGenSectionManifest([
'--check', '--workflows-dir', workflowsDir, '--manifest-path', manifestPath, '--repo-root', tmpRoot,
]);
assert.equal(r.code, 0, `stderr: ${r.stderr}`);
});
test('writesManifestAtomically (row 40)', (t) => {
const tmpRoot = createTempDir('gen-section-manifest-');
t.after(() => cleanup(tmpRoot));
const manifestPath = path.join(tmpRoot, 'section-manifest.json');
const originalContent = '{"sections":[{"id":"pre-existing","when":"always","read":"x.md"}]}\n';
fs.writeFileSync(manifestPath, originalContent, 'utf8');
// Cross-platform IO-failure injection (CLAUDE.md rule): monkeypatch the
// fs method, restore via t.after() (never chmod 0o000 — root bypasses
// mode bits, silently zero-coverage in root Docker/CI).
const origRenameSync = fs.renameSync;
fs.renameSync = function patchedRenameSync() {
throw new Error('injected rename failure (never a real fs fault)');
};
t.after(() => {
fs.renameSync = origRenameSync;
});
assert.throws(() => writeManifestAtomically(manifestPath, '{"sections":[]}\n'), (err) => {
// Finding 2 (#2932 review): the throw site must attach the typed
// FAIL_WRITE_ERROR reason, not just a plain ExitError, so a downstream
// `--write --json` consumer can emit the same {ok,reason,subject}
// envelope every other failure path in this file produces.
assert.ok(err instanceof ManifestBuildError, 'must throw a ManifestBuildError, not a plain ExitError');
assert.equal(err.reason, REASON.FAIL_WRITE_ERROR);
assert.equal(err.subject, manifestPath);
return true;
});
assert.equal(
fs.readFileSync(manifestPath, 'utf8'),
originalContent,
'a rename failure must leave the pre-existing manifest completely untouched, never truncated',
);
const leftoverTmp = fs.readdirSync(tmpRoot).filter((f) => f.includes('.tmp-'));
assert.deepEqual(leftoverTmp, [], 'the temp file must be cleaned up even when the rename step fails');
});
test('surfacesWriteFailureAndCleansTemp (row 41)', (t) => {
const tmpRoot = createTempDir('gen-section-manifest-');
t.after(() => cleanup(tmpRoot));
const manifestPath = path.join(tmpRoot, 'section-manifest.json');
assert.equal(fs.existsSync(manifestPath), false, 'sanity: no pre-existing manifest');
const origWriteFileSync = fs.writeFileSync;
fs.writeFileSync = function patchedWriteFileSync(target, ...rest) {
if (target === manifestPath || (typeof target === 'string' && target.includes('.tmp-'))) {
throw new Error('injected write failure (never a real fs fault)');
}
return origWriteFileSync.call(fs, target, ...rest);
};
t.after(() => {
fs.writeFileSync = origWriteFileSync;
});
assert.throws(() => writeManifestAtomically(manifestPath, '{"sections":[]}\n'), (err) => {
assert.ok(err instanceof ManifestBuildError, 'must throw a ManifestBuildError, not a plain ExitError');
assert.equal(err.reason, REASON.FAIL_WRITE_ERROR);
assert.equal(err.subject, manifestPath);
return true;
});
assert.equal(fs.existsSync(manifestPath), false, 'the target manifest must never be created on a write failure');
const leftover = fs.readdirSync(tmpRoot).filter((f) => f.includes('.tmp-'));
assert.deepEqual(leftover, [], 'no temp file must leak when the initial write itself fails');
});
test('loadWorkflowFragmentsLib throws ManifestBuildError with FAIL_LIB_NOT_BUILT when the compiled lib cannot be read (Finding 2, #2932 review)', (t) => {
// Cross-platform IO-failure injection (CLAUDE.md rule): monkeypatch the
// `fs` method Node's own module loader uses to read the file content
// (never chmod 0o000 — root bypasses mode bits, silently zero-coverage in
// root Docker/CI, and would also mutate a file shared by concurrent test
// workers). The real compiled artifact is never touched or renamed.
const origReadFileSync = fs.readFileSync;
fs.readFileSync = function patchedReadFileSync(target, ...rest) {
if (target === WORKFLOW_FRAGMENTS_LIB_PATH) {
throw new Error('injected read failure (never a real fs fault)');
}
return origReadFileSync.call(fs, target, ...rest);
};
t.after(() => {
fs.readFileSync = origReadFileSync;
});
assert.throws(() => loadWorkflowFragmentsLib(), (err) => {
assert.ok(err instanceof ManifestBuildError, 'must throw a ManifestBuildError, not a plain ExitError');
assert.equal(err.reason, REASON.FAIL_LIB_NOT_BUILT);
assert.equal(err.subject, 'msd-core/bin/lib/workflow-fragments.cjs');
assert.match(err.message, /npm run build:lib/);
return true;
});
});
test('checkReport surfaces FAIL_LIB_NOT_BUILT as the same typed envelope other --check failures produce (Finding 2, #2932 review)', (t) => {
const tmpRoot = createTempDir('gen-section-manifest-');
t.after(() => cleanup(tmpRoot));
const { workflowsDir, manifestPath } = buildFixture(
tmpRoot,
'sample',
markerSource('handle-x', 'always', 'handle-x.md'),
{ 'handle-x.md': '<step name="handle_x">\nbody\n</step>\n' },
);
const origReadFileSync = fs.readFileSync;
fs.readFileSync = function patchedReadFileSync(target, ...rest) {
if (target === WORKFLOW_FRAGMENTS_LIB_PATH) {
throw new Error('injected read failure (never a real fs fault)');
}
return origReadFileSync.call(fs, target, ...rest);
};
t.after(() => {
fs.readFileSync = origReadFileSync;
});
const report = checkReport(workflowsDir, manifestPath, tmpRoot);
assert.equal(report.ok, false);
assert.equal(report.reason, REASON.FAIL_LIB_NOT_BUILT);
assert.equal(report.subject, 'msd-core/bin/lib/workflow-fragments.cjs');
});
});
// ─── #2993 (epic #1671 Phase 6.2) rows C1/C2: the shipped, committed
// msd-core/workflows/section-manifest.json artifact itself — never a
// regenerated fixture — gains a `plan-phase` key with all 6 sections in
// document order (C1), while `execute-phase`'s own entry stays
// BYTE-IDENTICAL (C2, a Hyrum gate: #2932/#2992's 3 pre-existing sections
// must not shift shape just because a sibling workflow key was added).
describe('shipped msd-core/workflows/section-manifest.json (#2993 rows C1/C2)', () => {
const SHIPPED_MANIFEST_PATH = path.join(ROOT, 'msd-core', 'workflows', 'section-manifest.json');
function readShippedManifest() {
return JSON.parse(fs.readFileSync(SHIPPED_MANIFEST_PATH, 'utf8'));
}
test('planPhaseKeyHasAllSixSectionsInDocumentOrder (row C1)', () => {
const manifest = readShippedManifest();
assert.deepEqual(manifest.workflows['plan-phase'], [
{
id: 'reviews-prerequisite',
when: 'flag:--reviews',
read: 'msd-core/workflows/plan-phase/steps/reviews-prerequisite.md',
},
{
id: 'prd-express-gate',
when: 'flag:--prd',
read: 'msd-core/workflows/plan-phase/steps/prd-express-gate.md',
},
{
id: 'adr-ingest-express-path',
when: 'flag:--ingest',
read: 'msd-core/workflows/plan-phase/steps/adr-ingest-express-path.md',
},
{
id: 'research-only-modifiers',
when: 'flag:--research-phase',
read: 'msd-core/workflows/plan-phase/steps/research-only-modifiers.md',
},
{
id: 'research-only-early-exit',
when: 'flag:--research-phase',
read: 'msd-core/workflows/plan-phase/steps/research-only-early-exit.md',
},
{
id: 'chunked-planning-mode',
when: 'state:chunked-mode',
read: 'msd-core/workflows/plan-phase/steps/chunked-planning-mode.md',
},
]);
});
test('executePhaseKeyIsByteIdenticalToBeforeThisChange (row C2 — Hyrum gate)', () => {
const manifest = readShippedManifest();
assert.deepEqual(manifest.workflows['execute-phase'], [
{
id: 'partial-wave',
when: 'flag:--wave',
read: 'msd-core/workflows/execute-phase/steps/partial-wave.md',
},
{
id: 'gap-closure-artifacts',
when: 'state:gap-closure-phase',
read: 'msd-core/workflows/execute-phase/steps/gap-closure-artifacts.md',
},
{
id: 'regression-gate',
when: 'state:has-prior-phases',
read: 'msd-core/workflows/execute-phase/steps/regression-gate.md',
},
]);
});
test('shippedManifestPassesTheRealCheckAgainstTheRealRepo (idempotency, mirrors row 29)', () => {
const r = runGenSectionManifest([
'--check', '--workflows-dir', path.join(ROOT, 'msd-core', 'workflows'), '--manifest-path', SHIPPED_MANIFEST_PATH, '--repo-root', ROOT,
]);
assert.equal(r.code, 0, `the shipped manifest must already be up to date; stderr: ${r.stderr}`);
});
});
// ─── Flag-forwarding regression guard (BLOCKER fix, epic #1671 Phase 6.2) ──
//
// The workflows never forwarded their flags to the init CLI, so every
// `flag:--X` atom was permanently FALSE in production and its gated section
// permanently EXCLUDED (`prd-express-gate` unreachable under `--prd`;
// `partial-wave` unreachable under `--wave`, pre-existing since #2932 Phase
// 5). This guard is DERIVED from the shipped manifest — never a hardcoded
// {workflow, flag} list — so it also catches the NEXT `flag:--X` atom added
// to any workflow without its own dedicated test.
describe('workflow msd_run query init.<workflow> invocations forward every flag:--X atom (regression guard)', () => {
const SHIPPED_MANIFEST_PATH = path.join(ROOT, 'msd-core', 'workflows', 'section-manifest.json');
function readShippedManifest() {
return JSON.parse(fs.readFileSync(SHIPPED_MANIFEST_PATH, 'utf8'));
}
/**
* All shell variable names assigned (anywhere in `scope`, via a
* double-quoted `VAR="..."` assignment) a value whose whitespace-split
* tokens include the exact literal `atomFlag` token (e.g. `--prd`).
* Whole-token comparison (not substring) so `--research-phase` never
* satisfies a check for `--research`, and vice versa.
*/
function varsCarryingFlagToken(scope, atomFlag) {
const carriers = new Set();
const assignRe = /([A-Za-z_][A-Za-z0-9_]*)="([^"]*)"/g;
let m;
while ((m = assignRe.exec(scope)) !== null) {
const [, varName, rhs] = m;
if (rhs.split(/\s+/).includes(atomFlag)) carriers.add(varName);
}
return carriers;
}
/** `$VAR` / `${VAR}` variable references appearing anywhere in `line`. */
function varsReferencedIn(line) {
const referenced = new Set();
const refRe = /\$\{?([A-Za-z_][A-Za-z0-9_]*)\}?/g;
let m;
while ((m = refRe.exec(line)) !== null) referenced.add(m[1]);
return referenced;
}
const manifest = readShippedManifest();
for (const [workflow, sections] of Object.entries(manifest.workflows)) {
const flagAtoms = [
...new Set(
sections
.map((s) => s.when)
.filter((when) => when.startsWith('flag:--'))
.map((when) => when.slice('flag:'.length)),
),
];
if (flagAtoms.length === 0) continue;
test(`${workflow}.md forwards [${flagAtoms.join(', ')}] to its msd_run query init.${workflow} invocation`, () => {
const workflowMdPath = path.join(ROOT, 'msd-core', 'workflows', `${workflow}.md`);
const content = fs.readFileSync(workflowMdPath, 'utf8');
// A workflow may call `msd_run query init.<workflow>` more than once
// (e.g. new-milestone.md's early INIT_EARLY call, whose section_manifest
// gates ONLY project-md-milestone-write per its own prose, followed by
// a later full INIT call that gates every other flag-gated section and
// does forward the flag). Every occurrence is a candidate carrier —
// a flagAtom only needs forwarding by AT LEAST ONE invocation line,
// not necessarily the first.
const initLineRe = new RegExp(`^.*msd_run query init\\.${workflow}\\b.*$`, 'mg');
const initLineMatches = [...content.matchAll(initLineRe)];
assert.ok(initLineMatches.length > 0, `${workflow}.md must contain a "msd_run query init.${workflow}" invocation line`);
for (const atomFlag of flagAtoms) {
const forwardedByAnyInvocation = initLineMatches.some((match) => {
const initLine = match[0];
// Everything up to and including this invocation line: real
// workflows parse $ARGUMENTS into a param variable earlier in the
// same bash block, then reference that variable on the INIT= line
// itself.
const scope = content.slice(0, match.index + initLine.length);
const referencedVars = varsReferencedIn(initLine);
const directlyPresent = initLine
.split(/\s+/)
.includes(atomFlag);
const carriers = varsCarryingFlagToken(scope, atomFlag);
const forwardedViaVar = [...carriers].some((v) => referencedVars.has(v));
return directlyPresent || forwardedViaVar;
});
assert.ok(
forwardedByAnyInvocation,
`${workflow}.md's "msd_run query init.${workflow}" invocation line(s) must forward a parameter for ` +
`${atomFlag} (its gated section is otherwise permanently excluded — see BLOCKER, epic #1671 Phase 6.2). ` +
`Invocation line(s): ${initLineMatches.map((m) => m[0]).join(' | ')}`,
);
}
});
}
});
// ─── REASON enum shape lock (mirrors gen-context-index.cjs precedent) ──────
describe('gen-section-manifest.cjs REASON enum', () => {
test('REASON key set is exactly the documented set', () => {
assert.deepEqual(Object.keys(REASON).sort(), [
'FAIL_LIB_NOT_BUILT',
'FAIL_MANIFEST_MALFORMED_SHAPE',
'FAIL_MANIFEST_MISSING',
'FAIL_MANIFEST_UNPARSEABLE',
'FAIL_MISSING_STEP_FILE',
'FAIL_ORPHAN_STEP_FILE',
'FAIL_SOURCE_PARSE_ERROR',
'FAIL_STALE',
'FAIL_WRITE_ERROR',
'OK_UP_TO_DATE',
]);
});
test('REASON is frozen', () => {
assert.ok(Object.isFrozen(REASON));
});
});