refactor(red-proof): the 4.5a script is a wrapper over the shared one; add the 4.5b wrapper

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-10-26 12:58:23 +01:00
commit 7566b71674
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
7 changed files with 388 additions and 154 deletions

View file

@ -19,6 +19,11 @@
// Every test first asserts that the script exists, so each fails on an
// assertion while it does not. The script and its runs need Node >= 20.14
// (--test-force-exit); on an older Node each test is skipped.
//
// The two round wrappers (tests/fix-round-4-5a-red.sh, tests/fix-round-4-5b-red.sh)
// are run from a scratch copy whose tests/helpers/red-round.sh is a stub that
// prints its arguments, so what each wrapper hands the shared script is seen
// without a clone. Their mutation modules are loaded with a dynamic import.
import { after, test } from 'node:test';
import { strict as assert } from 'node:assert';
@ -28,7 +33,7 @@ import {
} from 'node:fs';
import { tmpdir } from 'node:os';
import { delimiter, dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { childEnv } from '../helpers/child-env.mjs';
import { pathWith } from '../helpers/bin-path.mjs';
@ -339,3 +344,169 @@ test('[E23b] the base run carries --test-timeout and --test-force-exit', { skip:
}
assert.match(text, /^#.*Node >= 20\.14/m, 'the header does not say that Node >= 20.14 is needed');
});
// --- the round wrappers ------------------------------------------------------
// Expected values from the plan (Step 3) and the 4.5a brief, written out here.
const WRAP_A = {
rel: 'tests/fix-round-4-5a-red.sh',
base: 'ab6b32c9304534b77c7ca2b36e7c993682c9efde',
mutations: 'tests/fix-round-4-5a-mutations.mjs',
files: ['tests/lib/fix-round-4-5a-prose.test.mjs', 'tests/lib/verdict-gate.test.mjs', 'tests/lib/park-attempt-files.test.mjs',
'tests/lib/autonomy-gate.test.mjs', 'tests/lib/arg-parser.test.mjs', 'tests/scripts/trekreview-armb-workflow.test.mjs',
'tests/lib/coordinator-contract.test.mjs', 'tests/validators/next-session-prompt-validator.test.mjs'],
};
const WRAP_B = {
rel: 'tests/fix-round-4-5b-red.sh',
base: 'be0b631fed21942b6c0fdae570b58e4112f8242d',
mutations: 'tests/fix-round-4-5b-mutations.mjs',
files: ['tests/lib/verdict-gate.test.mjs', 'tests/lib/done-line.test.mjs', 'tests/lib/park-attempt-files.test.mjs',
'tests/scripts/trekreview-armb-workflow.test.mjs', 'tests/lib/coordinator-contract.test.mjs', 'tests/lib/red-judge.test.mjs',
'tests/lib/red-script.test.mjs', 'tests/lib/review-recurrence.test.mjs', 'tests/lib/arg-parser.test.mjs',
'tests/commands/trekendsession.test.mjs', 'tests/validators/next-session-prompt-validator.test.mjs',
'tests/lib/fix-round-4-5b-prose.test.mjs', 'tests/lib/fix-round-4-5a-prose.test.mjs', 'tests/scripts/release-check.test.mjs'],
};
const IDS_B = ['M6', 'M11', 'M4', 'm4', 'M3', 'M7', 'E22c', 'E22e', 'M5', 'S3', 'E20', 'M9', 'M2', 'K3', 'M1', 'M1r', 'E23a',
'E23b', 'E23d', 'E23f', 'M8', 'M10', 'DJ5', 'M13', 'K2', 'DJ1', 'DJ2', 'DJ4', 'E14', 'E16a', 'E17a', 'E17b', 'E18a', 'Da',
'Db', 'K4', 'ARG', 'PTR', 'RS'];
/** A wrapper's text; asserts first that it exists. */
function wrapperText(w) {
const p = join(ROOT, w.rel);
assert.ok(existsSync(p), `the wrapper ${w.rel} does not exist`);
return readFileSync(p, 'utf8');
}
/** The words of a wrapper's one NAME='...' line. */
function quotedList(text, name, rel) {
const lines = text.split('\n').filter((l) => l.startsWith(`${name}=`));
assert.equal(lines.length, 1, `expected one ${name}= line in ${rel}; found ${lines.length}`);
const m = lines[0].match(new RegExp(`^${name}='([^']*)'$`));
assert.ok(m, `the ${name}= line of ${rel} is not one single-quoted list: ${lines[0]}`);
return m[1].split(/\s+/).filter(Boolean);
}
/** A stand-in for the shared script: one ARG: line per argument, exit $RS_STUB_EXIT (default 0). */
const STUB = `#!/bin/bash
for a in "$@"; do printf 'ARG:%s\\n' "$a"; done
exit "\${RS_STUB_EXIT:-0}"
`;
/** A scratch root holding both wrappers and the stub as tests/helpers/red-round.sh. */
function stubRoot() {
const a = wrapperText(WRAP_A);
const b = wrapperText(WRAP_B);
const dir = join(tmp('red-script-wrap-'), 'root');
mkdirSync(join(dir, 'tests', 'helpers'), { recursive: true });
writeFileSync(join(dir, WRAP_A.rel), a);
writeFileSync(join(dir, WRAP_B.rel), b);
writeFileSync(join(dir, 'tests', 'helpers', 'red-round.sh'), STUB, { mode: 0o755 });
return dir;
}
/** A wrapper run from the scratch root: status, output and the arguments the stub saw. */
function runWrapper(dir, w, over = {}) {
const r = spawnSync('/bin/bash', [join(dir, w.rel)], { cwd: tmpdir(), encoding: 'utf8', timeout: CHILD_TIMEOUT, env: env(over) });
const argv = r.stdout.split('\n').filter((l) => l.startsWith('ARG:')).map((l) => l.slice(4));
return { status: r.status, out: `${r.stdout}${r.stderr}`, stderr: r.stderr, argv };
}
/** The stub's arguments as options (each with a value but --require-sha256) and files. */
function parseCall(argv) {
const call = { flags: [], files: [] };
let i = 0;
while (i < argv.length && argv[i].startsWith('--')) {
if (argv[i] === '--require-sha256') {
call.flags.push(argv[i]);
i += 1;
} else {
call[argv[i]] = argv[i + 1];
i += 2;
}
}
call.files = argv.slice(i);
return call;
}
test('[RS] both wrappers exec the shared script with their own BASE, IDS and FILES', () => {
const dir = stubRoot();
for (const w of [WRAP_A, WRAP_B]) {
const text = wrapperText(w);
assert.deepEqual(text.split('\n').filter((l) => /^BASE=/.test(l)), [`BASE=${w.base}`], `${w.rel}: BASE is not the one full ${w.base.slice(0, 7)} sha`);
assert.deepEqual(quotedList(text, 'FILES', w.rel), w.files, `${w.rel}: the FILES= line is not the round's files`);
const ids = quotedList(text, 'IDS', w.rel);
const r = runWrapper(dir, w);
assert.equal(r.status, 0, `${w.rel} did not run the shared script:\n${r.out}`);
assert.ok(r.argv.length > 0, `${w.rel} handed the shared script no arguments:\n${r.out}`);
const call = parseCall(r.argv);
assert.equal(call['--base'], w.base, `${w.rel}: --base\n${r.out}`);
assert.deepEqual((call['--ids'] || '').split(/\s+/).filter(Boolean), ids, `${w.rel}: --ids is not its IDS= line\n${r.out}`);
assert.equal(call['--mutations'], join(dir, w.mutations), `${w.rel}: --mutations is not its own module by absolute path\n${r.out}`);
assert.deepEqual(call.files, w.files, `${w.rel}: the test files are not its FILES= line\n${r.out}`);
const failed = runWrapper(dir, w, { RS_STUB_EXIT: '1' });
assert.equal(failed.status, 1, `${w.rel} does not end with the shared script's exit status:\n${failed.out}`);
}
});
test('[RS] each wrapper reads the other round\'s ids from its IDS line, no copied list', () => {
const dir = stubRoot();
for (const [w, other] of [[WRAP_A, WRAP_B], [WRAP_B, WRAP_A]]) {
const otherText = wrapperText(other);
const otherIds = quotedList(otherText, 'IDS', other.rel);
const r = runWrapper(dir, w);
assert.equal(r.status, 0, r.out);
assert.deepEqual((parseCall(r.argv)['--other-ids'] || '').split(/\s+/).filter(Boolean), otherIds,
`${w.rel}: --other-ids is not the IDS= line of ${other.rel}\n${r.out}`);
const otherLine = otherText.split('\n').find((l) => l.startsWith('IDS='));
writeFileSync(join(dir, other.rel), otherText.replace(otherLine, "IDS='P1 P2'"));
const planted = runWrapper(dir, w);
assert.equal(planted.status, 0, planted.out);
assert.equal(parseCall(planted.argv)['--other-ids'], 'P1 P2', `${w.rel} does not read ${other.rel}'s IDS= line when it runs\n${planted.out}`);
writeFileSync(join(dir, other.rel), otherText.replace(otherLine, "IDS=''"));
const empty = runWrapper(dir, w);
assert.equal(empty.status, 2, `${w.rel}: an empty IDS= line in ${other.rel} is not a setup failure\n${empty.out}`);
assert.deepEqual(empty.argv, [], `${w.rel} ran the shared script with no other-round ids\n${empty.out}`);
assert.match(empty.stderr, /setup failure/, empty.out);
writeFileSync(join(dir, other.rel), otherText);
assert.ok(!wrapperText(w).includes(otherIds.join(' ')), `${w.rel} holds a copy of ${other.rel}'s id list`);
}
});
test('[RS] the 4.5b IDS line holds exactly the 39 ids', () => {
const ids = quotedList(wrapperText(WRAP_B), 'IDS', WRAP_B.rel);
assert.equal(IDS_B.length, 39, 'control: the expected list is not 39 ids');
assert.deepEqual(ids, IDS_B);
assert.equal(new Set(ids).size, ids.length, 'an id is on the 4.5b IDS= line twice');
});
test('[RS] only the 4.5b wrapper requires SHA-256', () => {
const dir = stubRoot();
const a = runWrapper(dir, WRAP_A);
const b = runWrapper(dir, WRAP_B);
assert.equal(a.status, 0, a.out);
assert.equal(b.status, 0, b.out);
assert.deepEqual(parseCall(a.argv).flags, [], `${WRAP_A.rel} passes a flag it should not:\n${a.out}`);
assert.deepEqual(parseCall(b.argv).flags, ['--require-sha256'], `${WRAP_B.rel} does not pass --require-sha256:\n${b.out}`);
});
test('[RS] every mutation module loads, keys are unique and every id is on its wrapper\'s IDS line', async () => {
const expectKeys = { [WRAP_A.rel]: ['DJ-A', 'DJ-B'], [WRAP_B.rel]: ['E23a', 'E23b', 'RS', 'E23d', 'E23f'] };
for (const w of [WRAP_A, WRAP_B]) {
const ids = quotedList(wrapperText(w), 'IDS', w.rel);
const p = join(ROOT, w.mutations);
assert.ok(existsSync(p), `the mutation module ${w.mutations} does not exist`);
const mod = await import(pathToFileURL(p).href);
assert.ok(Array.isArray(mod.MUTATIONS) && mod.MUTATIONS.length > 0, `${w.mutations} exports no non-empty MUTATIONS array`);
const keys = mod.MUTATIONS.map((m) => m.key);
assert.equal(new Set(keys).size, keys.length, `${w.mutations}: a key is used twice: ${keys.join(' ')}`);
for (const k of expectKeys[w.rel]) assert.ok(keys.includes(k), `${w.mutations} has no entry ${k}`);
for (const m of mod.MUTATIONS) {
assert.ok(ids.includes(m.id), `${w.mutations}: ${m.key} names id ${m.id}, which is not on the IDS= line of ${w.rel}`);
assert.ok(Array.isArray(m.spans) && m.spans.length > 0, `${w.mutations}: ${m.key} has no spans`);
for (const s of m.spans) assert.ok(typeof s.file === 'string' && s.file !== '' && !s.file.startsWith('/'), `${w.mutations}: ${m.key} has a span file that is not a relative path`);
}
}
});