Files
msd-core/tests/workflow-fragments.test.cjs
Tom Boucher 640eaee16e chore(#2930): fragmentize execute-phase.md and prove per-runtime composed emission (#2972)
* feat(#2930): fragmentize plan-phase.md workflow into per-runtime-composed sections

Adds src/workflow-fragments.cts (in-file <!-- gsd:section --> marker
parser/composer, ADR-1671 epic #1671 Phase 3), wires it into
bin/install.js's copyWithPathReplacement emission path, and pilots the
marker grammar on gsd-core/workflows/plan-phase.md.

Bookkeeping ripple for the new src/*.cts module: .gitignore,
eslint.config.mjs, docs/INVENTORY.md + docs/INVENTORY-MANIFEST.json,
and a CONTEXT.md glossary entry. Amends ADR-1671 with open questions 1
and 2 resolutions and records the closed when= applicability grammar.
Adds docs/reference/workflow-fragments.md and an ARCHITECTURE.md
section documenting the marker authoring model.

* fix(#2930): put allow-test-rule issue ref on the same line as the marker

lint-allow-test-rule-refs.cjs requires the #NNN issue reference on the
same source line as `allow-test-rule:`; it was one line below and read
as an unreferenced novel exemption.

* docs(#2930): link the orphaned gate-predicates reference from the docs index

Found while adding the workflow-fragments reference doc: docs/reference/gate-predicates.md
shipped without an entry in docs/README.md, so it was unreachable from the docs index.
Fixed inline rather than deferred.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(#2930): scope composition to workflows, add typed failure reasons

Review findings from two orthogonal passes:

- Scope composeWorkflow to gsd-core/workflows/ only. It previously ran on
  every .md the installer copied, so a future agent/command/reference doc
  documenting the marker syntax with an unfenced example would have been
  mis-parsed and silently stripped — a lossy drop the phase forbids.
- Add a frozen REASON enum; failures attach a typed .reason and tests assert
  on it instead of matching free-form message text (CONTRIBUTING.md:635-694).
- Derive the property generator's when= values from WHEN_VOCABULARY instead
  of duplicating them (DEFECT.GENERATIVE-FIX).
- Add adversarial parser fixtures: Unicode headings, NUL, U+FFFD, BOM,
  fence-within-fence, tilde and indented fences, lone-CR marker line.
- Document why --mvp is structurally unmarkable: its content is interleaved,
  not sectioned, so the whole-line grammar cannot reach it.

Also fixes two stale tests on this branch, each reproduced on the unmodified
tree before correction.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(#2930): retarget the pilot from plan-phase to execute-phase

The full remote matrix went red on both Linux lanes. Root cause was ours:
tests/phase6-capstone-conformance.test.cjs holds a PRE_PHASE6 ceiling of
94519 bytes for plan-phase.md, asserting an ADR-857 Phase-6 completion
property. That is a third size gate beyond the tier caps and the
differential ratchet, and it left plan-phase.md just 36 bytes of headroom
rather than the 3821 computed from the XL cap. The 330 marker bytes
overran it by 294.

Raising the ceiling is not an option: it is a red line certifying another
ADR's completion. plan-phase.md is reverted to byte-identical origin/next
and the pilot moves to execute-phase.md, which has 728 bytes of headroom
under its own ceiling and lands at 93147 with 3 marker pairs.

The vocabulary narrows to the atoms actually used: always, flag:--wave,
state:gap-closure-phase, state:has-prior-phases.

Recorded in the ADR: every branch the epic names lives in plan-phase.md,
which cannot be fragmentized until caps move from source to emitted bytes.
That is direct evidence for the epic's premise and may reorder phases 3-4.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* chore(#2930): backfill changeset PR number (#2972)

* fix(#2930): make the emission install tests portable on Windows

The windows-latest lane went red on three tests in the new install suite;
Linux was green. Both causes were in the test harness, not the module.

Root normalization: the opencode converter always embeds the install root
forward-slashed, but the tests stripped it with the native-separator string
from mkdtemp. On Windows that never matched, so the root leaked through
unstripped — and because the real and stub install roots have different
prefix lengths, that length difference landed directly in the byte-delta
assertion (344 observed vs 275 expected). Normalize both text and root to
one separator form before stripping.

@-ref resolution: the helper stripped only the @~/ and @$HOME/ forms, so a
Windows absolute ref (@C:/Users/...) fell through and was joined onto the
root, producing ...\@C:\Users\... Strip the @ first, then detect
absoluteness from the token's own shape (POSIX, drive-letter, or UNC) with
no platform branching, so every OS takes the same path.

Neither assertion was weakened; the exact-equality byte check is the point
of the test and still holds.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(#2930): document every REASON member and guard the doc/enum parity

Code review found the reference doc's 'Fails closed' list covering 10 of the
11 frozen REASON members — MALFORMED_ATTRIBUTES (parseAttrs rejects malformed
key="value" syntax) had no bullet, and it is distinct from
UNRECOGNIZED_ATTRIBUTE, which is valid syntax with an unknown key.

Two parallel surfaces sharing one constant with nothing asserting they agree is
the DEFECT.GENERATIVE-FIX class, so the same commit adds the parity assertion:
the test derives the enum side from the built module and the doc side by parsing
the reference page, keyed on the reason IDENTIFIER rather than prose so a
reworded bullet does not break it, and reports set differences in both
directions by name.

Proven non-vacuous: removing the MALFORMED_ATTRIBUTES bullet turns the suite
red naming that exact member; restoring it returns 44/44.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: sim <sim@local>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 12:12:20 -04:00

777 lines
31 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:--wave', 'state:gap-closure-phase', 'state:has-prior-phases'],
);
});
});
// ─── 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');
});
});