Files
msd-core/tests/workflow-fragments.test.cjs
Tom Boucher f1af47766a chore(#1671): widen the when= grammar and key the section manifest per workflow — Phase 6.1 (#3013)
* chore(#2992): widen the when= grammar and key the section manifest per workflow

Epic #1671 Phase 6.1. Two blockers stopped the fragment model reaching any
file beyond execute-phase.md: the when= vocabulary was frozen at 4 atoms
(3 execute-phase-specific), and the section manifest was single-workflow by
construction with 'execute-phase' hardcoded into buildSectionManifestField.

- widen WHEN_VOCABULARY 4 -> 14 via a coordinated ADR-1671 amendment; the
  grammar stays CLOSED (one atom, no operators, negation or nesting) and
  WHEN_PREDICATES stays a hand-written literal map, never deriving a
  predicate from its atom string
- InvocationFacts gains flags: ReadonlySet<string> plus three computed state
  booleans; add the missing reverse vocabulary/predicate parity guard
- key the manifest artifact per workflow; a stale flat {sections:[...]}
  artifact now fails shape validation instead of being misattributed
- wire the field into six init entry points and parse the flags each needs

An atom ships only with both a real consuming section and a fact the init
seam actually computes. Six surveyed atoms are withheld because their
workflows have no dedicated init entry point; an atom without a computed
fact evaluates false forever and silently disables its own section.

Fixes a defect found while wiring: parseNamedArgs always materializes a
boolean flag key, so folding its false into the absent sentinel is required
or every flag reads as present and gating is silently always-on.

Also resolves ADR-1671:194 by measurement: --mvp stays unmarkable, because
its interleaved sites are always-run flag resolution and a ~340 byte block
that already delegates lazily.

Refs #2992

* fix(#2992): treat any falsy option value as an absent flag and reject unsafe manifest read paths

Findings from two orthogonal reviews (Claude /code-review + an isolated
adversarial pass); both independently reproduced the first one.

- MAJOR: the flags-builder treated only `undefined` as absent, but
  parseNamedArgs yields `null` for an absent value-flag and `false` for an
  absent boolean-flag, so `--granularity` read as present on every
  plan-phase invocation. Fixed at the root: a flag is present iff its
  option value is truthy. The six per-handler `|| undefined` folds are now
  redundant and removed, which also closes the duplicate-translation and
  missed-onboard-handler findings.
- MAJOR: state:needs-codebase-map had zero coverage. Added unit, property
  and real-CLI integration tests.
- MINOR: reject absolute, UNC/drive and `..`-traversing `read` paths in the
  manifest, degrading the whole load to null like every other shape
  violation. Verified: `/etc/passwd` previously reached section_manifest.read.
- MINOR: corrected a stale "4 to 20" doc comment; the vocabulary is 14.

Refs #2992

* test(#2992): update the generator suite for the per-workflow manifest shape

The remote matrix went red with 5 unique failures, identical on
linux-node22 and linux-node24, all in tests/gen-section-manifest.test.cjs.
Re-keying the artifact to {workflows:{...}} left this suite asserting the
old flat {sections:[...]} shape; nothing else in the tree still does.

- three tests read manifest.sections.length, now undefined; retargeted at
  workflows.<name> with their original intent preserved (a fenced or
  loop-host marker still asserts NO section is produced, not merely a
  changed count)
- the stale-manifest test wrote its fixture in the OLD shape, so it tripped
  shape validation and stopped exercising staleness at all. Its fixture is
  now valid-but-mismatched so FAIL_STALE is genuinely reached again.
- added the coverage that exposed: a pre-6.1 flat artifact must report
  FAIL_MANIFEST_MALFORMED_SHAPE. That is the real upgrade path for an
  installed tree and nothing covered it.

Refs #2992

* chore(#2992): backfill changeset pr number to 3013

---------

Co-authored-by: sim <sim@local>
2026-08-02 22:36:45 -04:00

902 lines
35 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
/**
* Example-based unit tests for src/workflow-fragments.cts (compiled to
* gsd-core/bin/lib/workflow-fragments.cjs) — issue #2930 (epic #1671 Phase 3).
*
* Covers 50-test-matrix.md rows 1-29 and 37 (unit level). Rows 30/31
* (property) live in workflow-fragments.property.test.cjs; rows 32-36
* (install-level, real spawn-install) are out of scope for this module's
* unit suite per ADR-1671 "Architecture and contracts".
*
* No source-grep (CONTRIBUTING.md): every assertion is on typed values
* (WorkflowSection records, ComposeResult metadata, byte counts) — never on
* rendered text via `.includes()`/`.match()`.
*/
const { describe, test } = 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 {
parseWorkflowSections,
toFragments,
renderFragments,
composeWorkflow,
WHEN_VOCABULARY,
REASON,
} = require('../gsd-core/bin/lib/workflow-fragments.cjs');
const { composeWithinBudget } = require('../gsd-core/bin/lib/context-composer.cjs');
const measureBytes = (text) => Buffer.byteLength(text, 'utf8');
/** Compose a document string from an array of lines, joined with '\n'. */
const doc = (...lines) => lines.join('\n');
// ─── Row 1: unmarked document (the 88/89 production shape) ─────────────────
describe('unmarked document round trip', () => {
test('unmarkedDocumentRoundTripsByteIdentical', () => {
const source = doc(
'# Some Workflow',
'',
'Ordinary prose describing the workflow.',
'',
'## A heading',
'More prose.',
'',
);
const sections = parseWorkflowSections(source);
assert.equal(sections.length, 1);
assert.equal(sections[0].explicit, false);
assert.equal(sections[0].id, 'gap-0');
assert.equal(sections[0].body, source);
const rendered = composeWorkflow(source);
assert.equal(rendered, source);
});
});
// ─── Row 2: single well-formed marker pair ──────────────────────────────────
describe('single marker pair', () => {
test('singleMarkerPairStripsMarkersAndPreservesBody', () => {
const source = doc(
'before prose',
'<!-- gsd:section id="sec-a" when="flag:--wave" -->',
'body line 1',
'body line 2',
'<!-- /gsd:section -->',
'after prose',
);
const sections = parseWorkflowSections(source);
assert.equal(sections.length, 3);
assert.equal(sections[0].explicit, false);
assert.equal(sections[0].body, 'before prose\n');
assert.equal(sections[1].explicit, true);
assert.equal(sections[1].id, 'sec-a');
assert.equal(sections[1].when, 'flag:--wave');
assert.equal(sections[1].body, 'body line 1\nbody line 2\n');
assert.equal(sections[2].explicit, false);
assert.equal(sections[2].body, 'after prose');
const rendered = composeWorkflow(source);
assert.equal(rendered, 'before prose\nbody line 1\nbody line 2\nafter prose');
});
});
// ─── Row 3: several disjoint pairs + unmarked gaps ─────────────────────────
describe('multiple disjoint marker pairs', () => {
test('multiplePairsPartitionDocumentExactly', () => {
const source = doc(
'gap0',
'<!-- gsd:section id="a" when="always" -->',
'bodyA',
'<!-- /gsd:section -->',
'gap1',
'<!-- gsd:section id="b" when="state:has-prior-phases" -->',
'bodyB',
'<!-- /gsd:section -->',
'gap2',
);
const sections = parseWorkflowSections(source);
assert.deepEqual(
sections.map((s) => ({ id: s.id, explicit: s.explicit })),
[
{ id: 'gap-0', explicit: false },
{ id: 'a', explicit: true },
{ id: 'gap-1', explicit: false },
{ id: 'b', explicit: true },
{ id: 'gap-2', explicit: false },
],
);
const markerLineRe = /^<!--\s*\/?gsd:section.*-->\s*$/;
const expected = source
.split('\n')
.filter((line) => !markerLineRe.test(line))
.join('\n');
assert.equal(composeWorkflow(source), expected);
});
});
// ─── Row 4: the real pilot workflow ─────────────────────────────────────────
describe('real execute-phase.md', () => {
// NOTE (chore/2930 retarget): the pilot moved from plan-phase.md to
// execute-phase.md — plan-phase.md sits 36 B under the ADR-857 Phase-6
// PRE_PHASE6 gate (tests/phase6-capstone-conformance.test.cjs) and cannot
// absorb marker overhead, so the maintainer retargeted the pilot to
// execute-phase.md (partial-wave, gap-closure-artifacts, regression-gate).
test('pilotWorkflowParsesAndRendersToSourceMinusMarkers', () => {
const pilotPath = path.join(__dirname, '..', 'gsd-core', 'workflows', 'execute-phase.md');
const original = fs.readFileSync(pilotPath, 'utf8');
// execute-phase.md carries the pilot's real marker pairs today: parsing
// it must recognize exactly those three explicit sections, in document
// order, and composing it must strip every marker line while leaving
// every byte of body content untouched.
const baselineSections = parseWorkflowSections(original, pilotPath);
const baselineExplicit = baselineSections.filter((s) => s.explicit);
assert.deepEqual(
baselineExplicit.map((s) => s.id),
['partial-wave', 'gap-closure-artifacts', 'regression-gate'],
);
const composedOriginal = composeWorkflow(original, { sourcePath: pilotPath });
assert.equal(composedOriginal.includes('gsd:section'), false);
assert.ok(Buffer.byteLength(composedOriginal, 'utf8') < Buffer.byteLength(original, 'utf8'));
// Wrap an ADDITIONAL, disjoint marker pair around an arbitrary interior
// slice of real content that sits outside every existing marker pair
// (lines 11-15, well before "partial-wave") and confirm it parses as a
// fourth explicit section and composes to the SAME final output as the
// unmodified file — every fragment is `verbatim` (row 23), so wrapping
// already-included content in a new marker pair can never change what
// is emitted, only how it is partitioned internally.
const lines = original.split(/\r?\n/);
const sliceStart = 10;
const sliceEnd = 15;
const markedLines = [
...lines.slice(0, sliceStart),
'<!-- gsd:section id="pilot-slice" when="always" -->',
...lines.slice(sliceStart, sliceEnd),
'<!-- /gsd:section -->',
...lines.slice(sliceEnd),
];
const marked = markedLines.join('\n');
const sections = parseWorkflowSections(marked, pilotPath);
const explicitSections = sections.filter((s) => s.explicit);
assert.deepEqual(
explicitSections.map((s) => s.id),
['pilot-slice', 'partial-wave', 'gap-closure-artifacts', 'regression-gate'],
);
assert.equal(explicitSections[0].body, lines.slice(sliceStart, sliceEnd).join('\n') + '\n');
const rendered = composeWorkflow(marked, { sourcePath: pilotPath });
assert.equal(rendered, composedOriginal);
assert.equal(measureBytes(rendered), measureBytes(composedOriginal));
});
});
// ─── Row 5/6: fence negative space ──────────────────────────────────────────
describe('marker lookalikes inside fences', () => {
test('markerInsideFencedBlockIsLiteral', () => {
const source = doc(
'prose before',
'```',
'<!-- gsd:section id="fake" when="always" -->',
'```',
'prose after',
);
const sections = parseWorkflowSections(source);
assert.equal(sections.length, 1);
assert.equal(sections[0].explicit, false);
assert.equal(sections[0].body, source);
assert.equal(composeWorkflow(source), source);
});
test('markerInsideFenceInsideSectionStaysLiteral', () => {
const source = doc(
'<!-- gsd:section id="real" when="always" -->',
'intro',
'```',
'<!-- gsd:section id="fake" when="always" -->',
'<!-- /gsd:section -->',
'```',
'outro',
'<!-- /gsd:section -->',
);
const sections = parseWorkflowSections(source);
assert.equal(sections.length, 1);
assert.equal(sections[0].explicit, true);
assert.equal(sections[0].id, 'real');
assert.equal(
sections[0].body,
['intro', '```', '<!-- gsd:section id="fake" when="always" -->', '<!-- /gsd:section -->', '```', 'outro', ''].join(
'\n',
),
);
});
});
// ─── Row 7/8: fence/comment mutual precedence ───────────────────────────────
describe('fence and comment mutual precedence', () => {
test('fenceDelimiterInsideCommentDoesNotOpenFence', () => {
const source = doc(
'<!-- unrelated comment',
'```',
'still commented',
'-->',
'<!-- gsd:section id="after-comment" when="always" -->',
'body',
'<!-- /gsd:section -->',
);
// If the fence delimiter on line 2 had wrongly opened a fence, the real
// marker pair below would never be recognized (it would be swallowed as
// "fence content" all the way to EOF).
const sections = parseWorkflowSections(source);
const explicitSections = sections.filter((s) => s.explicit);
assert.equal(explicitSections.length, 1);
assert.equal(explicitSections[0].id, 'after-comment');
assert.equal(explicitSections[0].body, 'body\n');
});
test('commentTokenInsideFenceDoesNotOpenComment', () => {
const source = doc(
'```',
'<!-- unclosed comment token inside fence',
'```',
'<!-- gsd:section id="after-fence" when="always" -->',
'body',
'<!-- /gsd:section -->',
);
// If the `<!--` inside the fence had wrongly opened a real comment, the
// real marker pair below would never be recognized (swallowed as
// "comment content" to EOF).
const sections = parseWorkflowSections(source);
const explicitSections = sections.filter((s) => s.explicit);
assert.equal(explicitSections.length, 1);
assert.equal(explicitSections[0].id, 'after-fence');
assert.equal(explicitSections[0].body, 'body\n');
});
});
// ─── Row 9/10: other negative space ─────────────────────────────────────────
describe('loop-host and backtick negative space', () => {
test('loopHostMarkerIsNotASectionMarker', () => {
const source = doc(
'<!-- gsd:loop-host',
'step: plan',
'points: plan:pre, plan:post',
'-->',
'<purpose>Do the thing.</purpose>',
);
const sections = parseWorkflowSections(source);
assert.equal(sections.length, 1);
assert.equal(sections[0].explicit, false);
assert.equal(sections[0].body, source);
assert.equal(composeWorkflow(source), source);
});
test('backtickedMarkerMentionIsNotAMarker', () => {
const source = doc(
'See `<!-- gsd:section id="x" when="always" -->` for the marker syntax.',
'And the close form is `<!-- /gsd:section -->` on its own line.',
);
const sections = parseWorkflowSections(source);
assert.equal(sections.length, 1);
assert.equal(sections[0].explicit, false);
assert.equal(sections[0].body, source);
assert.equal(composeWorkflow(source), source);
});
});
// ─── Rows 11-14: structural negatives with location ────────────────────────
describe('structural negatives throw with file + line', () => {
test('unclosedSectionThrowsWithLocation', () => {
const source = doc('prose', '<!-- gsd:section id="a" when="always" -->', 'body, never closed');
assert.throws(
() => parseWorkflowSections(source, 'workflow.md'),
(err) => err instanceof TypeError && err.message.includes('workflow.md:2') && err.reason === REASON.UNCLOSED_SECTION,
);
});
test('unmatchedCloseThrowsWithLocation', () => {
const source = doc('prose', '<!-- /gsd:section -->', 'more prose');
assert.throws(
() => parseWorkflowSections(source, 'workflow.md'),
(err) => err instanceof TypeError && err.message.includes('workflow.md:2') && err.reason === REASON.UNMATCHED_CLOSE,
);
});
test('nestedSectionThrows', () => {
const source = doc(
'<!-- gsd:section id="outer" when="always" -->',
'<!-- gsd:section id="inner" when="always" -->',
'body',
'<!-- /gsd:section -->',
'<!-- /gsd:section -->',
);
assert.throws(
() => parseWorkflowSections(source, 'workflow.md'),
(err) => err instanceof TypeError && err.message.includes('workflow.md:2') && err.reason === REASON.NESTED_SECTION,
);
});
test('duplicateSectionIdThrows', () => {
const source = doc(
'<!-- gsd:section id="dup" when="always" -->',
'first',
'<!-- /gsd:section -->',
'<!-- gsd:section id="dup" when="flag:--wave" -->',
'second',
'<!-- /gsd:section -->',
);
// Throws on the SECOND occurrence's line, not the first.
assert.throws(
() => parseWorkflowSections(source, 'workflow.md'),
(err) => err instanceof TypeError && err.message.includes('workflow.md:4') && err.reason === REASON.DUPLICATE_ID,
);
});
});
// ─── Rows 15-18: attribute-shape negatives ─────────────────────────────────
describe('attribute-shape negatives', () => {
test('missingIdAttributeThrows', () => {
const source = '<!-- gsd:section when="always" -->\nbody\n<!-- /gsd:section -->';
assert.throws(
() => parseWorkflowSections(source, 'workflow.md'),
(err) => err instanceof TypeError && err.reason === REASON.MISSING_ID,
);
});
test('missingWhenAttributeThrows', () => {
const source = '<!-- gsd:section id="x" -->\nbody\n<!-- /gsd:section -->';
assert.throws(
() => parseWorkflowSections(source, 'workflow.md'),
(err) => err instanceof TypeError && err.reason === REASON.MISSING_WHEN,
);
});
test('unknownWhenValueThrows', () => {
const source = '<!-- gsd:section id="x" when="flag:--nonexistent" -->\nbody\n<!-- /gsd:section -->';
assert.throws(
() => parseWorkflowSections(source, 'workflow.md'),
(err) => err instanceof TypeError && err.reason === REASON.UNKNOWN_WHEN,
);
});
test('whenValueWithBooleanOperatorThrows', () => {
for (const when of ['flag:--wave && state:has-prior-phases', 'flag:--wave || state:has-prior-phases', '!flag:--wave']) {
const source = `<!-- gsd:section id="x" when="${when}" -->\nbody\n<!-- /gsd:section -->`;
assert.throws(
() => parseWorkflowSections(source, 'workflow.md'),
(err) => err instanceof TypeError && err.reason === REASON.UNKNOWN_WHEN,
`expected throw for when="${when}"`,
);
}
});
test('malformedAttributesThrows', () => {
const source = '<!-- gsd:section id="x" when -->\nbody\n<!-- /gsd:section -->';
assert.throws(
() => parseWorkflowSections(source, 'workflow.md'),
(err) => err instanceof TypeError && err.reason === REASON.MALFORMED_ATTRIBUTES,
);
});
test('unrecognizedAttributeThrows', () => {
const source = '<!-- gsd:section id="x" when="always" bogus="1" -->\nbody\n<!-- /gsd:section -->';
assert.throws(
() => parseWorkflowSections(source, 'workflow.md'),
(err) => err instanceof TypeError && err.reason === REASON.UNRECOGNIZED_ATTRIBUTE,
);
});
test('malformedIdValueThrows', () => {
const source = '<!-- gsd:section id="-bad-" when="always" -->\nbody\n<!-- /gsd:section -->';
assert.throws(
() => parseWorkflowSections(source, 'workflow.md'),
(err) => err instanceof TypeError && err.reason === REASON.MALFORMED_ID,
);
});
test('closeMarkerWithAttributesThrows', () => {
const source = '<!-- gsd:section id="x" when="always" -->\nbody\n<!-- /gsd:section foo="1" -->';
assert.throws(
() => parseWorkflowSections(source, 'workflow.md'),
(err) => err instanceof TypeError && err.reason === REASON.CLOSE_WITH_ATTRIBUTES,
);
});
});
// ─── FIX 2/3 (chore/2930 review): REASON enum shape is locked ─────────────
describe('REASON enum is frozen and its shape is locked', () => {
test('reasonEnumKeysAreLocked', () => {
assert.equal(Object.isFrozen(REASON), true);
assert.deepEqual(Object.keys(REASON).sort(), [
'CLOSE_WITH_ATTRIBUTES',
'DUPLICATE_ID',
'MALFORMED_ATTRIBUTES',
'MALFORMED_ID',
'MISSING_ID',
'MISSING_WHEN',
'NESTED_SECTION',
'UNCLOSED_SECTION',
'UNKNOWN_WHEN',
'UNMATCHED_CLOSE',
'UNRECOGNIZED_ATTRIBUTE',
]);
});
});
// ─── Doc/enum parity guard (DEFECT.GENERATIVE-FIX, code review #2930) ──────
describe('REASON enum and docs "Fails closed" bullets stay in parity', () => {
test('everyReasonMemberIsDocumentedAndNoStaleBulletsRemain', () => {
// allow-test-rule: docs-parity — the doc text IS the contract being checked here (#2930)
const docPath = path.join(__dirname, '..', 'docs', 'reference', 'workflow-fragments.md');
const docText = fs.readFileSync(docPath, 'utf8');
const sectionMatch = /## Fails closed\r?\n([\s\S]*?)\r?\n## /.exec(docText);
assert.ok(sectionMatch, 'docs/reference/workflow-fragments.md must have a "## Fails closed" section');
const sectionText = sectionMatch[1];
const enumMembers = Object.keys(REASON);
// Key on the reason IDENTIFIER (e.g. `MALFORMED_ATTRIBUTES`) appearing in
// a bullet, never on bullet prose — a reword of the human-readable
// sentence must never falsely trip or falsely clear this guard.
const undocumented = enumMembers.filter((name) => !sectionText.includes(name));
const mentionedIdentifiers = [...sectionText.matchAll(/`([A-Z][A-Z0-9_]*)`/g)].map((m) => m[1]);
const staleMentions = mentionedIdentifiers.filter((name) => !enumMembers.includes(name));
assert.deepEqual(
undocumented,
[],
`REASON member(s) missing a "Fails closed" bullet in docs/reference/workflow-fragments.md: ${undocumented.join(', ')}`,
);
assert.deepEqual(
staleMentions,
[],
`"Fails closed" section mentions identifier(s) that are not REASON members (stale bullet?): ${staleMentions.join(', ')}`,
);
});
});
// ─── Row 19: frozen vocabulary ──────────────────────────────────────────────
describe('frozen when= vocabulary', () => {
test('whenVocabularyIsFrozenAndLocked', () => {
// WHEN_VOCABULARY is a frozen array (not an enum object) per the shipped
// public API — lock the actual VALUES (sorted), not Object.keys() (which
// for an array only reflects index positions '0','1',... and would not
// catch a value being silently renamed). See the dispatch report for
// this deliberate deviation from the test matrix's literal wording.
assert.equal(Object.isFrozen(WHEN_VOCABULARY), true);
assert.deepEqual(
[...WHEN_VOCABULARY].sort(),
[
'always',
'flag:--auto',
'flag:--discuss',
'flag:--forensic',
'flag:--full',
'flag:--research',
'flag:--reset-phase-numbers',
'flag:--validate',
'flag:--wave',
'state:gap-closure-phase',
'state:has-prior-phases',
'state:needs-codebase-map',
'state:phase-mvp-mode',
'state:worktrees-enabled',
],
);
});
});
// ─── #2992 (epic #1671 Phase 6.1): widened when= vocabulary (14 atoms) ─────
// 50-test-matrix.md rows A2/A5/A6/A14/A19.
describe('widened when= vocabulary (#2992)', () => {
// The 10 net-new atoms shipped by #2992, independent of the pre-existing 4
// (Object.keys-style derivation of the diff would be tokenization of the
// vocabulary itself, so this list is a deliberate hand-written literal,
// mirroring the discipline WHEN_PREDICATES already applies).
const NET_NEW_ATOMS = Object.freeze([
'flag:--auto',
'flag:--discuss',
'flag:--forensic',
'flag:--full',
'flag:--research',
'flag:--reset-phase-numbers',
'flag:--validate',
'state:needs-codebase-map',
'state:phase-mvp-mode',
'state:worktrees-enabled',
]);
test('everyNetNewAtomIsInWhenVocabulary', () => {
// Sanity that the hand-written NET_NEW_ATOMS list above has not drifted
// from the module's own frozen export.
for (const atom of NET_NEW_ATOMS) {
assert.ok(WHEN_VOCABULARY.includes(atom), `expected "${atom}" in WHEN_VOCABULARY`);
}
});
test('acceptsEveryWidenedAtom (row A2)', () => {
for (const when of NET_NEW_ATOMS) {
const source = `<!-- gsd:section id="x" when="${when}" -->\nbody\n<!-- /gsd:section -->`;
const sections = parseWorkflowSections(source);
const explicitSections = sections.filter((s) => s.explicit);
assert.equal(explicitSections.length, 1, `expected acceptance for when="${when}"`);
assert.equal(explicitSections[0].when, when);
assert.equal(composeWorkflow(source), 'body\n');
}
});
test('sameAtomOnTwoDifferentSectionsIsLegal (row A19)', () => {
// Atom reuse is legal; only `id` must be unique.
const source = doc(
'<!-- gsd:section id="first" when="flag:--auto" -->',
'bodyA',
'<!-- /gsd:section -->',
'<!-- gsd:section id="second" when="flag:--auto" -->',
'bodyB',
'<!-- /gsd:section -->',
);
const sections = parseWorkflowSections(source);
const explicitSections = sections.filter((s) => s.explicit);
assert.deepEqual(
explicitSections.map((s) => ({ id: s.id, when: s.when })),
[
{ id: 'first', when: 'flag:--auto' },
{ id: 'second', when: 'flag:--auto' },
],
);
});
test('atomMatchIsCaseSensitive (row A5)', () => {
// A case variant of a real net-new atom must still throw — exact `===`,
// no case folding.
for (const when of ['Flag:--auto', 'flag:--Auto', 'FLAG:--AUTO', 'flag:--RESEARCH']) {
const source = `<!-- gsd:section id="x" when="${when}" -->\nbody\n<!-- /gsd:section -->`;
assert.throws(
() => parseWorkflowSections(source, 'workflow.md'),
(err) => err instanceof TypeError && err.reason === REASON.UNKNOWN_WHEN,
`expected throw for when="${when}"`,
);
}
});
test('atomValueIsNotTrimmed (row A6)', () => {
// A padded value must still throw — the value is not trimmed before the
// vocabulary membership check.
for (const when of [' flag:--auto', 'flag:--auto ', ' state:worktrees-enabled ']) {
const source = `<!-- gsd:section id="x" when="${when}" -->\nbody\n<!-- /gsd:section -->`;
assert.throws(
() => parseWorkflowSections(source, 'workflow.md'),
(err) => err instanceof TypeError && err.reason === REASON.UNKNOWN_WHEN,
`expected throw for when="${when}"`,
);
}
});
test('crlfMarkerLineCarryingAWidenedAtomRoundTripsExactly (row A14)', () => {
const source = [
'prose one',
'<!-- gsd:section id="x" when="flag:--forensic" -->',
'crlf body',
'<!-- /gsd:section -->',
'prose two',
].join('\r\n');
const sections = parseWorkflowSections(source);
const explicitSections = sections.filter((s) => s.explicit);
assert.equal(explicitSections.length, 1);
assert.equal(explicitSections[0].when, 'flag:--forensic');
assert.equal(explicitSections[0].body, 'crlf body\r\n');
const rendered = composeWorkflow(source);
const expected = source
.split('\r\n')
.filter((line) => !/^<!--\s*\/?gsd:section.*-->\s*$/.test(line))
.join('\r\n');
assert.equal(rendered, expected);
});
});
// ─── Rows 20-22: boundary documents ─────────────────────────────────────────
describe('boundary documents', () => {
test('emptyDocumentProducesNoFragments', () => {
const sections = parseWorkflowSections('');
assert.deepEqual(sections, []);
assert.equal(composeWorkflow(''), '');
});
test('documentOfOnlyAMarkerPairYieldsEmptyBody', () => {
const source = '<!-- gsd:section id="x" when="always" -->\n<!-- /gsd:section -->';
const sections = parseWorkflowSections(source);
assert.equal(sections.length, 1);
assert.equal(sections[0].explicit, true);
assert.equal(sections[0].id, 'x');
assert.equal(sections[0].body, '');
assert.equal(composeWorkflow(source), '');
});
test('unclosedFenceAtEofDoesNotThrow', () => {
const source = doc('prose', '```', 'never closed', '<!-- gsd:section id="x" when="always" -->');
assert.doesNotThrow(() => parseWorkflowSections(source));
const sections = parseWorkflowSections(source);
// The whole document, including the marker-shaped line, is literal
// fence content — one implicit gap fragment, byte-identical.
assert.equal(sections.length, 1);
assert.equal(sections[0].explicit, false);
assert.equal(sections[0].body, source);
});
});
// ─── Rows 23-25: cross-platform + liberal formatting ───────────────────────
describe('cross-platform line endings and liberal marker formatting', () => {
test('crlfDocumentRoundTripsByteIdentical', () => {
const source = ['prose one', '<!-- gsd:section id="x" when="always" -->', 'crlf body', '<!-- /gsd:section -->', 'prose two'].join(
'\r\n',
);
const rendered = composeWorkflow(source);
const expected = source
.split('\r\n')
.filter((line) => !/^<!--\s*\/?gsd:section.*-->\s*$/.test(line))
.join('\r\n');
assert.equal(rendered, expected);
});
test('mixedLineEndingsPreservedExactly', () => {
const source = 'prose\r\n<!-- gsd:section id="x" when="always" -->\r\nbody one\nbody two\n<!-- /gsd:section -->\nprose two';
const sections = parseWorkflowSections(source);
const explicitSections = sections.filter((s) => s.explicit);
assert.equal(explicitSections.length, 1);
assert.equal(explicitSections[0].body, 'body one\nbody two\n');
const rendered = composeWorkflow(source);
assert.equal(rendered, 'prose\r\nbody one\nbody two\nprose two');
});
test('attributeOrderAndSpacingAreAccepted', () => {
const variants = [
'<!-- gsd:section id="x" when="always" -->',
'<!--gsd:section id="x" when="always"-->',
'<!-- gsd:section when="always" id="x" -->',
' <!-- gsd:section when="always" id="x" --> ',
'<!--gsd:section when="always"id="x"-->',
];
for (const openLine of variants) {
const source = `${openLine}\nbody\n<!-- /gsd:section -->`;
const sections = parseWorkflowSections(source);
const explicitSections = sections.filter((s) => s.explicit);
assert.equal(explicitSections.length, 1, `expected recognition for: ${openLine}`);
assert.equal(explicitSections[0].id, 'x');
assert.equal(explicitSections[0].when, 'always');
// Re-render never leaks the original spacing — the marker is dropped
// entirely, so only the body survives.
assert.equal(composeWorkflow(source), 'body\n');
}
});
});
// ─── Rows 26-29: budget boundary set (non-lossiness is structural) ─────────
describe('budget boundary set: nothing is ever trimmed', () => {
const source = doc(
'gap prose',
'<!-- gsd:section id="a" when="always" -->',
'section a body',
'<!-- /gsd:section -->',
'more gap prose',
);
function composeAt(budget) {
const sections = parseWorkflowSections(source);
const fragments = toFragments(sections);
return composeWithinBudget({ fragments, budget, measure: measureBytes, options: { charsPerUnit: 1 } });
}
const baseline = (() => {
const sections = parseWorkflowSections(source);
const fragments = toFragments(sections);
return fragments.reduce((sum, f) => sum + measureBytes(f.content), 0);
})();
const expectedRendered = composeWorkflow(source);
test('nothingTrimmedWhenBudgetEqualsContent', () => {
const result = composeAt(baseline);
assert.deepEqual(result.metadata.omitted, []);
assert.deepEqual(result.metadata.shrunk, []);
assert.equal(renderFragments(result), expectedRendered);
});
test('nothingTrimmedWhenBudgetIsOneUnderContent', () => {
const result = composeAt(baseline - 1);
assert.deepEqual(result.metadata.omitted, []);
assert.deepEqual(result.metadata.shrunk, []);
assert.equal(renderFragments(result), expectedRendered);
});
test('nothingTrimmedWhenBudgetIsOneOverContent', () => {
const result = composeAt(baseline + 1);
assert.deepEqual(result.metadata.omitted, []);
assert.deepEqual(result.metadata.shrunk, []);
assert.equal(renderFragments(result), expectedRendered);
});
test('nothingTrimmedUnderAbsurdBudgetPressure', () => {
const result = composeAt(1);
assert.deepEqual(result.metadata.omitted, []);
assert.deepEqual(result.metadata.shrunk, []);
assert.equal(result.metadata.hardFailed, false);
assert.equal(renderFragments(result), expectedRendered);
});
});
// ─── Row 37: fs.readFileSync fault injection ───────────────────────────────
/**
* Simulate the realistic caller shape (read a workflow file, compose it,
* write the composed result elsewhere) with `fs.readFileSync` monkeypatched
* to throw. The monkeypatch is saved/restored HERE, in a helper, inside a
* `finally` — never inside a test body, and never via chmod/permission
* tricks (CLAUDE.md cross-platform fault-injection rule).
*/
function withInjectedReadFailure(fn) {
const original = fs.readFileSync;
fs.readFileSync = () => {
throw new Error('injected read failure');
};
try {
return fn();
} finally {
fs.readFileSync = original;
}
}
// ─── FIX 4 (chore/2930 review): adversarial parser-input fixtures ─────────
// CONTRIBUTING.md:484-513 requires adversarial fixtures for a new parser's
// inputs. Each case here either round-trips byte-identical or produces the
// correct typed REASON — never a message-text match.
describe('adversarial content bytes', () => {
test('unicodeHeadingRoundTripsByteIdentical', () => {
const source = doc(
'# 見出し — Ünïcödé Hëading 🚀',
'<!-- gsd:section id="sec" when="always" -->',
'body with 中文, кириллица, emoji 🎉',
'<!-- /gsd:section -->',
'trailing プロース',
);
const expected = doc('# 見出し — Ünïcödé Hëading 🚀', 'body with 中文, кириллица, emoji 🎉', 'trailing プロース');
const rendered = composeWorkflow(source);
assert.equal(rendered, expected);
assert.equal(measureBytes(rendered), measureBytes(expected));
});
test('nulByteInBodyRoundTripsByteIdentical', () => {
const source = `prose\0more\n<!-- gsd:section id="x" when="always" -->\nbody\0with\0nul\n<!-- /gsd:section -->\nafter\0`;
const sections = parseWorkflowSections(source);
const explicitSections = sections.filter((s) => s.explicit);
assert.equal(explicitSections.length, 1);
assert.equal(explicitSections[0].body, 'body\0with\0nul\n');
const rendered = composeWorkflow(source);
assert.equal(rendered, 'prose\0more\nbody\0with\0nul\nafter\0');
});
test('unicodeReplacementCharacterRoundTripsByteIdentical', () => {
const source = `prose <20> end\n<!-- gsd:section id="x" when="always" -->\nbody <20><>\n<!-- /gsd:section -->\nafter <20>`;
const rendered = composeWorkflow(source);
assert.equal(rendered, 'prose <20> end\nbody <20><>\nafter <20>');
});
test('leadingByteOrderMarkRoundTripsByteIdentical', () => {
const source = '# Heading\n<!-- gsd:section id="x" when="always" -->\nbody\n<!-- /gsd:section -->\ntail';
const sections = parseWorkflowSections(source);
const gaps = sections.filter((s) => !s.explicit);
// The BOM is ordinary content of the leading gap — never stripped or
// otherwise special-cased by this parser.
assert.equal(gaps[0].body, '# Heading\n');
const rendered = composeWorkflow(source);
assert.equal(rendered, '# Heading\nbody\ntail');
});
});
describe('adversarial fence shapes', () => {
test('fenceWithinFenceStaysLiteralUntilOuterCloser', () => {
const source = doc(
'prose before',
'````',
'```',
'<!-- gsd:section id="fake" when="always" -->',
'```',
'````',
'prose after',
);
const sections = parseWorkflowSections(source);
assert.equal(sections.length, 1);
assert.equal(sections[0].explicit, false);
assert.equal(sections[0].body, source);
assert.equal(composeWorkflow(source), source);
});
test('tildeFenceHidesMarkerLookalike', () => {
const source = doc('prose', '~~~', '<!-- gsd:section id="fake" when="always" -->', '~~~', 'prose after');
const sections = parseWorkflowSections(source);
assert.equal(sections.length, 1);
assert.equal(sections[0].explicit, false);
assert.equal(sections[0].body, source);
assert.equal(composeWorkflow(source), source);
});
test('indentedFenceUpToThreeSpacesHidesMarkerLookalike', () => {
const source = doc('prose', ' ```', '<!-- gsd:section id="fake" when="always" -->', ' ```', 'prose after');
const sections = parseWorkflowSections(source);
assert.equal(sections.length, 1);
assert.equal(sections[0].explicit, false);
assert.equal(sections[0].body, source);
assert.equal(composeWorkflow(source), source);
});
});
describe('adversarial line-ending shapes', () => {
test('markerLineTerminatedByLoneCrIsNotRecognizedAsAMarker', () => {
// A bare `\r` with no accompanying `\n` anywhere in the document is not
// an EOL this grammar recognizes (only '' / '\n' / '\r\n' — see the
// module doc comment). The marker-shaped text is therefore never on its
// "own line" and must be left as ordinary literal content, not parsed
// as an open marker.
const source = '<!-- gsd:section id="x" when="always" -->\rbody, never a real line break';
const sections = parseWorkflowSections(source);
assert.equal(sections.length, 1);
assert.equal(sections[0].explicit, false);
assert.equal(sections[0].body, source);
assert.equal(composeWorkflow(source), source);
});
});
describe('fs.readFileSync fault injection mid-compose', () => {
test('readFailureDuringCompositionLeavesNoPartialArtifact', (t) => {
const tmpDir = createTempDir('gsd-wf-fault-');
t.after(() => cleanup(tmpDir));
const srcPath = path.join(tmpDir, 'source.md');
const destPath = path.join(tmpDir, 'composed.md');
fs.writeFileSync(srcPath, '<!-- gsd:section id="x" when="always" -->\nbody\n<!-- /gsd:section -->\n');
function readComposeWrite() {
const content = fs.readFileSync(srcPath, 'utf8');
const result = composeWorkflow(content, { sourcePath: srcPath });
fs.writeFileSync(destPath, result);
return result;
}
assert.throws(
() => withInjectedReadFailure(() => readComposeWrite()),
(err) => err instanceof Error && err.message === 'injected read failure',
);
assert.equal(fs.existsSync(destPath), false, 'no partial artifact must be written when the read fails');
// Restored correctly: a subsequent real call succeeds and DOES write.
const result = readComposeWrite();
assert.equal(fs.existsSync(destPath), true);
assert.equal(result, 'body\n');
});
});