Files
msd-core/tests/capability-probe-fallback.test.cjs
Tom Boucher 03b7125293 enhance(#3909): a probe that could not run no longer asserts a verdict (#3944)
* test(#3909): failing-first suite for the fabricated probe fallbacks

Binds the four fabrication sites found by executing the surfaces (ADR-3889
failure class (c)), each with a positive control so an over-firing fix goes red:

- the blocking api-coverage.verify-pre gate certifying "no external-API
  integration" from a zero-byte phase scope
- the assumption-delta query route scanning an unresolvable phase section as
  the empty string and reporting it as an examined negative
- both capability fragments' probe fallbacks, which append a fabricated
  verdict rather than replacing, and fire on the legitimate exit-1 negative

Verification runs on the remote runner.

Refs #3909

* enhance(#3909): a probe that could not run no longer asserts a verdict

ADR-3889 Phase 5. Four sites turned a failed or unexamined probe into a
confident negative; each now reports what it could not establish.

- check api-coverage.verify-pre: a phase with no plan body and no roadmap
  section ran detection over zero bytes and PASSED the blocking seal gate,
  certifying "no external-API integration" from input it never read. It now
  holds with scope_unavailable. The discriminator is bytes examined, never
  signals found, so a phase whose plans are real and simply carry no API
  vocabulary passes exactly as before.
- query assumption-delta scan: an unresolvable phase section was scanned as
  the empty string and reported as an examined negative. It now returns
  {skipped, reason: phase_unresolved}, still at exit 0 — an ADR-2980 degraded
  result in the payload, leaving the gsd-tools exit projection to P8.
- both capability fragments: `|| echo '{"detected":false}'` appended rather
  than replaced, and fired on the legitimate exit-1 negative, so a correct
  answer and an honest skip both arrived as two concatenated objects. They now
  keep the probe's own payload and manufacture only an explicit
  probe_unavailable skip when the probe produced nothing at all.

Every registered outcome is more restrictive on a blocking gate, so this can
turn a false green red and never a red green.

Docs: FEATURES 156, CONFIGURATION (both keys), references/api-coverage.md
seal-time outcome table, and a new how-to for the reason-code vocabulary.

Verification runs on the remote runner.

Closes #3909

* test(#3909): correct the stale unknown-phase assertion

`unknown phase → detected:false, no throw (graceful)` scanned phase 999
against a two-phase roadmap and asserted `detected === false`. That pinned
the fabrication as intended behavior: the phase does not exist, so the
detector was handed the empty string and its "no core assumption changed"
answer described nothing that was ever read.

It now asserts the skipped-with-reason shape. The graceful-degradation
contract the test was actually protecting — the query succeeds and does not
throw on an unknown phase — is unchanged.

Found by code review, not by the author.

Refs #3909

* docs(#3909): author the FEATURES entry in its generator source

`docs/FEATURES.md` is generated by `scripts/gen-features.cjs` from the
per-feature fragments in `docs/features/`. The API-coverage entry was edited
in the generated file, so the next regeneration silently dropped it.

The text now lives in `docs/features/api-coverage-gate.md` and
`docs/FEATURES.md` is regenerated from it, leaving the shipped file
byte-identical and its content actually derivable.

Caught by `lint:generated-sync`.

Refs #3909

* test(#3909): bind the skip to "not found", and pin the discriminator

The first verification run went red on one case, and the case was wrong
rather than the code.

`getRoadmapPhaseWithFallback` returns `null` for an unknown phase and for a
missing ROADMAP.md, but for a section whose body is whitespace-only it returns
the heading line alone — which is not empty. So a body-less section WAS found,
and reporting `detected:false` over its heading is a real negative, not a
fabrication. The test had assumed the resolver yielded `''` there.

Correcting the test rather than the resolver keeps `skipped` bound to the
distinction the issue asks for — found versus not found — and avoids diverging
`assumption-delta scan` from `roadmap.get-phase`, which the fragment documents
as sharing one resolver.

Also adds the seeded property the test matrix had promised: for any plan body,
the scope read back is whitespace-only exactly when the body was. That pins the
gate's discriminator to bytes examined, so it cannot quietly become "no signals
found", across unicode whitespace and CRLF.

`docs/INVENTORY.md` picks up the reference doc's new seal-time outcome table —
surfaced by the co-change gate, not by a lint failure.

Refs #3909

* chore(#3909): backfill the changeset PR number

Refs #3909

---------

Co-authored-by: sim <sim@local>
2026-08-27 15:50:12 -04:00

291 lines
12 KiB
JavaScript

'use strict';
/**
* The capability fragments' probe fallbacks must be honest (#3909, ADR-3889 P5).
*
* Two capability fragments carry a shell snippet that runs a detector and
* captures its JSON. Those snippets used `… 2>/dev/null || echo '{"detected":false}'`,
* which FABRICATES a negative verdict whenever the probe exits non-zero. That is
* wrong three separate ways, and all three are covered here:
*
* 1. `||` fires on exit 1 — which ADR-3889 P3 made the LEGITIMATE negative —
* so a correct "no integration" answer got a second object appended to it.
* 2. `$( )` captures the whole compound's stdout, so the fallback APPENDS
* rather than replaces: an honest `{"skipped":true}` was immediately
* contradicted by a fabricated `{"detected":false}` in the same string.
* 3. A probe that genuinely could not run produced a clean, confident,
* wrong `detected:false`.
*
* BEHAVIORAL, not source-grep: each test extracts the fragment's own fenced
* bash block, executes it under `bash` with the surrounding contract stubbed
* (`gsd_run`, `PHASE_DIR`, `PHASE`), and asserts on the captured variable.
*/
const { describe, test, afterEach } = 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 { cleanup } = require('./helpers.cjs');
const { runNode, OUTCOME } = require('./helpers/process-seam.cjs');
const { splitLines } = require('../gsd-core/bin/lib/text-lines.cjs');
const REPO_ROOT = path.join(__dirname, '..');
const API_FRAGMENT = path.join(
REPO_ROOT, 'capabilities', 'ai-integration', 'fragments', 'api-coverage-plan-pre.md');
const DELTA_FRAGMENT = path.join(
REPO_ROOT, 'capabilities', 'assumption-delta', 'fragments', 'plan-pre.md');
const TOOLS_PATH = path.join(REPO_ROOT, 'gsd-core', 'bin', 'gsd-tools.cjs');
/**
* Pull the fragment's probe snippet out of its markdown: the first fenced
* ```bash block that assigns `varName`. The block is returned verbatim so the
* test executes exactly the bytes the planner is handed.
*/
function extractProbeBlock(fragmentPath, varName) {
const md = splitLines(fs.readFileSync(fragmentPath, 'utf8'));
const fences = [];
let current = null;
for (const line of md) {
if (current === null) {
if (line.trim() === '```bash') {
current = [];
}
continue;
}
if (line.trim() === '```') {
fences.push(current.join('\n'));
current = null;
continue;
}
current.push(line);
}
const block = fences.find((body) => body.includes(`${varName}=`));
assert.ok(
block,
`${path.basename(fragmentPath)} must contain a fenced bash block assigning ${varName}`,
);
return block;
}
/**
* Run a fragment snippet under bash and return the captured variable's value.
* `prelude` stubs the surrounding workflow contract; `cwd` decides whether the
* detector module is reachable (an unreachable one is how "the probe could not
* launch" is simulated — no chmod, no monkeypatch, just a different cwd).
*/
function runSnippet({ block, varName, prelude, cwd }) {
const script = `set -u\n${prelude}\n${block}\nprintf '%s' "\${${varName}}"\n`;
const scriptFile = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-frag-')), 'run.sh');
fs.writeFileSync(scriptFile, script, 'utf8');
try {
const r = runNode(['-e', `
const { spawnSync } = require('node:child_process');
const r = spawnSync('bash', [process.argv[1]], { cwd: process.argv[2], encoding: 'utf8', timeout: 60000 });
process.stdout.write(r.stdout || '');
`, scriptFile, cwd], { cwd, timeoutMs: 60000 });
assert.strictEqual(r.outcome, OUTCOME.EXITED, `snippet runner outcome: ${r.outcome}`);
return r.stdout;
} finally {
cleanup(path.dirname(scriptFile));
}
}
/** Assert the captured value is exactly ONE JSON object, and return it. */
function parseSingleObject(captured, what) {
assert.notStrictEqual(captured.trim(), '', `${what}: the fragment captured nothing at all`);
let parsed;
try {
parsed = JSON.parse(captured);
} catch (err) {
assert.fail(
`${what}: the fragment produced text that is not a single JSON object — ` +
`a concatenated fallback is exactly this failure. Captured: ${JSON.stringify(captured)} ` +
`(${err.message})`,
);
}
assert.strictEqual(typeof parsed, 'object', `${what}: payload must be an object`);
assert.notStrictEqual(parsed, null, `${what}: payload must not be null`);
return parsed;
}
// ─── api-coverage fragment ────────────────────────────────────────────────────
describe('api-coverage fragment probe fallback is honest (#3909)', () => {
let tmpDir;
afterEach(() => { if (tmpDir) { cleanup(tmpDir); tmpDir = null; } });
// The fragment's snippet builds SCOPE from `${PHASE_DIR}/*-PLAN.md` plus a
// `gsd_run query roadmap.get-phase` call. Both are stubbed so the test
// controls exactly what reaches the detector.
function preludeFor(planBody) {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-apifrag-'));
if (planBody !== null) {
fs.writeFileSync(path.join(tmpDir, '01-PLAN.md'), planBody, 'utf8');
}
return [
`PHASE_DIR=${JSON.stringify(tmpDir)}`,
'PHASE=01',
'gsd_run() { return 0; }',
].join('\n');
}
function capture(planBody, { cwd = REPO_ROOT } = {}) {
return runSnippet({
block: extractProbeBlock(API_FRAGMENT, 'API_COVERAGE_JSON'),
varName: 'API_COVERAGE_JSON',
prelude: preludeFor(planBody),
cwd,
});
}
test('CONTROL: a scope with API vocabulary captures a detected verdict', () => {
const j = parseSingleObject(
capture('# Plan\nIntegrate the Stripe API and wrap its SDK.'), 'detected case');
assert.strictEqual(j.detected, true);
assert.strictEqual(j.skipped, undefined);
});
test('a LEGITIMATE negative (probe exit 1) is captured as ONE valid object', () => {
// Regression: the detector exits 1 for a real negative, so `|| echo …`
// fired on the success path and appended a second object.
const j = parseSingleObject(
capture('# Plan\nRefactor the internal state machine.'), 'legit negative');
assert.strictEqual(j.detected, false, 'a real negative must survive intact');
assert.strictEqual(j.skipped, undefined, 'a real negative is not a skip');
});
test('an HONEST skip (empty scope) is not contradicted by a fabricated verdict', () => {
const j = parseSingleObject(capture(null), 'empty scope');
assert.strictEqual(j.skipped, true, 'an unexamined scope must report skipped');
assert.strictEqual(
j.detected,
undefined,
'the skip must not carry a detected key — that contradiction is the defect',
);
});
test('a probe that CANNOT LAUNCH reports skipped, never detected:false', () => {
// cwd without the module → node fails, stdout empty. The fragment must not
// manufacture a verdict from that.
const away = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-nomodule-'));
try {
const j = parseSingleObject(
capture('# Plan\nIntegrate the Stripe API.', { cwd: away }), 'probe unavailable');
assert.strictEqual(j.skipped, true, 'a probe that could not run must report skipped');
assert.strictEqual(j.reason, 'probe_unavailable');
assert.strictEqual(
j.detected,
undefined,
'asserting detected:false from a probe that never ran is the bug this closes',
);
} finally {
cleanup(away);
}
});
});
// ─── assumption-delta fragment ────────────────────────────────────────────────
describe('assumption-delta fragment probe fallback is honest (#3909)', () => {
let tmpDir;
afterEach(() => { if (tmpDir) { cleanup(tmpDir); tmpDir = null; } });
function projectWithRoadmap(body) {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-deltafrag-'));
fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true });
if (body !== null) {
fs.writeFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), body, 'utf8');
}
return tmpDir;
}
// `gsd_run` is the workflow launcher's shell function; stub it to the real
// CLI so the snippet exercises the genuine query route.
function realGsdRunPrelude(projectDir) {
return [
'PHASE=01',
`gsd_run() { ( cd ${JSON.stringify(projectDir)} && ` +
`node ${JSON.stringify(TOOLS_PATH)} "$@" ); }`,
].join('\n');
}
function capture(prelude) {
return runSnippet({
block: extractProbeBlock(DELTA_FRAGMENT, 'ASSUMPTION_DELTA_JSON'),
varName: 'ASSUMPTION_DELTA_JSON',
prelude,
cwd: REPO_ROOT,
});
}
test('CONTROL: a resolved section with a cue captures a detected verdict', () => {
const dir = projectWithRoadmap(
'# Roadmap\n\n### Phase 01: Auth\n\nAdd a second authentication method.\n');
const j = parseSingleObject(capture(realGsdRunPrelude(dir)), 'detected case');
assert.strictEqual(j.detected, true);
assert.strictEqual(j.skipped, undefined);
});
test('CONTROL: a resolved section with no cue captures ONE valid negative', () => {
const dir = projectWithRoadmap(
'# Roadmap\n\n### Phase 01: Cleanup\n\nRefactor the internal state machine.\n');
const j = parseSingleObject(capture(realGsdRunPrelude(dir)), 'legit negative');
assert.strictEqual(j.detected, false);
assert.strictEqual(j.skipped, undefined);
});
test('an unresolvable phase captures skipped, not a fabricated negative', () => {
const dir = projectWithRoadmap(null);
const j = parseSingleObject(capture(realGsdRunPrelude(dir)), 'unresolved phase');
assert.strictEqual(j.skipped, true);
assert.strictEqual(j.detected, undefined);
});
test('a probe that CANNOT LAUNCH reports skipped, never detected:false', () => {
const j = parseSingleObject(
capture(['PHASE=01', 'gsd_run() { return 127; }'].join('\n')), 'probe unavailable');
assert.strictEqual(j.skipped, true, 'a launcher that failed must not yield a verdict');
assert.strictEqual(j.reason, 'probe_unavailable');
assert.strictEqual(j.detected, undefined);
});
});
// ─── Parity: one vocabulary across both fragments ─────────────────────────────
describe('both fragments share one skipped-with-reason vocabulary (#3909)', () => {
// Generative-fix divergence guard: two surfaces adopting one convention must
// fail this test the moment they drift apart.
test('both fragments emit the same probe-unavailable reason token', () => {
const results = [
{ name: 'api-coverage', block: extractProbeBlock(API_FRAGMENT, 'API_COVERAGE_JSON') },
{ name: 'assumption-delta', block: extractProbeBlock(DELTA_FRAGMENT, 'ASSUMPTION_DELTA_JSON') },
].map(({ name, block }) => {
const varName = name === 'api-coverage' ? 'API_COVERAGE_JSON' : 'ASSUMPTION_DELTA_JSON';
const captured = runSnippet({
block,
varName,
// Force the unavailable path for both: no PHASE_DIR contents, and a
// launcher/cwd that cannot produce output.
prelude: [
'PHASE_DIR=/nonexistent-phase-dir-3909',
'PHASE=01',
'gsd_run() { return 127; }',
].join('\n'),
cwd: os.tmpdir(),
});
return { name, payload: parseSingleObject(captured, name) };
});
for (const { name, payload } of results) {
assert.strictEqual(payload.skipped, true, `${name} must report skipped when the probe cannot run`);
}
assert.strictEqual(
results[0].payload.reason,
results[1].payload.reason,
'the two fragments must not invent different reason tokens for the same condition',
);
});
});