Files
msd-core/tests/workflow-fragments.test.cjs
Tom Boucher ff4a57b78c chore(#1671): migrate the remaining 13 LARGE/XL workflows to the fragment model — Phase 6.3 (#3030)
* chore(#2994): fragmentize progress.md forensic audit onto the fragment model

Extract the --forensic-gated forensic_audit step to
workflows/progress/steps/forensic-audit.md behind a section marker, and
repair progress.md's init line to forward --forensic so the atom is
actually true in production rather than only under direct CLI tests.

progress.md shrinks 32630 -> 27207 bytes.

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

* chore(#2994): fragmentize the four manifest-wired workflows

new-project, quick, new-milestone and progress each already had a
dedicated cmdInit* entry point but zero marked sections. Extract nine
gated bodies to workflows/<wf>/steps/ behind section markers and repair
each init line to forward its flags.

Fold --full into the discuss/research/validate facts inside cmdInitQuick
so the when= grammar never sees an OR, per the chunked-mode precedent.

Fixes found while working, per the no-defer rule:
- cmdInitProgress passed no phase info to buildSectionManifestField, so
  state:phase-mvp-mode was permanently false — an atom in the vocabulary
  whose fact could never be computed.
- the quick init router folded flag tokens into the free-text
  description, which the new forwarding would have corrupted.
- a #2508 dispatch note was nested inside quick.md's Agent(prompt=)
  fence, leaking orchestrator guidance into the subagent prompt.
- progress.md had a 3-vs-4 backtick outer-fence imbalance.

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

* chore(#2994): fragmentize verify-work.md and admit state:ui-phase-active

Wire cmdInitVerifyWork to buildSectionManifestField — it was a dedicated
entry point that never emitted a manifest — and mark two sections.

state:ui-phase-active folds (plan:pre hooks include an active ui step) OR
(the phase dir holds a *-UI-SPEC.md) into one boolean in init.cts, so the
grammar still sees a single operator-free atom. The inner Playwright-MCP
check stays as prose inside the fragment: it is live session state and no
init seam can precompute it.

The MVP false-branch note is a real fallback, not redundant prose, so it
sits outside the marker — gating it away would delete the text needed
precisely when MVP mode is off.

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

* test(#2994): follow moved workflow content in drift guards

Retarget every guard that asserted on content this branch moved into
workflows/<wf>/steps/, mirroring 815b3d897. Each retargeted assertion was
verified to still fail when its step file is blanked, so none was
weakened into vacuity.

Three assertions in verify-mvp-uat were genuinely red. Three more were
worse than red — passing for the wrong reason:
- quick-commit-boundary and worktree-cleanup anchored on indexOf('Step
  5.6'), which matched a later cross-reference and sliced 16069 chars
  that coincidentally held the asserted substrings. Replaced with an
  expandWorkflowSections helper that splices step content back in place.
- phase6-review-capabilities lost its end boundary and widened to EOF.
- playwright-ui-verify matched 'UI' in an unrelated bullet and 'fall
  back' in a subagent-dispatch line after the real content moved.

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

* chore(#2994): fragmentize code-review and complete-milestone, admit three atoms

Add dedicated cmdInitCodeReview and cmdInitCompleteMilestone entry points
alongside the shared generic ones rather than modifying them — init.phase-op
and init.manager carry a CRITICAL blast radius (179 dependents, 24
processes) and stay byte-identical for their other callers.

Admit flag:--fix, state:fallow-enabled and state:git-create-tag, each with
a consuming section and a fact its own entry point computes.

Both sections had the resolver-in-body hazard: the fallow config-gate and
the git.create_tag check each sat inside the very block being gated, so
gating would have disabled the resolver that decides the gate. Both are
hoisted into init and the bodies now consume the resolved fact.

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

* test(#2994): retarget code-review and milestone drift guards, fix two red tests

Retarget guards that asserted on content moved into steps/, proving
non-vacuity by blanking each step file and confirming failure.

Also fixes two genuinely red tests found while working, per the no-defer
rule:
- workflow-fragments' frozen-vocabulary lock was missing
  state:ui-phase-active, so commit 7ef7f8336 shipped red. Lint and build
  both passed over it, which is why neither is sufficient verification.
- code-review's quick.md capability-hook assertion carried a stale
  delimiter after the 18ff35d20 extraction.

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

* chore(#2994): fragmentize autonomous.md and admit state:plan-strategy-converge

Five sections share one atom, the pattern plan-phase already uses for
flag:--research-phase. The atom folds --converge OR --cross-ai into a
single boolean in cmdInitAutonomous so the grammar stays operator-free.

cmdInitAutonomous is additive; init.milestone-op, init.manager and
init.phase-op are untouched and still consumed. The $PLAN_STRATEGY bash
resolver is deliberately retained — ungated local-planning bullets still
read it, so the init-side fact supplements it rather than replacing it.

converge-fail-fast required splitting one bash fence so the always-run
CONVERGENCE_ARGS construction stays outside the marker. All three
flag-absent fallbacks were left outside their markers.

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

* chore(#2994): fragmentize review and discuss-phase-assumptions

Admit state:reviewer-instances-configured (two peripheral notes share it;
the core reviewer-lane dispatch stays unmarked — it is the workflow's
primary always-evaluated logic, not an optional branch) and
state:auto-advance-active, which folds --auto OR two config keys into one
boolean so the grammar stays operator-free.

discuss-phase-assumptions was the highest-risk edit in this PR. Its
auto_advance step is a full if/elif/else; gating it whole would have
deleted the flag-absent fallback needed exactly when --auto is off. Split
verified exact: resolvers 636-651 and the 'End here' fallback 668-669 both
stay outside the marker; only 653-667 is gated.

Adds emitted-drift acks for the two files that grew — review.md (+55 B)
and autonomous.md (+737 B from 80799211c, which had none and would have
red-gated the push.

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

* chore(#2994): fragmentize docs-update, update, transition and new-milestone Part A

Completes the 13-workflow rollout. Three of these had no init call at all
and gained a dedicated entry point plus their first gsd_run query line.

Admits state:is-monorepo and adds state:next-channel, state:workstream-active
and state:flat-mode. Vocabulary 26 -> 30 atoms.

Part A of new-milestone applies when NO workstream is active — the negation
of state:workstream-active. Rather than teach the grammar negation, which is
the Greenspun drift the frozen list exists to prevent, it gets a separate
positively-phrased atom whose fact is the inverse. Part B, which always runs,
stays outside the marker.

flag:--verify-only is deliberately NOT admitted: docs-update has no
contiguous purely-additive region for it, and an atom without a consuming
section is dead vocabulary. Evidence recorded in the slice report.

update.md reuses its existing resolved $GSD_TOOLS rather than prepending the
canonical preamble, which would have clobbered it.

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

* fix(#2994): stop automated-ui-verification re-resolving its own gate, retire dead vocabulary

Two defects the new tests caught.

The automated-ui-verification step re-ran gsd_run loop render-hooks and
recomputed UI_PHASE_ACTIVE inside a body that is only read when that fact
is already true — the circular self-disabling pattern this design forbids,
introduced by 3c654b168. cmdInitVerifyWork now exposes ui_phase_active and
the step consumes it. Its launcher preamble goes too: no gsd_run remains.
The Playwright-MCP check stays as prose — that is live session state.

Dead vocabulary predating this PR: flag:--full and state:needs-codebase-map
were admitted with a gate-1 claim that never materialized. flag:--full is
removed, redundant once quick folds it into discuss/research/validate.
state:needs-codebase-map gets the real consumer it always lacked, gating
new-project's codebase-map offer. Vocabulary 30 -> 29, and no atom is now
without a consuming section.

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

* test(#2994): add the atom-admission, inversion and resolver-hoist gates

The two existing parity guards prove vocabulary/predicate symmetry but
never that a fact is computed — an atom no cmdInit* assembles evaluates
false forever. These close that hole:

- per-atom satisfiability for all 29 atoms, plus an anti-vacuity assertion
  so the loop cannot silently cover zero atoms
- dead-vocabulary check against the shipped manifest
- inversion guard: the flag-absent fallbacks in discuss-phase-assumptions
  and verify-work must stay outside their markers
- data-driven resolver-hoist guard over the shipped manifest, so a future
  extraction cannot reintroduce the circular class
- compound-fold coverage (--full, --cross-ai, --rc, config-only --auto)
- null-vs-[] degraded/computed distinction, and flag value shapes

Also repairs the frozen-vocabulary lock, which was stale and red for the
seven atoms earlier commits on this branch shipped.

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

* docs(#2994): add changeset for the fragment-model rollout

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

* test(#2994): cite the issue on the two new allow-test-rule exemptions

ADR-456 requires an issue ref on the same line as the annotation.

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

* docs(#2994): correct the atom-count claims after retiring flag:--full

The vocabulary doc comments still said 30 entries; it is 29 since
flag:--full was removed as dead vocabulary.

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

* fix(#2994): dedupe the phase-fallback block and harden --ws parsing

Review findings.

MAJOR: the three new init entry points each pasted a verbatim copy of the
guardedFindPhase/guardedGetRoadmapPhase fallback, taking the repo from four
copies to seven — DEFECT.GENERATIVE-FIX. Extracted applyRoadmapFallback and
folded six of the seven; each call site keeps its own field-set via a
closure. Duplication removed rather than papered over with a parity test.
cmdInitPhaseOp stays out: its fallback omits has_reviews, so it is not a
byte-identical copy, and it is CRITICAL-radius.

LOW, pre-existing: GSD_WS captured [^[:space:]]+ and expands unquoted, so a
workstream name holding glob metacharacters would expand against the
filesystem. Narrowed to [A-Za-z0-9._-]+. The unquoted expansion is kept —
it must word-split into two args and vanish when empty.

Also restores the vocabulary ordering convention, and fixes a masked test
bug the mandated run surfaced: the flag-forwarding guard checked only the
first init line per workflow, but new-milestone has two, so a real failure
was reporting exit 0.

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

* fix(#2994): drop the stale new-milestone emitted-drift ack

new-milestone.md was acked for a +406 B growth measured against an
intermediate commit. Net against origin/next it SHRANK by 8 bytes, so
nothing needed the ack and it explained nothing — which the differential
attribution check reports as a stale acknowledgment, not a pass.

update.md's entry stays: it genuinely grew +703 B.

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

* fix(#2994): resolve the 15 failures from the full matrix run

All 15 were real and identical on both lanes.

REAL REGRESSION: autonomous.md hit 41479 chars against the #2196 guard's
40960 cap — a CHARS cap distinct from the LARGE tier byte cap, which the
five section stubs pushed it over. Extracted the 3a.5 UI Design Contract
body to references/; now 39968 chars, and the file nets -795 B vs base, so
its growth ack is deleted rather than left stale.

REAL DEFECT: docs referenced /gsd-transition, which is not a live
registered command. Reworded.

STALE FIXTURE: the emission byte-identity test hardcoded two marked
workflows; this branch legitimately marks fifteen. Fixture corrected — the
source was right.

The rest were drift guards over the eight workflows the earlier sweep did
not cover, retargeted at where the content now lives with non-vacuity
proven by blanking each step file and confirming failure. The GSD_WS
forwarding guard was checked as a possible real break and is not one: the
charclass narrowing is intact and forwarding works end to end.

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

* fix(#2994): drop the ack for a newly-added reference file

A new file's emitted ripple is attributable to the diff that adds it, so
the acknowledgment explained nothing and the differential check reports it
as stale. Removing the last entry removes the fragment — an empty one
signals nothing.

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

* fix(#2994): retarget the UI-contract guards and clear two transitive advisories

The §3a.5 extraction that brought autonomous.md under the #2196 char cap
moved its body to references/autonomous-ui-design-contract.md, so ten
guards in autonomous-ui-steps and check-ui-safety-gate were asserting it
against the host. Retargeted via a combined read, each proven non-vacuous
by blanking the reference file and confirming failure.

This class had already bitten twice on this branch because each sweep was
scoped to the workflows touched at that moment, so this one was
exhaustive: ~70 test files across all 13 workflows, zero further broken or
vacuous assertions found.

Also clears two high transitive advisories the matrix flagged on one lane
— fast-uri GHSA-7p8r-x3mc-p8w7 and three ip-address SSRF/trust-boundary
issues. Both pre-date this branch: package-lock.json was untouched until
now, so the production tree was byte-identical to the base. Lockfile-only,
package.json unchanged, verified against a real npm ci install.

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

* chore(#2994): backfill changeset pr number to 3030

---------

Co-authored-by: sim <sim@local>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 19:59:58 -04:00

1154 lines
47 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 { selectSections } = require('../gsd-core/bin/lib/section-manifest.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');
/** Minimal InvocationFacts factory for the real-plan-phase.md D4 test below. */
function facts(overrides) {
return { flags: new Set(), phaseNumber: null, hasPriorPhases: false, ...overrides };
}
// ─── 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));
});
});
// ─── #2993 (epic #1671 Phase 6.2): real plan-phase.md — C1/D1/D4/D5 ───────
describe('real plan-phase.md (#2993)', () => {
const PLAN_PHASE_PATH = path.join(__dirname, '..', 'gsd-core', 'workflows', 'plan-phase.md');
const STEPS_DIR = path.join(__dirname, '..', 'gsd-core', 'workflows', 'plan-phase', 'steps');
// The 6 sections the #2993 design survey names, in document order.
const EXPECTED_SECTIONS = Object.freeze([
{ id: 'reviews-prerequisite', when: 'flag:--reviews' },
{ id: 'prd-express-gate', when: 'flag:--prd' },
{ id: 'adr-ingest-express-path', when: 'flag:--ingest' },
{ id: 'research-only-modifiers', when: 'flag:--research-phase' },
{ id: 'research-only-early-exit', when: 'flag:--research-phase' },
{ id: 'chunked-planning-mode', when: 'state:chunked-mode' },
]);
test('parsesExactlySixSectionsInDocumentOrder (row C1)', () => {
const source = fs.readFileSync(PLAN_PHASE_PATH, 'utf8');
const sections = parseWorkflowSections(source, PLAN_PHASE_PATH);
const explicitSections = sections.filter((s) => s.explicit);
assert.deepEqual(
explicitSections.map((s) => ({ id: s.id, when: s.when })),
[...EXPECTED_SECTIONS],
);
});
test('parsesAndRendersWithoutThrowingAndStripsEveryMarker', () => {
const source = fs.readFileSync(PLAN_PHASE_PATH, 'utf8');
const composed = composeWorkflow(source, { sourcePath: PLAN_PHASE_PATH });
assert.equal(composed.includes('gsd:section'), false);
assert.ok(Buffer.byteLength(composed, 'utf8') < Buffer.byteLength(source, 'utf8'));
});
test('everyExtractedStepFileExistsIsNonEmptyAndTheHostNoLongerCarriesItsBody (row D1)', () => {
// Behavioral, never a source-grep substring match: for each of the 6
// sections, the step file on disk (1) exists, (2) is non-empty, and (3)
// is EXACTLY the section's own parsed body (the bytes the marker pair
// wraps in the host) is a stub/reference line, not a re-paste of the
// step file's content — the host's marker body and the step file's
// content are two DIFFERENT, disjoint pieces of text after the move.
const source = fs.readFileSync(PLAN_PHASE_PATH, 'utf8');
const sections = parseWorkflowSections(source, PLAN_PHASE_PATH);
const byId = new Map(sections.filter((s) => s.explicit).map((s) => [s.id, s]));
for (const { id } of EXPECTED_SECTIONS) {
const stepPath = path.join(STEPS_DIR, `${id}.md`);
assert.ok(fs.existsSync(stepPath), `expected step file to exist: ${stepPath}`);
const stepContent = fs.readFileSync(stepPath, 'utf8');
assert.ok(stepContent.length > 0, `expected non-empty step file: ${stepPath}`);
const hostSection = byId.get(id);
assert.ok(hostSection, `expected an explicit host section for id="${id}"`);
// The host's marker body is a short stub (the conditional read-and-execute
// instruction), never the step file's own moved content — proves the
// body actually left the host rather than being duplicated in place.
assert.ok(
hostSection.body.length < stepContent.length,
`expected host stub body for "${id}" to be shorter than the extracted step file`,
);
assert.equal(
hostSection.body.includes(stepContent.trim()),
false,
`host stub body for "${id}" must not still contain the moved step file's content verbatim`,
);
}
});
test('prdExpressPathIsReadOnlyWhenPrdSectionIsIncluded (row D4)', () => {
// prd-express-path.md was previously read unconditionally; the #2993
// wrapper (prd-express-gate, when="flag:--prd") is what now actually
// gates it. Prove the gate via the real evaluator: absent --prd excludes
// prd-express-gate; present --prd includes it.
const source = fs.readFileSync(PLAN_PHASE_PATH, 'utf8');
const sections = parseWorkflowSections(source, PLAN_PHASE_PATH).filter((s) => s.explicit);
const withoutPrd = selectSections(sections, facts({}));
assert.ok(withoutPrd.excluded.includes('prd-express-gate'), 'prd-express-gate must be excluded when --prd is absent');
assert.ok(!withoutPrd.included.includes('prd-express-gate'));
const withPrd = selectSections(sections, facts({ flags: new Set(['--prd']) }));
assert.ok(withPrd.included.includes('prd-express-gate'), 'prd-express-gate must be included when --prd is present');
assert.ok(!withPrd.excluded.includes('prd-express-gate'));
// The host's marker body is a stub that reads plan-phase/steps/prd-express-gate.md
// ONLY when the section is included; THAT step file (nested, one hop
// further — the reachability shape gen-section-manifest.cjs's own
// "nested step reference" precedent covers) is what references
// prd-express-path.md — proving the express-path read is reachable only
// through the now-conditional wrapper, never as a second, independent
// unconditional read site elsewhere in the host.
const gateSection = sections.find((s) => s.id === 'prd-express-gate');
assert.ok(gateSection.body.includes('prd-express-gate.md'), 'prd-express-gate\'s host stub must read its own step file');
const gateStepContent = fs.readFileSync(path.join(STEPS_DIR, 'prd-express-gate.md'), 'utf8');
assert.ok(
gateStepContent.includes('prd-express-path.md'),
'prd-express-gate.md must be the (nested) step that references prd-express-path.md',
);
});
test('skipIfProseAppearsExactlyOnceAcrossHostAndStepFile (row D5)', () => {
// For each section, the "Skip if:" gating prose must live in exactly ONE
// place — either the host stub or the step file — never duplicated in
// both after the move.
const source = fs.readFileSync(PLAN_PHASE_PATH, 'utf8');
const sections = parseWorkflowSections(source, PLAN_PHASE_PATH);
const byId = new Map(sections.filter((s) => s.explicit).map((s) => [s.id, s]));
for (const { id } of EXPECTED_SECTIONS) {
const stepContent = fs.readFileSync(path.join(STEPS_DIR, `${id}.md`), 'utf8');
const hostBody = byId.get(id).body;
const hostHasSkipIf = /Skip if:/.test(hostBody);
const stepHasSkipIf = /Skip if:/.test(stepContent);
assert.notEqual(
hostHasSkipIf && stepHasSkipIf,
true,
`"Skip if:" prose for "${id}" must not appear in BOTH the host stub and the step file`,
);
}
});
});
// ─── 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.
//
// #2993 (epic #1671 Phase 6.2, matrix row A3) widened this lock 14 -> 19:
// flag:--ingest, flag:--prd, flag:--research-phase, flag:--reviews,
// state:chunked-mode. #2994 (epic #1671 Phase 6.3) widened it 19 -> 20
// (state:ui-phase-active, verify-work.md), then a further #2994 amendment
// widened it 20 -> 23 (flag:--fix, state:fallow-enabled,
// state:git-create-tag, fragmentizing code-review.md and
// complete-milestone.md), then further #2994 amendments widened it to 30
// (autonomous.md, review.md, discuss-phase-assumptions.md, docs-update.md,
// update.md, transition.md, new-milestone.md — see
// docs/reference/workflow-fragments.md's own widening history). A final
// #2994 dead-vocabulary cleanup then narrowed it 30 -> 29: `flag:--full`
// was removed (never consumed by any `when=` marker — `quick.md` folds
// `--full` into `--discuss`/`--research`/`--validate` before evaluation),
// and `state:needs-codebase-map` gained its first real consumer
// (`new-project.md`'s `codebase-map-offer` section). This is the row the
// lock exists to force — a deliberate, coordinated update, never a
// silent drift (Greenspun's Tenth Rule / ADR-1671:69).
assert.equal(Object.isFrozen(WHEN_VOCABULARY), true);
assert.deepEqual(
[...WHEN_VOCABULARY].sort(),
[
'always',
'flag:--auto',
'flag:--discuss',
'flag:--fix',
'flag:--forensic',
'flag:--ingest',
'flag:--prd',
'flag:--research',
'flag:--research-phase',
'flag:--reset-phase-numbers',
'flag:--reviews',
'flag:--validate',
'flag:--wave',
'state:auto-advance-active',
'state:chunked-mode',
'state:fallow-enabled',
'state:flat-mode',
'state:gap-closure-phase',
'state:git-create-tag',
'state:has-prior-phases',
'state:is-monorepo',
'state:needs-codebase-map',
'state:next-channel',
'state:phase-mvp-mode',
'state:plan-strategy-converge',
'state:reviewer-instances-configured',
'state:ui-phase-active',
'state:workstream-active',
'state:worktrees-enabled',
],
);
});
});
// ─── A further #2994 widening (epic #1671 Phase 6.3): code-review.md /
// complete-milestone.md (3 atoms) ────────────────────────────────────────
describe('widened when= vocabulary (#2994 code-review/complete-milestone)', () => {
const NET_NEW_ATOMS_2994B = Object.freeze([
'flag:--fix',
'state:fallow-enabled',
'state:git-create-tag',
]);
test('everyNetNewAtomIsInWhenVocabulary', () => {
for (const atom of NET_NEW_ATOMS_2994B) {
assert.ok(WHEN_VOCABULARY.includes(atom), `expected "${atom}" in WHEN_VOCABULARY`);
}
});
test('acceptsEveryWidenedAtom', () => {
for (const when of NET_NEW_ATOMS_2994B) {
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');
}
});
});
// ─── #2993 (epic #1671 Phase 6.2): widened when= vocabulary (19 atoms) ─────
// 50-test-matrix.md rows A1/A6.
describe('widened when= vocabulary (#2993)', () => {
// The 5 net-new atoms shipped by #2993, fragmentizing plan-phase.md.
const NET_NEW_ATOMS_2993 = Object.freeze([
'flag:--ingest',
'flag:--prd',
'flag:--research-phase',
'flag:--reviews',
'state:chunked-mode',
]);
test('everyNetNewAtomIsInWhenVocabulary', () => {
for (const atom of NET_NEW_ATOMS_2993) {
assert.ok(WHEN_VOCABULARY.includes(atom), `expected "${atom}" in WHEN_VOCABULARY`);
}
});
test('acceptsEveryWidenedAtom (row A1)', () => {
for (const when of NET_NEW_ATOMS_2993) {
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('researchPhaseAndResearchAreDistinctAtomsAtTheParserLevel (row A6)', () => {
// flag:--research-phase (net-new, #2993) and flag:--research (pre-existing,
// #2992) are DISTINCT vocabulary entries — the parser must accept both,
// as different `when` values, on sections that sit side by side, and must
// never conflate one for the other (e.g. via prefix-matching or a shared
// token derivation). See section-manifest.test.cjs for the predicate-level
// half of this guard (no aliasing at evaluation time).
const source = doc(
'<!-- gsd:section id="research-section" when="flag:--research" -->',
'researchBody',
'<!-- /gsd:section -->',
'<!-- gsd:section id="research-phase-section" when="flag:--research-phase" -->',
'researchPhaseBody',
'<!-- /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: 'research-section', when: 'flag:--research' },
{ id: 'research-phase-section', when: 'flag:--research-phase' },
],
);
});
});
// ─── #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).
//
// `flag:--full` was ONE of the original 10 (#2992) but a later #2994
// dead-vocabulary cleanup removed it from WHEN_VOCABULARY entirely — it
// never gained a consuming `when=` marker (`quick.md` folds `--full` into
// `--discuss`/`--research`/`--validate` before evaluation, so no section
// ever gates on the raw flag). Dropped here too so this list stays a
// faithful subset of the LIVE frozen export (the parser now throws
// REASON.UNKNOWN_WHEN for `when="flag:--full"`, which `acceptsEveryWidenedAtom`
// below would otherwise incorrectly assert acceptance for).
const NET_NEW_ATOMS = Object.freeze([
'flag:--auto',
'flag:--discuss',
'flag:--forensic',
'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');
});
});