Files
msd-core/tests/edge-probe-spec-phase-contract.test.cjs
sim 7dd9e59f6b test(#3090): stop exempting violations under categories that do not fit
An allow-test-rule annotation citing a category that does not apply is worse
than no annotation, because it reads as reviewed. Eight were confirmed by
reading the assertions each one covered, and auditing the rest found five more
plus one refutation — a converter test whose wording described the wrong
mechanism while the covered assertion genuinely was deployed-text.

The instructive one used the CANONICAL string for the same mistake: STATE.md
command output labelled as a deployed artifact. A canonical string is not
evidence the category fits, which is why normalising strings alone would have
laundered the problem rather than fixed it. Every mapping the audit had inferred
rather than code-verified was spot-checked before rewriting, and the ones that
turned out not to fit were re-annotated rather than relabelled.

Fourteen STATE.md assertions had a typed extractor available all along and now
use it; their annotations came out because nothing needs exempting. Eight
assertions genuinely need a production change first — CLI stdout and stderr with
no structured mode — and are tagged pending-migration-to-typed-ir citing #3090,
which is what that category is for. It had zero real uses before this, while one
file carried a real citation to migration issue #2974 under a non-canonical tag.

Six annotations covered assertions that do no text matching at all. An exemption
for a violation that does not exist is noise that makes the real ones harder to
audit; those are removed.

atomic-write-coverage gains the annotation it always warranted — its own
docstring describes a structural-regression-guard while the file carried none.

Fifty-nine non-canonical strings across roughly thirty files are normalised, and
the allow-test-rule allowlist is regenerated to match. 472 annotations became
463: every one now uses a canonical category, and the two remaining
non-canonical strings are ESLint RuleTester fixtures, not annotations.

Refs #3057

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 17:20:56 -04:00

194 lines
9.3 KiB
JavaScript

// allow-test-rule: source-text-is-the-product — spec-phase.md Step 5.5 is the deployed workflow runtime contract under assertion
// spec-phase.md is the deployed spec workflow contract; these checks lock
// the Step 5.5 wiring so the edge-probe.cjs runtime invocation cannot
// silently rot the way the original plan-phase no-op did (reviewer finding RR-11).
// Assertions scope to the extracted Step 5.5 block to avoid false positives
// from incidental mentions elsewhere in the file.
'use strict';
process.env.GSD_TEST_MODE = '1';
const { test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const SPEC_PHASE_PATH = path.join(__dirname, '..', 'gsd-core', 'workflows', 'spec-phase.md');
function readSpecPhase() {
return fs.readFileSync(SPEC_PHASE_PATH, 'utf8');
}
// Slice the Step 5.5 block: from the "Step 5.5" heading to the next "## " or "Step " heading.
// This scopes assertions to Step 5.5 only, preventing false positives from mentions elsewhere.
function extractStep55Block(content) {
const startIdx = content.indexOf('## Step 5.5');
if (startIdx === -1) {
// Also try without the ## prefix
const altIdx = content.indexOf('Step 5.5');
if (altIdx === -1) return '';
// Find end: next heading starting with ## or Step N (not Step 5.5)
const rest = content.slice(altIdx + 'Step 5.5'.length);
const nextHeading = rest.search(/\n## |\nStep \d/);
if (nextHeading === -1) return content.slice(altIdx);
return content.slice(altIdx, altIdx + 'Step 5.5'.length + nextHeading);
}
const rest = content.slice(startIdx + '## Step 5.5'.length);
const nextHeading = rest.search(/\n## /);
if (nextHeading === -1) return content.slice(startIdx);
return content.slice(startIdx, startIdx + '## Step 5.5'.length + nextHeading);
}
// Test A (RR-11): Step 5.5 resolves and invokes edge-probe.cjs via node.
// MUST FAIL before the RR-04 wire (Step 5.5 is prose-only today — no CLI invocation).
test('RR-11: spec-phase Step 5.5 resolves edge-probe.cjs via path-fallback loop', () => {
const content = readSpecPhase();
const block = extractStep55Block(content);
assert.ok(block.length > 0, 'Step 5.5 block must be extractable from spec-phase.md');
// Assert the path-fallback resolution loop for edge-probe.cjs is present in Step 5.5.
// The token "edge-probe.cjs" must appear inside the block (the artifact being resolved).
assert.match(
block,
/edge-probe\.cjs/,
'Step 5.5 must reference edge-probe.cjs as the artifact being resolved'
);
// Assert node invocation of edge-probe.cjs in Step 5.5.
// Matches: node "$EDGE_PROBE_JS" or node ... edge-probe.cjs
assert.match(
block,
/node\s+["$].*[Ee][Dd][Gg][Ee][-_][Pp][Rr][Oo][Bb][Ee]/,
'Step 5.5 must invoke edge-probe.cjs via node (e.g. node "$EDGE_PROBE_JS" ...)'
);
});
// Test C (RR-11 FUNCTION — the assertion that catches "decorative bash"):
// Token presence is not enough. The invocation is a no-op unless $REQS_JSON is actually
// POPULATED before the engine runs (the original block only mktemp'd it + left a comment,
// so the CLI parsed an empty file). Assert the block (a) writes $REQS_JSON via a redirect,
// (b) does so BEFORE the node invocation, and (c) guards against an empty/invalid file.
test('RR-11 function: Step 5.5 writes $REQS_JSON before invoking, and guards against empty input', () => {
const content = readSpecPhase();
const block = extractStep55Block(content);
assert.ok(block.length > 0, 'Step 5.5 block must be extractable from spec-phase.md');
// (a) A redirect that writes the requirements into $REQS_JSON (e.g. `cat > "$REQS_JSON"`).
const writeIdx = block.search(/>\s*"\$REQS_JSON"/);
assert.ok(
writeIdx !== -1,
'Step 5.5 must WRITE requirements into $REQS_JSON (a redirect like `cat > "$REQS_JSON"`), not just mktemp it — an empty file makes the probe a silent no-op'
);
// (b) The write must precede the node invocation of the engine.
const invokeIdx = block.search(/node\s+["$].*[Ee][Dd][Gg][Ee][-_][Pp][Rr][Oo][Bb][Ee]/);
assert.ok(invokeIdx !== -1, 'Step 5.5 must invoke the engine via node');
assert.ok(
writeIdx < invokeIdx,
'Step 5.5 must populate $REQS_JSON BEFORE invoking edge-probe.cjs (write precedes the node call)'
);
// (c) A guard that refuses to run on an empty/invalid requirements array.
assert.match(
block,
/Array\.isArray|empty\/invalid|empty or invalid|REQS_JSON[^\n]*empty/i,
'Step 5.5 must guard against an empty/invalid $REQS_JSON before invoking (fail loud, not silent no-op)'
);
});
// Test B (RR-11): Step 5.5 has an explicit not-found branch — build:lib or error token.
// MUST FAIL before the RR-04 wire (no not-found handling today).
test('RR-11: spec-phase Step 5.5 has an explicit not-found branch (build:lib or blocking error)', () => {
const content = readSpecPhase();
const block = extractStep55Block(content);
assert.ok(block.length > 0, 'Step 5.5 block must be extractable from spec-phase.md');
// Assert either a build:lib invocation or an explicit "not found" / error message exists.
// This prevents the wire from being added as a silent-skip with no fallback.
assert.match(
block,
/build:lib|not found|ERROR.*edge-probe|edge-probe.*not found/i,
'Step 5.5 must have an explicit not-found branch (build:lib attempt or clear blocking error)'
);
});
// Test D (review High): the build fallback must NEVER run the CONSUMING project's package
// scripts. Every executable `build:lib` invocation must be pinned to the GSD dir with
// `npm --prefix`, and the build must be gated behind a verified GSD source checkout. A bare
// `npm run build:lib` (no --prefix) uses cwd — which, under the git-toplevel fallback, is the
// consumer repo — and would execute its codegen/migrations during a spec workflow.
test('review High: Step 5.5 build:lib is --prefix-pinned to the GSD dir and gated on a source checkout', () => {
const content = readSpecPhase();
const block = extractStep55Block(content);
assert.ok(block.length > 0, 'Step 5.5 block must be extractable from spec-phase.md');
// Collect lines that actually INVOKE npm (trimmed start === "npm"), excluding echo/comment
// mentions (e.g. the error message that quotes `npm run build:lib` for the user).
const buildInvocations = block
.split('\n')
.filter((l) => l.trim().startsWith('npm') && l.includes('build:lib'));
assert.ok(
buildInvocations.length > 0,
'Step 5.5 must contain at least one npm build:lib invocation (the dev-checkout fallback)'
);
for (const line of buildInvocations) {
assert.match(
line,
/npm\s+--prefix\s+"?\$?\{?_?GSD_RT/,
`build:lib must be pinned with \`npm --prefix "$_GSD_RT"\` so it never runs the consuming project's scripts — offending line: ${line.trim()}`
);
}
// The build must be gated behind a verified GSD source checkout (tsconfig.build.json present),
// so it cannot fire inside a plain consumer repo where the artifact merely happens to be absent.
assert.match(
block,
/tsconfig\.build\.json/,
'Step 5.5 must gate the build behind a GSD source checkout (e.g. test -f "$_GSD_RT/tsconfig.build.json")'
);
});
// Test E (review #4 High): the engine's fail-closed exit(2) must NOT be swallowed by command
// substitution. A bare `COVERAGE=$(node "$EDGE_PROBE_JS" ...)` discards the exit status, leaves
// $COVERAGE empty on an invalid-shapes failure, and lets the workflow proceed into prose
// re-derivation — fail-OPEN at the very boundary the engine validation exists to protect.
test('review #4 High: Step 5.5 exit-checks the engine capture and validates the report (no fail-open)', () => {
const content = readSpecPhase();
const block = extractStep55Block(content);
assert.ok(block.length > 0, 'Step 5.5 block must be extractable from spec-phase.md');
// The engine capture must be FATAL — guarded by `if ! COVERAGE=$(node "$EDGE_PROBE_JS" …)`
// (or an explicit exit-status check) that exits non-zero on failure.
assert.match(
block,
/if\s+!\s+COVERAGE=\$\(node\s+"\$EDGE_PROBE_JS"/,
'Step 5.5 must exit-check the engine invocation (e.g. `if ! COVERAGE=$(node "$EDGE_PROBE_JS" …)`) — a bare command substitution swallows the engine exit code and fails open'
);
// And the captured report must be validated as JSON before the resolution loop consumes it
// (guards against an exit-0-but-garbage capture).
assert.match(
block,
/(COVERAGE[\s\S]{0,500}JSON\.parse)|(JSON\.parse[\s\S]{0,500}\$COVERAGE)/,
'Step 5.5 must validate $COVERAGE parses as JSON before use (guard against exit-0-but-malformed output)'
);
});
// Test F (adversarial review): a report with ZERO applicable edges across all requirements is
// the likely-classification-miss fail-open (same shape as an invalid shape yielding applicable:0).
// Step 5.5 must surface it, not silently emit a green empty ## Edge Coverage section.
test('adversarial review: Step 5.5 guards a zero-applicable coverage report', () => {
const content = readSpecPhase();
const block = extractStep55Block(content);
assert.ok(block.length > 0, 'Step 5.5 block must be extractable from spec-phase.md');
assert.match(
block,
/coverage\.applicable/,
'Step 5.5 must read coverage.applicable and guard the zero-applicable case (warn/confirm, not silently proceed)'
);
});