'use strict'; // allow-test-rule: source-text-is-the-product — noSectionMarkerLeaksIntoEmittedArtifacts (#2930) // asserts on the literal bytes of an EMITTED install artifact, which IS the // deployed contract (a leaked `gsd:section` marker byte would ship to every user). This // mirrors the test-matrix's own row-34/35 exemption from the "no source-grep" rule // (50-test-matrix.md "No source-grep" note) — the ESLint rule itself only fires on // readFileSync of a .cjs/.js/.ts SOURCE path, never on an installed .md artifact, so this // annotation is documentation of intent, not a required suppression. /** * workflow-fragments-emission.install.test.cjs — 50-test-matrix.md rows 32-36 * (issue #2930, epic #1671 Phase 3). * * Real spawn-install coverage for `composeWorkflow`'s wiring into * `bin/install.js`'s `copyWithPathReplacement` (ADR-1671 "Architecture and * contracts": an engine-direct assertion is false-green for install * behavior — only a real spawned installer proves bytes actually reach * disk). The pure parser/composer itself is covered by * tests/workflow-fragments.test.cjs (unit, rows 1-29/37) and * tests/workflow-fragments.property.test.cjs (prop, rows 30-31). * * Each test builds and tears down its own tmp fixture(s) inline (no shared * `before()` install cache) — independence per matrix row 38. * * ── The overlay technique (rows 33/36) ─────────────────────────────────── * * Rows 33 and 36 need a spawned `bin/install.js` that reads a DIFFERENT * `gsd-core/workflows/execute-phase.md` (malformed, row 36) or a different * `gsd-core/bin/lib/workflow-fragments.cjs` (stubbed to identity, row 33) * than this checkout's real files, without paying to copy the ~400 MB * repository (mostly node_modules) for every run. `buildOverlayRepo` / * `linkOrCopyFile` now live in `./helpers/overlay-repo.cjs` (extracted, * #2933, shared with `tests/fragment-single-edit-propagation.install.test.cjs` * so the mechanism has exactly ONE implementation) — see that file's own doc * comment for the hard-link-mirror mechanism, the Dirent `isFile()`/ * `isDirectory()` quirks it works around, the EXDEV/EPERM copy fallback, and * why only `node_modules`/`.git` are symlinked at the top level. Every * overlay-spawned installer below still runs with `--preserve-symlinks * --preserve-symlinks-main` as a defensive belt: with an all-hardlink leaf * layout this checkout does not currently NEED symlink-preservation for * correctness, but the flag is free insurance against a future install.js * change that resolves a node_modules package by real path. */ const { test } = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const os = require('node:os'); const path = require('node:path'); const crypto = require('node:crypto'); const { runNode } = require('./helpers/process-seam.cjs'); const { cleanup } = require('./helpers.cjs'); const { RUNTIME_META, runMinimalInstall, installerEnv } = require('./helpers/install-shared.cjs'); const { buildOverlayRepo } = require('./helpers/overlay-repo.cjs'); const { executionContextRefs } = require('../scripts/command-contract-helpers.cjs'); const { composeWorkflow } = require('../gsd-core/bin/lib/workflow-fragments.cjs'); const REPO_ROOT = path.join(__dirname, '..'); // #3145: class-norm timeout, not a per-suite value — see helpers/timeouts.cjs. const { INSTALL_TIMEOUT_MS } = require('./helpers/timeouts.cjs'); const PILOT_REL = path.join('gsd-core', 'workflows', 'execute-phase.md'); const PILOT_PATH = path.join(REPO_ROOT, PILOT_REL); // plan-phase.md was the original #2930 pilot but was reverted to unmarked // (chore/2930 retarget: it sits 36 B under the ADR-857 Phase-6 PRE_PHASE6 // gate and cannot absorb marker overhead) — it was a genuinely unmarked // file again, so row 33 used it instead of plan-phase.md. #2993 (epic #1671 // Phase 6.2) now marks plan-phase.md itself (6 sections, the fragmentization // this change ships), so it is no longer a valid "genuinely unmarked" fixture // either — retargeted a second time to discuss-phase.md, which carries no // gsd:section markers as of this change. const UNMARKED_REL = path.join('gsd-core', 'workflows', 'discuss-phase.md'); const RUNTIMES = Object.keys(RUNTIME_META); // ─── Overlay-repo builder (rows 33/36) — see ./helpers/overlay-repo.cjs ──── /** Spawn a (possibly overlaid) installScript at global scope. Does NOT * assert success — callers decide (row 36 expects failure). */ function spawnGlobalInstall(installScript, runtime, extraArgs = []) { const root = fs.mkdtempSync(path.join(os.tmpdir(), `gsd-2930-dest-${runtime}-`)); // #3547 — same real config-home shape as runMinimalInstall: the strict // / subdirectory, not the collapsed . The size // comparisons below normalize each side's own root out of the text; a // collapsed stub against a real-shape install bakes in a `.claude`-style // suffix delta that has nothing to do with the compose wiring under test. const configDir = path.join(root, RUNTIME_META[runtime].globalSuffix); const args = [ '--preserve-symlinks', '--preserve-symlinks-main', installScript, `--${runtime}`, '--global', '--config-dir', configDir, ...extraArgs, ]; const seamResult = runNode(args, { cwd: root, env: installerEnv({ HOME: root, USERPROFILE: root }), timeoutMs: INSTALL_TIMEOUT_MS, }); const result = { status: seamResult.exitCode, stdout: seamResult.stdout, stderr: seamResult.stderr }; return { result, configDir, root }; } /** Convert native path separators to POSIX forward slashes, unconditionally * (never gate on `path.sep` — CONTEXT.md's path-separator-normalization * rule). Windows installs embed the SAME root in more than one spelling: * the `@`-ref / pathPrefix rewrites always emit posix-normalized * forward-slash paths, while other embedded content can still carry the * native backslash spelling. `root` itself (from `fs.mkdtempSync`) is a * native-separator string, so comparing it against text verbatim only * matches ONE of those spellings. */ function toPosixSlashes(value) { return value.replace(/\\/g, '/'); } /** Strip an install's own absolute root out of emitted text so two installs * under DIFFERENT temp roots (different lengths, different runtime-name * prefixes) can be compared byte-for-byte. Normalizes BOTH the text and the * root to forward-slash spelling first, so every embedded spelling of the * root collapses onto the SAME placeholder — leaving the comparison * measuring only composeWorkflow's own contribution. */ function stripRoot(text, root) { return toPosixSlashes(text).split(toPosixSlashes(root)).join(''); } // ─── Row 32: emitted execute-phase.md shrinks by exactly the marker bytes ──── // // Defect found and fixed inline while verifying (chore/2930 review; not one // of the five assigned findings, but discovered incidentally): this test // previously compared the RAW `composeWorkflow(source)` byte length directly // against `fs.statSync(emittedPath).size`. That equality only holds when the // OTHER rewrites `copyWithPathReplacement` also runs (the `~/.claude/` -> // `pathPrefix` substitution, attribution stamping, per-runtime converters) // happen to be byte-neutral — which they are NOT here: `runMinimalInstall` // passes `--config-dir root` with `HOME=root` (root IS the target, not // `root/.claude`), so `computePathPrefix` degenerates to the short literal // `$HOME/` instead of the real-world `$HOME/.claude/`, shrinking the // installed `~/.claude/` references by additional bytes unrelated to // composeWorkflow. Proven with a real spawned install and reverted to // confirm this reproduces on the UNMODIFIED tree, before this change. Fixed // by isolating composeWorkflow's OWN contribution the same way row 33 // already does: compare two REAL installs of the pilot workflow, one via // this checkout's real composeWorkflow and one via an identity-stubbed // composeWorkflow, and assert the size DELTA equals exactly the marker // bytes stripped — never an absolute emitted byte count, which conflates // unrelated rewrites this module does not own. // // A second, independent contaminant surfaced fixing the first one: opencode // embeds the install's own absolute configDir path into execute-phase.md // content (same fact row 33/atRefContractStillResolvesAfterComposition // documents for SKILL.md), and `runMinimalInstall`'s temp-dir prefix // (`gsd---`) is a DIFFERENT length than // `spawnGlobalInstall`'s (`gsd-2930-dest--`) — so comparing RAW // file sizes between the two installs bakes in a root-path-length delta // that has nothing to do with composeWorkflow. Normalize each side's own // root out of the text before measuring, exactly as row 33 already does. test('emittedWorkflowShrinksByMarkerBytesForEveryRuntime', (t) => { const source = fs.readFileSync(PILOT_PATH, 'utf8'); const composed = composeWorkflow(source, { sourcePath: PILOT_PATH }); const sourceBytes = Buffer.byteLength(source, 'utf8'); const composedBytes = Buffer.byteLength(composed, 'utf8'); const expectedMarkerBytes = sourceBytes - composedBytes; assert.ok( expectedMarkerBytes > 0, 'sanity: the pilot workflow must actually carry gsd:section markers to strip', ); const identityStubRepo = buildOverlayRepo({ 'gsd-core/bin/lib/workflow-fragments.cjs': 'module.exports = { composeWorkflow: (c) => c };\n', }); t.after(() => cleanup(identityStubRepo)); for (const runtime of RUNTIMES) { // Belt-and-braces cleanup: t.after() is the failure-path safety net (a // thrown assertion still tears the temp install dirs down when the test // returns), but t.after() alone defers EVERY registered cleanup across // all ~18 runtimes until the whole test finishes, so up to 36 full // install trees would coexist on disk at once. The eager cleanup() calls // below bound peak disk to one iteration's trees on the success path; // t.after() still fires afterward as a no-op (cleanup is idempotent on // an already-removed path — see helpers.cjs). const real = runMinimalInstall({ runtime, scope: 'global' }); t.after(() => cleanup(real.root)); const stub = spawnGlobalInstall(path.join(identityStubRepo, 'bin', 'install.js'), runtime); t.after(() => cleanup(stub.root)); assert.equal( stub.result.status, 0, `${runtime}: identity-stub install must succeed\nstderr: ${stub.result.stderr}`, ); const realPath = path.join(real.configDir, PILOT_REL); const stubPath = path.join(stub.configDir, PILOT_REL); assert.ok(fs.existsSync(realPath), `${runtime}: real install is missing execute-phase.md`); assert.ok(fs.existsSync(stubPath), `${runtime}: identity-stub install is missing execute-phase.md`); const realText = stripRoot(fs.readFileSync(realPath, 'utf8'), real.root); const stubText = stripRoot(fs.readFileSync(stubPath, 'utf8'), stub.root); const realBytes = Buffer.byteLength(realText, 'utf8'); const stubBytes = Buffer.byteLength(stubText, 'utf8'); assert.equal( stubBytes - realBytes, expectedMarkerBytes, `${runtime}: emitted size delta (stub ${stubBytes} - real ${realBytes}, root-normalized) must equal exactly the marker bytes stripped (${expectedMarkerBytes})`, ); cleanup(real.root); cleanup(stub.root); } }); // ─── Row 33: an unmarked workflow emits byte-identical for every runtime ── // // "Byte-identical" here means identical to what the SAME runtime's install // pipeline would emit WITHOUT the #2930 composeWorkflow wiring — not // necessarily identical to the raw repo source, since path-prefix rewrites, // attribution stamping, and per-runtime .md converters already ran before // this change and still run today. Proven empirically per runtime by // comparing two REAL installs of the SAME unmarked file: one through this // checkout's real composeWorkflow, one through an overlay whose // gsd-core/bin/lib/workflow-fragments.cjs is stubbed to plain identity — any // difference is attributable ONLY to the compose wiring, never to an // unrelated converter (which fires identically on both sides). test('unmarkedWorkflowEmitsByteIdenticalForEveryRuntime', (t) => { const identityStubRepo = buildOverlayRepo({ 'gsd-core/bin/lib/workflow-fragments.cjs': 'module.exports = { composeWorkflow: (c) => c };\n', }); t.after(() => cleanup(identityStubRepo)); for (const runtime of RUNTIMES) { const real = runMinimalInstall({ runtime, scope: 'global' }); t.after(() => cleanup(real.root)); const stub = spawnGlobalInstall(path.join(identityStubRepo, 'bin', 'install.js'), runtime); t.after(() => cleanup(stub.root)); assert.equal( stub.result.status, 0, `${runtime}: identity-stub install must succeed\nstderr: ${stub.result.stderr}`, ); const realPath = path.join(real.configDir, UNMARKED_REL); const stubPath = path.join(stub.configDir, UNMARKED_REL); assert.ok(fs.existsSync(realPath), `${runtime}: real install is missing discuss-phase.md`); assert.ok(fs.existsSync(stubPath), `${runtime}: stub install is missing discuss-phase.md`); // Normalize each side's own randomly-generated temp root out of the // content before hashing: some runtimes (opencode) embed the // install's own absolute configDir path in execution_context refs, // and the two installs necessarily used DIFFERENT temp roots — an // unnormalized compare would report a spurious mismatch driven by // temp-path length, not by anything composeWorkflow's wiring did. const realText = stripRoot(fs.readFileSync(realPath, 'utf8'), real.root); const stubText = stripRoot(fs.readFileSync(stubPath, 'utf8'), stub.root); assert.equal( Buffer.byteLength(realText, 'utf8'), Buffer.byteLength(stubText, 'utf8'), `${runtime}: discuss-phase.md byte size drifted between real compose and identity-stub compose`, ); const realHash = crypto.createHash('sha256').update(realText).digest('hex'); const stubHash = crypto.createHash('sha256').update(stubText).digest('hex'); assert.equal( realHash, stubHash, `${runtime}: discuss-phase.md content drifted between real compose and identity-stub compose`, ); cleanup(real.root); cleanup(stub.root); } }); // ─── Row 34: no gsd:section marker survives into any emitted artifact ───── test('noSectionMarkerLeaksIntoEmittedArtifacts', (t) => { for (const runtime of RUNTIMES) { const { configDir, root } = runMinimalInstall({ runtime, scope: 'global' }); t.after(() => cleanup(root)); const emittedPath = path.join(configDir, PILOT_REL); assert.ok(fs.existsSync(emittedPath), `${runtime}: emitted execute-phase.md is missing`); const emittedText = fs.readFileSync(emittedPath, 'utf8'); assert.equal( emittedText.includes('gsd:section'), false, `${runtime}: emitted execute-phase.md still contains a gsd:section marker token`, ); cleanup(root); } }); // ─── Row 35: ADR-0002 @-ref contract still resolves after composition ───── // // Two representative runtimes chosen to cover BOTH observed @-ref forms // (empirically confirmed, #2930 dispatch): claude/cursor/codex rewrite to // `@$HOME/...`, while opencode rewrites to a bare `@/...`. // Both installed SKILL.md files themselves pass through composeWorkflow too // (as a no-op, being unmarked) — this proves that pass never corrupts or // relocates the referenced workflow file. /** Detect absoluteness from the token's own shape only — never from * `process.platform` — so the same logic runs identically on every OS. * Covers POSIX (`/...`), Windows drive-letter (`C:/...` or `C:\...`), and * UNC (`\\server\share`) forms. */ function isAbsoluteRefTarget(candidate) { return ( candidate.startsWith('/') || /^[a-zA-Z]:[\\/]/.test(candidate) || candidate.startsWith('\\\\') ); } function resolveExecutionContextRefTarget(token, root) { const withoutAt = token.replace(/^@/, ''); if (isAbsoluteRefTarget(withoutAt)) return withoutAt; // already absolute (opencode form) const stripped = withoutAt.replace(/^(?:~|\$HOME)\//, ''); return path.join(root, stripped); } test('atRefContractStillResolvesAfterComposition', (t) => { for (const runtime of ['claude', 'opencode']) { const { configDir, root } = runMinimalInstall({ runtime, scope: 'global' }); t.after(() => cleanup(root)); const skillPath = path.join(configDir, 'skills', 'gsd-plan-phase', 'SKILL.md'); assert.ok(fs.existsSync(skillPath), `${runtime}: installed gsd-plan-phase SKILL.md is missing`); const skillContent = fs.readFileSync(skillPath, 'utf8'); const refs = executionContextRefs(skillContent); assert.ok(refs.length > 0, `${runtime}: SKILL.md has no execution_context @-refs to check`); for (const { token } of refs) { const target = resolveExecutionContextRefTarget(token, root); assert.ok( fs.existsSync(target), `${runtime}: execution_context @-ref "${token}" resolved to "${target}", which does not exist on disk`, ); } cleanup(root); } }); // ─── FIX 1 (chore/2930 review): composeWorkflow is scoped to gsd-core/workflows/ ── // // copyWithPathReplacement is the emit path for the ENTIRE gsd-core/ tree // (skillSrc = path.join(src, 'gsd-core'), bin/install.js:10806-10809), not // just gsd-core/workflows/. A non-workflow .md elsewhere under gsd-core/ // (e.g. gsd-core/references/) that merely DOCUMENTS the marker syntax with // an unfenced, structurally-invalid example line must never be run through // composeWorkflow — doing so would either throw (breaking install for an // unrelated file class) or silently strip/mis-parse the documentation line. // Proven here by overlaying BOTH a marked workflow (must still compose) and // an EXISTING non-workflow reference doc (buildOverlayRepo can only replace // the content of a real leaf file, not graft in a net-new path — see the // module doc comment's overlay-technique note) rewritten to carry an // intentionally-UNCLOSED marker-shaped line (would throw if composeWorkflow // ever touched it) in the SAME install run. test('nonWorkflowMarkdownWithMarkerShapedLineIsNotComposed', (t) => { const markedWorkflow = '\nbody\n\n'; const nonWorkflowDoc = '# Marker syntax\n\nExample (deliberately unfenced and unclosed to prove non-composition):\n\n\nnever closed on purpose\n'; const NON_WORKFLOW_DOC_REL = path.join('gsd-core', 'references', 'context-budget.md'); const overlayRepo = buildOverlayRepo({ 'gsd-core/workflows/execute-phase.md': markedWorkflow, [NON_WORKFLOW_DOC_REL.split(path.sep).join('/')]: nonWorkflowDoc, }); t.after(() => cleanup(overlayRepo)); const dest = spawnGlobalInstall(path.join(overlayRepo, 'bin', 'install.js'), 'claude'); t.after(() => cleanup(dest.root)); assert.equal( dest.result.status, 0, `install must succeed: a non-workflow doc's marker-shaped line must never reach composeWorkflow\nstderr: ${dest.result.stderr}`, ); const emittedWorkflowPath = path.join(dest.configDir, PILOT_REL); assert.ok(fs.existsSync(emittedWorkflowPath), 'emitted execute-phase.md is missing'); assert.equal( fs.readFileSync(emittedWorkflowPath, 'utf8'), 'body\n', 'gsd-core/workflows/execute-phase.md must still compose (markers stripped)', ); const emittedDocPath = path.join(dest.configDir, NON_WORKFLOW_DOC_REL); assert.ok(fs.existsSync(emittedDocPath), 'emitted context-budget.md is missing'); assert.equal( fs.readFileSync(emittedDocPath, 'utf8'), nonWorkflowDoc, 'a non-workflow .md must pass through composeWorkflow untouched, byte-identical, including its marker-shaped line', ); }); // ─── Row 36: a malformed marker fails install loudly, with no partial emit ─ // ─── Row 63 (50-test-matrix.md, issue #2932 Phase 5): extracting // execute-phase.md's 3 sections must not perturb any OTHER workflow's // emission — independence guard for the CRITICAL blast radius Phase 5's own // design doc calls out. Pure-function check (no spawn needed): composeWorkflow // is a documented no-op for every unmarked file, so any file other than the // one Phase 5 migrates must still compose to itself, byte-identical. ──────── test('leavesUnmarkedWorkflowEmissionByteIdentical', () => { const workflowsDir = path.join(REPO_ROOT, 'gsd-core', 'workflows'); // execute-phase.md (#2932 Phase 5), plan-phase.md (#2993 Phase 6.2), and // the thirteen workflows #2994 (epic #1671 Phase 6.3) fragmentizes onto the // marker grammar are the files this repo has fragmentized with gsd:section // markers — all excluded here since composeWorkflow is deliberately NOT a // no-op for them. This set is DELIBERATELY hardcoded rather than derived // from "does the file contain a marker": deriving it would make the guard // tautological — a marker that LEAKED into an unrelated file via a bad // extraction would just get silently excluded instead of failing the // independence check this test exists to enforce. Update this list by hand // whenever a workflow is legitimately marked. const MARKED_WORKFLOWS = new Set([ 'autonomous.md', 'code-review.md', 'complete-milestone.md', 'discuss-phase-assumptions.md', 'docs-update.md', 'execute-phase.md', 'new-milestone.md', 'new-project.md', 'plan-phase.md', 'progress.md', 'quick.md', 'review.md', 'transition.md', 'update.md', 'verify-work.md', ]); const workflowFiles = fs .readdirSync(workflowsDir, { withFileTypes: true }) .filter((d) => d.isFile() && d.name.endsWith('.md')) .map((d) => d.name); assert.ok(workflowFiles.length > MARKED_WORKFLOWS.size, 'sanity: there must be more than the marked workflows on disk'); let checkedCount = 0; for (const fileName of workflowFiles) { if (MARKED_WORKFLOWS.has(fileName)) continue; const filePath = path.join(workflowsDir, fileName); const source = fs.readFileSync(filePath, 'utf8'); const composed = composeWorkflow(source, { sourcePath: filePath }); assert.equal( composed, source, `${fileName}: emission drifted — a marked workflow's extraction must not touch any other workflow`, ); checkedCount += 1; } assert.equal( checkedCount, workflowFiles.length - MARKED_WORKFLOWS.size, 'every workflow file except the marked ones must have been checked', ); }); test('malformedMarkersFailInstallWithoutPartialEmit', (t) => { const malformed = '\nnever closed\n'; const overlayRepo = buildOverlayRepo({ 'gsd-core/workflows/execute-phase.md': malformed }); t.after(() => cleanup(overlayRepo)); const dest = spawnGlobalInstall(path.join(overlayRepo, 'bin', 'install.js'), 'claude'); t.after(() => cleanup(dest.root)); // stderr text is a child process's rendered prose, not a typed value // this test can assert on across the process boundary (CONTRIBUTING.md // "Prohibited: Raw Text Matching on Test Outputs" — err.reason is only // reachable in-process; see tests/workflow-fragments.test.cjs's REASON // assertions for the in-process equivalent of this same failure mode). // Assert typed, observable facts instead: the install process exits // non-zero, and no output file is written for the file that failed to // compose. assert.notEqual( dest.result.status, 0, `install must fail loudly on a malformed marker, got exit 0\nstdout: ${dest.result.stdout}`, ); const emittedPath = path.join(dest.configDir, PILOT_REL); assert.equal( fs.existsSync(emittedPath), false, 'a half-composed execute-phase.md must never be written when composition throws', ); });