Files
msd-core/tests/assumption-delta-checkpoint-e2e.test.cjs
0xdhx bc5c362610 fix(#2665): replace the hand-synced TEST_ENV_BASE copies with the canonical import
Eight declarations were kept in sync by hand with no parity assertion. The drift
was already in the tree: api-coverage-gate-e2e and representative-corpus declared
TERM_SESSION, but the real variable is TERM_SESSION_ID, so both scrubbed nothing
for that slot and leaked TERM_SESSION_ID into every child. Deleting the copies
removes the dead key with them -- there is no longer a second place to get wrong.

run-tests-harness keeps its local session-identity literal: that helper mirrors
the production runGsdTools to prove its CONTRACT, so it must not re-import what
it is testing. The config-LOCATION keys are a safety scrub rather than part of
that contract, so it spreads the canonical derived set and keeps the rest local.

Addresses review findings: Blocker 2, Blocker 3.
2026-08-08 05:50:17 -05:00

229 lines
9.6 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 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: 60000,
});
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 → detected:false, no throw (graceful)', () => {
tmpDir = makeRoadmapProject();
const r = scanQuery(tmpDir, 999, ['--json']);
assert.ok(r.success, `scan should succeed on unknown phase. stderr: ${r.error}`);
assert.strictEqual(JSON.parse(r.output).detected, false);
});
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'
);
});
});