#!/usr/bin/env node // scripts/verify-phase-5.test.mjs // Source: locks the orchestrator structure of scripts/verify-phase-5.mjs. // This test asserts the final shape so any future re-ordering or step // addition/removal must update both the orchestrator AND this gate-test // deliberately. // // Two-mode behavior: // - Static analysis: regex-extract step labels from verify-phase-5.mjs // source and deepEqual against EXPECTED_LABELS (always run). // - Smoke (optional, env REBNO_TEST_SMOKE=1): also spawn the orchestrator // end-to-end. Skipped by default — the composite gate runs in CI via // `pnpm verify:phase-5` directly, so the smoke duplicates that and // pulls full Phase-4 carry-over which is host-sensitive. // // Usage: node scripts/verify-phase-5.test.mjs // OR: pnpm test:verify-phase-5 // Exit: 0 on success, 1 on any assertion failure. import { readFileSync } from 'node:fs'; import { spawnSync } from 'node:child_process'; // WR-14 reorder: cheap regex lints run BEFORE Workspace: test so a // failure surfaces in seconds, not after a 2-minute integration suite. // First (Phase 4 carry-over) and last (Traceable-reqs: check) anchors // are still asserted explicitly below. // // NOTE: Soak (Plan 11) is OUT-OF-BAND — not in this list. // RESTORE.md manual drill (Plan 13) is not in this list either. const EXPECTED_LABELS = [ 'Phase 4 carry-over: verify-phase-4', 'Workspace: typecheck', 'Lint: deploy-stack', 'Lint: deploy-stack test', 'ADR 0005 lint', 'ADR 0006 lint', 'Workspace: test', 'Traceable-reqs: check', ]; // ─── Static analysis: parse the steps array from source ─────────────────── const src = readFileSync('scripts/verify-phase-5.mjs', 'utf-8'); if (!src.includes('SKIP_PHASE_4_CARRYOVER')) { console.error('FAIL: verify-phase-5 must retain the explicit SKIP_PHASE_4_CARRYOVER debug escape hatch'); process.exit(1); } const m = src.match(/const steps\s*=\s*\[([\s\S]*?)\n\];/); if (!m) { console.error('FAIL: steps array not found in scripts/verify-phase-5.mjs'); process.exit(1); } const arrText = m[1]; // Each step is a row of the form ['Label', 'cmd', ['arg', ...]]. Conditional // spread rows use the form : [['Label', ...]]. Match only line-opening row // labels so command args are not counted. const labels = [...arrText.matchAll(/^\s*(?::\s*)?\[\[?'([^']+)'/gm)].map((x) => x[1]); if (labels.length !== EXPECTED_LABELS.length) { console.error( `FAIL: expected ${EXPECTED_LABELS.length} step labels, found ${labels.length}\n` + ` expected:\n ${EXPECTED_LABELS.join('\n ')}\n` + ` got:\n ${labels.join('\n ')}` ); process.exit(1); } for (let i = 0; i < EXPECTED_LABELS.length; i++) { if (labels[i] !== EXPECTED_LABELS[i]) { console.error( `FAIL: step label drift at index ${i}\n` + ` expected: '${EXPECTED_LABELS[i]}'\n` + ` got: '${labels[i]}'\n` + ` full expected:\n ${EXPECTED_LABELS.join('\n ')}\n` + ` full got:\n ${labels.join('\n ')}` ); process.exit(1); } } // Invariants — first/last anchor (CLAUDE.md hard rules): if (labels[0] !== 'Phase 4 carry-over: verify-phase-4') { console.error("FAIL: 'Phase 4 carry-over: verify-phase-4' MUST be first step (Phase 5 must not regress Phase 4)"); process.exit(1); } if (labels[labels.length - 1] !== 'Traceable-reqs: check') { console.error("FAIL: 'Traceable-reqs: check' MUST be the final step (CLAUDE.md hard rule)"); process.exit(1); } // ─── Optional smoke: spawn orchestrator end-to-end ─────────────────────── if (process.env['REBNO_TEST_SMOKE'] === '1') { const r = spawnSync('node', ['scripts/verify-phase-5.mjs'], { encoding: 'utf-8' }); if (r.status !== 0) { console.error('FAIL: verify-phase-5 smoke exited non-zero', { status: r.status }); console.error('--- stdout ---\n' + (r.stdout ?? '')); console.error('--- stderr ---\n' + (r.stderr ?? '')); process.exit(1); } const stdout = r.stdout ?? ''; if (!/OK \(\d+ steps green\)/.test(stdout)) { console.error('FAIL: expected /OK \\(\\d+ steps green\\)/ in smoke stdout'); console.error('--- stdout ---\n' + stdout); process.exit(1); } console.log(`verify-phase-5.test: OK (${labels.length} steps in canonical order; smoke green)`); process.exit(0); } console.log(`verify-phase-5.test: OK (${labels.length} steps in canonical order)`); process.exit(0);