ktg-plugin-marketplace/docs/marketplace-polyrepo-migration/migration/41-validate-or-regression.test.mjs
Kjell Tore Guttormsen 3403648c6c fix(migration): operator-window SC2 must be regression-relative, not strict
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>
2026-06-18 06:25:55 +02:00

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 }); }
});