Files
msd-core/tests/assumption-delta-checkpoint-e2e.test.cjs
Tom Boucher a841575037 test(#4527): migrate planning/review-lane batch to named timeout constants (#4680)
Batch 16 of 17 in the ad hoc timeout literal migration (epic #4445).
Replaces every bare numeric timeout/timeoutMs object-literal property in
9 test files with a named constant, per
eslint-rules/no-adhoc-timeout-literal.cjs. No src/bin file touched, no
numeric value changed anywhere.

Ground truth via eslint found 48 sites, not the issue's stated 38 —
tests/code-review.test.cjs alone has 11, not 1 (a 10-site undercount,
the largest single-file drift in this epic). All 11 are migrated.

Reuses PROBE_TIMEOUT_MS (17 sites across assumption-delta.test.cjs and
code-review.test.cjs), LOOP_HOOK_POINT_CLI_TIMEOUT_MS (1 site),
QUICK_SPAWN_TIMEOUT_MS (1 site). Adds a new shared constant,
HTTP_REACHABLE_PROBE_TIMEOUT_FIXTURE_MS, promoted because two files
(reviewer-manifest-body.test.cjs, reviewer-trust-disclosure.test.cjs)
independently arrived at the same probe-fixture value across 3 sites.

File-local constants elsewhere for values not shared across files:
FALLOW_AUDIT_TIMEOUT_MS (code-review-pipeline-regression.test.cjs);
a 5-constant set covering runBashScript's own override/forced-timeout/
boundary-triple tests (plan-phase-stall-detection.test.cjs); 9
lane-specific NATIVE_TIMEOUT_MS constants, one per shipped reviewer CLI
tool, even where 4 lanes coincidentally share a value
(review-lane-invocation.test.cjs); and a 6-constant set covering
reviewer-manifest-body.test.cjs's own probe-kind fixtures and its
separate, unrelated boundary/invalid set that coincidentally overlaps
in shape (not value) with plan-phase-stall-detection.test.cjs's triple.

Fixes applied inline from review: GENERATOR_SCRIPT_TIMEOUT_MS was
misapplied to 3 sites in plan-review-convergence.test.cjs whose actual
shape (nested bash -> node -> gsd-tools.cjs chain) doesn't match that
constant's documented direct-spawn class, confirmed against its own
cited precedent files — replaced with a new file-local constant
naming the correct class, same value. Two stray un-renamed literals in
review-lane-invocation.test.cjs (missed by eslint's object-literal-only
detection) were investigated and left as disclosed literals with an
explanatory comment rather than force-fit onto an unrelated lane's
constant, since they belong to a distinct config-override code path.

Co-authored-by: sim <sim@local>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-13 02:35:23 -04:00

319 lines
14 KiB
JavaScript

'use strict';
/**
* E2E capability-wiring tests for the assumption-delta checkpoint (#1561).
*
* Drives the real `loop render-hooks plan:pre` CLI subprocess against temp
* projects with different config values and asserts on the typed envelope's
* activeHooks — proving the capability contribution activates/deactivates by
* config (acceptance criteria #4 non-blocking advisory + #6 capability hook).
*
* CONTENT/E2E only: every test drives a real CLI subprocess. No readFileSync
* source-grep. Genuine assertions: each case asserts the SPECIFIC differing
* value (capId presence/absence), not a count (plan:pre carries other
* default-on contributions owned by other capabilities).
*/
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 { execFileSync } = require('node:child_process');
const { cleanup, TEST_ENV_BASE } = require('./helpers.cjs');
const { LOOP_HOOK_POINT_CLI_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
const TOOLS_PATH = path.join(__dirname, '..', 'gsd-core', 'bin', 'gsd-tools.cjs');
function runTools(args, cwd) {
const argv = Array.isArray(args)
? args
: (args.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) || [])
.map((t) => t.replace(/"([^"]*)"/g, '$1').replace(/'([^']*)'/g, '$1'));
try {
const stdout = execFileSync(process.execPath, [TOOLS_PATH, ...argv], {
cwd,
encoding: 'utf-8',
env: { ...process.env, ...TEST_ENV_BASE },
timeout: LOOP_HOOK_POINT_CLI_TIMEOUT_MS,
});
return { success: true, output: stdout.trim(), exitCode: 0, error: '' };
} catch (err) {
return {
success: false,
output: err.stdout?.toString().trim() || '',
error: err.stderr?.toString().trim() || err.message,
exitCode: err.status ?? 1,
};
}
}
function makeProject(config) {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-adelta-'));
fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true });
fs.writeFileSync(
path.join(tmpDir, '.planning', 'config.json'),
JSON.stringify(config),
'utf8'
);
return tmpDir;
}
function planPreHooks(cwd) {
const result = runTools('loop render-hooks plan:pre --raw', cwd);
assert.ok(result.success, `render-hooks plan:pre should succeed. stderr: ${result.error}`);
const envelope = JSON.parse(result.output);
assert.strictEqual(envelope.point, 'plan:pre', 'point field must be plan:pre');
assert.ok(Array.isArray(envelope.activeHooks), 'activeHooks must be an array');
return envelope;
}
function findCap(envelope, capId) {
return envelope.activeHooks.find((h) => h.capId === capId) || null;
}
// ─── ROADMAP fixture for the scan query ──────────────────────────────────────
const ROADMAP = [
'# Roadmap',
'',
'## v1.0.0',
'',
'### Phase 1: Add a second auth method alongside passwords',
'**Goal:** Users can authenticate via SSO in addition to passwords',
'**Success Criteria**:',
'1. SSO login works',
'2. Password login still works',
'',
'### Phase 2: Refactor the parser for readability',
'**Goal:** Smaller functions, no behavior change',
'**Success Criteria**:',
'1. All existing tests still pass',
'',
].join('\n');
function makeRoadmapProject() {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-adelta-rm-'));
fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true });
fs.writeFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), ROADMAP, 'utf8');
return tmpDir;
}
function scanQuery(cwd, phase, extra) {
const argv = ['query', 'assumption-delta', 'scan', String(phase)];
if (extra) argv.push(...extra);
return runTools(argv, cwd);
}
describe('assumption-delta scan query — phase-section detection (#1561)', () => {
let tmpDir;
afterEach(() => { if (tmpDir) { cleanup(tmpDir); tmpDir = null; } });
test('phase with a pluralization signal → detected:true', () => {
tmpDir = makeRoadmapProject();
const r = scanQuery(tmpDir, 1, ['--json']);
assert.ok(r.success, `scan should succeed. stderr: ${r.error}`);
const parsed = JSON.parse(r.output);
assert.strictEqual(parsed.detected, true);
assert.ok(parsed.signals.some((s) => s.kind === 'pluralization'), 'phase 1 must trip pluralization');
});
test('phase with no signal → detected:false (low false-positive)', () => {
tmpDir = makeRoadmapProject();
const r = scanQuery(tmpDir, 2, ['--json']);
assert.ok(r.success, `scan should succeed. stderr: ${r.error}`);
const parsed = JSON.parse(r.output);
assert.strictEqual(parsed.detected, false);
assert.deepStrictEqual(parsed.signals, []);
});
test('unknown phase → skipped, no throw (graceful) (#3909)', () => {
tmpDir = makeRoadmapProject();
const r = scanQuery(tmpDir, 999, ['--json']);
assert.ok(r.success, `scan should succeed on unknown phase. stderr: ${r.error}`);
const parsed = JSON.parse(r.output);
// This asserted `detected:false` until #3909. That was the fabrication
// itself pinned as intended behavior: phase 999 does not exist, so the
// detector was handed the empty string and its "no core assumption
// changed" answer described nothing. The graceful-degradation contract
// the test was protecting (succeeds, does not throw) is unchanged.
assert.strictEqual(parsed.skipped, true);
assert.strictEqual(parsed.reason, 'phase_unresolved');
assert.strictEqual(parsed.detected, undefined);
});
test('--terms override narrows the vocabulary (custom cue fires, default cue does not)', () => {
tmpDir = makeRoadmapProject();
// phase 1 trips "second" by default; override to "xyzzy" → must NOT fire
const r = scanQuery(tmpDir, 1, ['--json', '--terms', 'xyzzy']);
assert.ok(r.success, `scan should succeed. stderr: ${r.error}`);
assert.strictEqual(JSON.parse(r.output).detected, false);
});
test('missing phase arg → non-zero exit (usage error)', () => {
tmpDir = makeRoadmapProject();
const r = runTools(['query', 'assumption-delta', 'scan'], tmpDir);
assert.notStrictEqual(r.exitCode, 0, 'scan with no phase must exit non-zero');
});
// ── Hardening (Codex Step-4 review): malformed args ──────────────────────
test('flag-shaped phase (scan --json) → non-zero exit, not treated as phase', () => {
tmpDir = makeRoadmapProject();
const r = runTools(['query', 'assumption-delta', 'scan', '--json'], tmpDir);
assert.notStrictEqual(r.exitCode, 0, '"--json" must not be accepted as a phase number');
});
test('empty --terms restores curated defaults (detected, not disabled)', () => {
tmpDir = makeRoadmapProject();
const r = scanQuery(tmpDir, 1, ['--terms', '', '--json']);
assert.ok(r.success, `scan should succeed. stderr: ${r.error}`);
const parsed = JSON.parse(r.output);
assert.strictEqual(parsed.detected, true, 'phase 1 must still trip defaults under empty --terms');
assert.ok(parsed.terms.pluralization.includes('second'));
});
test('flag-shaped --terms value (--terms --json) falls back to defaults, not treated as a term', () => {
tmpDir = makeRoadmapProject();
const r = scanQuery(tmpDir, 1, ['--terms', '--json']);
assert.ok(r.success, `scan should succeed. stderr: ${r.error}`);
const parsed = JSON.parse(r.output);
// 'json' must NOT have been consumed as a pluralization cue; defaults apply.
assert.ok(!parsed.terms.pluralization.includes('json'), '"--json" must not become a trigger term');
assert.ok(parsed.terms.pluralization.includes('second'), 'defaults restored');
});
});
describe('assumption-delta capability — plan:pre render-hooks wiring (#1561)', () => {
let tmpDir;
afterEach(() => { if (tmpDir) { cleanup(tmpDir); tmpDir = null; } });
test('default config (no toggle): assumption-delta contribution is ACTIVE', () => {
tmpDir = makeProject({});
const env = planPreHooks(tmpDir);
const hook = findCap(env, 'assumption-delta');
assert.ok(hook, 'assumption-delta contribution must be active under default config');
assert.strictEqual(hook.kind, 'contribution', 'must be a non-blocking contribution, not a gate');
assert.strictEqual(hook.into, 'planner', 'contribution injects into the planner role');
assert.strictEqual(hook.when, 'workflow.assumption_delta', 'when must gate on workflow.assumption_delta');
assert.strictEqual(hook.onError, 'skip', 'onError must be skip (advisory, never halts)');
// The fragment body must be inlined so the planner receives concrete prose.
const frag = hook.fragment && hook.fragment.inline ? hook.fragment.inline : hook.fragment;
assert.ok(typeof frag === 'string' && frag.length > 0, 'fragment must be inlined as non-empty text');
assert.ok(
/Assumption-Delta Architecture Checkpoint/i.test(frag),
'inlined fragment must carry the checkpoint heading'
);
});
test('workflow.assumption_delta=true: contribution is ACTIVE', () => {
tmpDir = makeProject({ workflow: { assumption_delta: true } });
const env = planPreHooks(tmpDir);
assert.ok(findCap(env, 'assumption-delta'), 'must be active when explicitly true');
});
test('workflow.assumption_delta=false: contribution is INACTIVE (absent from activeHooks)', () => {
tmpDir = makeProject({ workflow: { assumption_delta: false } });
const env = planPreHooks(tmpDir);
assert.strictEqual(
findCap(env, 'assumption-delta'),
null,
'must NOT appear in activeHooks when disabled — other default-on plan:pre hooks may still be present'
);
});
test('non-blocking guarantee: no assumption-delta entry is ever a blocking gate', () => {
// Advisory contract (acceptance #4): the checkpoint informs; it never blocks.
// Across both on/off states the capability must never surface as kind=gate
// with blocking=true.
tmpDir = makeProject({ workflow: { assumption_delta: true } });
const env = planPreHooks(tmpDir);
const gates = env.activeHooks.filter((h) => h.kind === 'gate');
assert.ok(
gates.every((g) => g.capId !== 'assumption-delta'),
'assumption-delta must never register a blocking gate at plan:pre'
);
});
});
// ─── An unresolved phase section is not a negative verdict (#3909, P5) ────────
// `routeAssumptionDelta` fed `detectAssumptionDelta(section ?? '')`, so a phase
// section that could not be resolved at all (no ROADMAP.md, unknown phase
// number) was scanned as the empty string and reported as an examined-and-
// negative result. That is a fabricated verdict: the probe never had input.
// The route now reports the skipped-with-reason shape instead. Exit code stays
// 0 — this is an ADR-2980 degraded result reported through the payload, and
// ADR-3889 P8 pins the gsd-tools projection at v1.
describe('query assumption-delta scan — unresolved phase reports skipped (#3909)', () => {
let tmpDir;
afterEach(() => { if (tmpDir) { cleanup(tmpDir); tmpDir = null; } });
function writeRoadmap(dir, body) {
fs.writeFileSync(path.join(dir, '.planning', 'ROADMAP.md'), body, 'utf8');
}
function scan(cwd, phase, extra = []) {
const r = runTools(['query', 'assumption-delta', 'scan', phase, '--json', ...extra], cwd);
return { raw: r, json: JSON.parse(r.output) };
}
test('CONTROL: a resolved section with no cues still reports detected:false', () => {
tmpDir = makeProject({ workflow: { assumption_delta: true } });
writeRoadmap(tmpDir, '# Roadmap\n\n### Phase 01: Cleanup\n\nRefactor the internal state machine.\n');
const { raw, json } = scan(tmpDir, '01');
assert.strictEqual(raw.exitCode, 0);
assert.strictEqual(json.detected, false, 'an examined section with no cues is a real negative');
assert.strictEqual(json.skipped, undefined, 'a real scan must NOT be reported as skipped');
});
test('CONTROL: a resolved section with a pluralization cue still detects', () => {
tmpDir = makeProject({ workflow: { assumption_delta: true } });
writeRoadmap(tmpDir, '# Roadmap\n\n### Phase 01: Auth\n\nAdd a second authentication method.\n');
const { json } = scan(tmpDir, '01');
assert.strictEqual(json.detected, true);
assert.strictEqual(json.skipped, undefined);
});
test('no ROADMAP.md at all reports skipped, not detected:false', () => {
tmpDir = makeProject({ workflow: { assumption_delta: true } });
const { raw, json } = scan(tmpDir, '01');
assert.strictEqual(json.skipped, true, 'a probe with no input must not assert a verdict');
assert.strictEqual(json.reason, 'phase_unresolved');
assert.strictEqual(
json.detected,
undefined,
'a skipped payload must carry no detected key — that is what makes it distinguishable',
);
assert.strictEqual(raw.exitCode, 0, 'degraded result stays exit 0 (ADR-2980; P8 pins v1)');
});
test('a ROADMAP.md that does not contain the phase reports skipped', () => {
tmpDir = makeProject({ workflow: { assumption_delta: true } });
writeRoadmap(tmpDir, '# Roadmap\n\n### Phase 01: Auth\n\nAdd a second authentication method.\n');
const { json } = scan(tmpDir, '99');
assert.strictEqual(json.skipped, true, 'an unknown phase number resolves to nothing');
assert.strictEqual(json.reason, 'phase_unresolved');
});
test('BOUNDARY: a resolved but body-less section is EXAMINED, not skipped', () => {
tmpDir = makeProject({ workflow: { assumption_delta: true } });
writeRoadmap(tmpDir, '# Roadmap\n\n### Phase 01: Empty\n\n \n\t\n\n### Phase 02: Next\n\nBody.\n');
const { json } = scan(tmpDir, '01');
// The resolver returns the heading line alone for a body-less section, so
// the phase WAS found and the detector did examine what the roadmap holds
// for it. `skipped` is reserved for the cases where the resolver returns
// null — an unknown phase number, or no ROADMAP.md at all. Pinning that
// line here keeps the distinction the issue actually asks for (found vs
// not found) from drifting into a vaguer "looks empty to me".
assert.strictEqual(json.skipped, undefined, 'a resolved section is not a skip');
assert.strictEqual(json.detected, false, 'the heading alone carries no cues — a real negative');
});
test('--terms does not rescue an unresolved phase', () => {
tmpDir = makeProject({ workflow: { assumption_delta: true } });
const { json } = scan(tmpDir, '01', ['--terms', 'second,alternative']);
assert.strictEqual(json.skipped, true, 'a vocabulary override cannot manufacture a scan');
assert.strictEqual(json.reason, 'phase_unresolved');
});
});