The operator window's step [a] called 40-validate-standalone.sh directly (strict exit-code), so it STOPped on the first target carrying pre-existing in-repo test red — voyage (2 doc-consistency drifts re phase_models/phase_signals, content moved to docs/operations.md) and ai-psychosis (1). But the migration's ratified contract, the one the Step-11 dry-run validated as PASS 11/11, is 'introduce no regression': pre-existing in-repo red is the plugin's own concern, not a migration regression. The window enforced a STRICTER gate than the contract the dry-run signed off. Fix: new 41-validate-or-regression.sh — the single per-target gate the window calls in [a]. It runs 40 strict, then on failure passes iff the standalone failing-test NAME set is a SUBSET of the live in-repo set (the exact decision 99-dryrun.sh makes), reusing sc2-regression.sh. A genuine extraction-introduced regression still STOPs the window; a structure-validator fail and the config-audit gate stay strict. Single-source the failing-name capture: extract capture_fails into capture-fails.sh (mirrors the sc2-regression.sh extraction) so the live gate and the dry-run agree on what 'failing' means; 99-dryrun.sh now delegates to it (behaviour identical). Verified end-to-end on the real extracts: 40 strict fails voyage+ai-psychosis while 41 passes them 'N pre-existing, regression-relative'; clean targets (llm-security, graceful-handoff) still pass via the strict path. New hermetic tests: capture-fails 3/3, 41 6/6 (strict-pass, regression-relative-pass, genuine-regression-fail, structure-not-eligible, gate pass/fail). RUNBOOK per-repo step updated to 41. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
141 lines
6.8 KiB
JavaScript
141 lines
6.8 KiB
JavaScript
// Coverage for 41-validate-or-regression.sh — the per-target gate the operator window calls in step [a].
|
|
// It enforces the migration's RATIFIED contract ("introduce no regression"), NOT the stricter zero-failure
|
|
// gate that made run-operator-window.sh STOP on voyage's / ai-psychosis's pre-existing in-repo red. 41 is
|
|
// thin glue over already-tested detectors (sc2-regression.sh — sc-checks.test.mjs; capture-fails.sh —
|
|
// capture-fails.test.mjs) and over 40-validate-standalone.sh (strict). This suite drives the REAL 41 inside
|
|
// a self-contained fake migration tree: the heavy extract pipeline (40) is STUBBED so the regression-branch
|
|
// glue is exercised hermetically, with no /tmp/polyrepo-migration state and no real plugin extracts.
|
|
//
|
|
// Branches covered: strict-PASS passthrough; regression-relative PASS (standalone ⊆ in-repo); genuine
|
|
// regression FAIL (standalone-only failure); structure-validator FAIL is NOT regression-eligible; the
|
|
// sc2_gate target stays strict (PASS + FAIL).
|
|
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { spawnSync } from 'node:child_process';
|
|
import { mkdtempSync, rmSync, writeFileSync, mkdirSync, copyFileSync, chmodSync } from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
|
|
// One node:test file = imports ONCE + a failing test() per name. (Concatenating per-name single-file
|
|
// strings would duplicate `import test` → SyntaxError → a file-level not-ok instead of per-test names.)
|
|
const failSuite = (names) =>
|
|
"import test from 'node:test';\nimport assert from 'node:assert/strict';\n" +
|
|
names.map((n) => `test(${JSON.stringify(n)}, () => assert.equal(1, 2));`).join('\n') +
|
|
'\n';
|
|
|
|
function write(p, content, mode) {
|
|
mkdirSync(path.dirname(p), { recursive: true });
|
|
writeFileSync(p, content);
|
|
if (mode) chmodSync(p, mode);
|
|
}
|
|
|
|
// Build a fake migration tree: SCRIPT_DIR nested 3 deep so 41's REPO_ROOT ($SCRIPT_DIR/../../..) is the root.
|
|
function buildTree() {
|
|
const root = mkdtempSync(path.join(os.tmpdir(), 'mig41-'));
|
|
const mig = path.join(root, 'a', 'b', 'c'); // SCRIPT_DIR; ../../.. === root
|
|
const work = path.join(root, '_work'); // $WORK
|
|
mkdirSync(mig, { recursive: true });
|
|
|
|
// real scripts under test + reused detectors
|
|
for (const f of ['41-validate-or-regression.sh', 'capture-fails.sh', 'sc2-regression.sh']) {
|
|
copyFileSync(path.join(here, f), path.join(mig, f));
|
|
}
|
|
|
|
// stub strict 40-validate-standalone.sh: passes ONLY for the designated strict-pass target.
|
|
write(path.join(mig, '40-validate-standalone.sh'),
|
|
'#!/usr/bin/env bash\n[ "${1:-}" = "strictpass" ] && exit 0\nexit 1\n', 0o755);
|
|
// sc2_gate stubs
|
|
write(path.join(mig, 'gate-pass.sh'), '#!/usr/bin/env bash\nexit 0\n', 0o755);
|
|
write(path.join(mig, 'gate-fail.sh'), '#!/usr/bin/env bash\nexit 1\n', 0o755);
|
|
|
|
const NT = "node --test 'tests/**/*.test.mjs'";
|
|
const map = {
|
|
targets: {
|
|
strictpass: { test_cmd: NT },
|
|
subset: { test_cmd: NT },
|
|
regress: { test_cmd: NT },
|
|
structure: { test_cmd: 'bash validate-plugin.sh' },
|
|
gatepass: { test_cmd: NT, sc2_gate: 'gate-pass.sh' },
|
|
gatefail: { test_cmd: NT, sc2_gate: 'gate-fail.sh' },
|
|
},
|
|
};
|
|
write(path.join(mig, 'plugin-map.json'), JSON.stringify(map, null, 2));
|
|
|
|
// node:test suites for the regression-eligible targets. dest = $WORK/<key> (needs .git); baseline = root/plugins/<key>.
|
|
const suite = (key, names) => {
|
|
mkdirSync(path.join(work, key, '.git'), { recursive: true });
|
|
write(path.join(work, key, 'tests', 'x.test.mjs'), failSuite(names.standalone));
|
|
write(path.join(root, 'plugins', key, 'tests', 'x.test.mjs'), failSuite(names.baseline));
|
|
};
|
|
suite('subset', { standalone: ['shared red'], baseline: ['shared red', 'other in-repo red'] });
|
|
suite('regress', { standalone: ['shared red', 'extract regressed'], baseline: ['shared red'] });
|
|
|
|
return { root, mig, work };
|
|
}
|
|
|
|
function run41(tree, key) {
|
|
// Strip NODE_TEST_CONTEXT: 41 → capture-fails → `node --test`, which emits no TAP if it inherits the
|
|
// harness's nested-test context (Step-6 env-clean gotcha). The real window runs as plain bash, unnested.
|
|
const env = { ...process.env, WORK: tree.work };
|
|
delete env.NODE_TEST_CONTEXT;
|
|
return spawnSync('bash', [path.join(tree.mig, '41-validate-or-regression.sh'), key],
|
|
{ encoding: 'utf8', env });
|
|
}
|
|
|
|
test('strict-PASS passthrough: 40 passes → 41 PASS (standalone strict), exit 0, no capture', () => {
|
|
const tree = buildTree();
|
|
try {
|
|
const r = run41(tree, 'strictpass');
|
|
assert.equal(r.status, 0, `strict pass must exit 0: ${r.stderr}`);
|
|
assert.match(r.stdout, /PASS \(standalone strict\)/, r.stdout);
|
|
} finally { rmSync(tree.root, { recursive: true, force: true }); }
|
|
});
|
|
|
|
test('regression-relative PASS: standalone failing-set ⊆ in-repo → exit 0, reports N pre-existing', () => {
|
|
const tree = buildTree();
|
|
try {
|
|
const r = run41(tree, 'subset');
|
|
assert.equal(r.status, 0, `a subset of in-repo red is not a migration regression: ${r.stdout}${r.stderr}`);
|
|
assert.match(r.stdout, /pre-existing, regression-relative/, r.stdout);
|
|
} finally { rmSync(tree.root, { recursive: true, force: true }); }
|
|
});
|
|
|
|
test('genuine regression FAIL: a standalone-only failure (absent in-repo) → non-zero, names the regression', () => {
|
|
const tree = buildTree();
|
|
try {
|
|
const r = run41(tree, 'regress');
|
|
assert.notEqual(r.status, 0, 'a new failure introduced by extraction must STOP the window');
|
|
assert.match(r.stderr, /regression/, r.stderr);
|
|
assert.match(r.stderr, /extract regressed/, `the regressing test name must be surfaced: ${r.stderr}`);
|
|
} finally { rmSync(tree.root, { recursive: true, force: true }); }
|
|
});
|
|
|
|
test('structure-validator FAIL is NOT regression-eligible: bash validator fail → non-zero, never softened', () => {
|
|
const tree = buildTree();
|
|
try {
|
|
const r = run41(tree, 'structure');
|
|
assert.notEqual(r.status, 0, 'a deterministic structure-validator failure is a genuine defect');
|
|
assert.match(r.stderr, /not regression-eligible/, r.stderr);
|
|
} finally { rmSync(tree.root, { recursive: true, force: true }); }
|
|
});
|
|
|
|
test('sc2_gate target stays strict: gate PASS → exit 0', () => {
|
|
const tree = buildTree();
|
|
try {
|
|
const r = run41(tree, 'gatepass');
|
|
assert.equal(r.status, 0, r.stderr);
|
|
assert.match(r.stdout, /PASS \(gate-pass\.sh\)/, r.stdout);
|
|
} finally { rmSync(tree.root, { recursive: true, force: true }); }
|
|
});
|
|
|
|
test('sc2_gate target stays strict: gate FAIL → non-zero', () => {
|
|
const tree = buildTree();
|
|
try {
|
|
const r = run41(tree, 'gatefail');
|
|
assert.notEqual(r.status, 0, 'a failing dedicated gate must STOP the window');
|
|
assert.match(r.stderr, /FAIL \(gate-fail\.sh\)/, r.stderr);
|
|
} finally { rmSync(tree.root, { recursive: true, force: true }); }
|
|
});
|