fix(execute): below 1.8 a plan that names no test file gets the files the helper wrote

Operator decision 41 (14.10); review finding 7b3b59ed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-10-14 18:17:24 +02:00
commit 829da487ec
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
6 changed files with 90 additions and 9 deletions

View file

@ -28,6 +28,10 @@
# line " plan_tests: tests/parser.test.mjs").
# 13. red-proof --red-only on a 1.8 plan whose File line names two tests,
# each with a (new) note (exit 0; tests is both, in order; decision 31).
# 14. red-proof --red-only on a 1.7 plan whose File line names its test
# without backticks: no --tests (exit 2, no test files given, no tests
# line), then --tests with the file the helper wrote (exit 0, the line
# " tests: tests/parser.test.mjs"; decision 41).
# Prints "e2e: ok" and exits 0, or names the failed check and exits 1.
# bash 3.2 compatible, ASCII only.
@ -193,7 +197,12 @@ writeFileSync(dir + "/p13.md", buildPlan({ steps: [{
filesRaw: "`src/parser.mjs`, `tests/a.test.mjs`, `tests/b.test.mjs`",
testFirstRaw: " - File: `tests/a.test.mjs` (new), `tests/b.test.mjs` (new)",
}, e2e] }));
' "$TMP" || fail "could not write the plans for checks 8 to 13"
writeFileSync(dir + "/p14.md", buildPlan({ version: "1.7", steps: [{
file: "src/parser.mjs",
filesRaw: "`src/parser.mjs`, `tests/parser.test.mjs`",
testFirstRaw: " - File: tests/parser.test.mjs (new)",
}, e2e] }));
' "$TMP" || fail "could not write the plans for checks 8 to 14"
out="$(node lib/validators/plan-validator.mjs --strict --json "$TMP/p8.md" 2>&1)"
rc=$?
@ -256,5 +265,22 @@ rc=$?
tests_equal "$TMP/rp13.json" '["tests/a.test.mjs","tests/b.test.mjs"]' \
|| fail "13: red-proof tests is not the two test paths in order: $(cat "$TMP/rp13.json")"
# ---- 14. decision 41: a 1.7 plan that names no test file, then --tests ----
out="$(node lib/verification/red-proof.mjs --red-only --cwd "$R" --base HEAD --cmd 'node --test tests/parser.test.mjs' --plan "$TMP/p14.md" --step 1 2>&1)"
rc=$?
[ "$rc" -eq 2 ] || fail "14: red-proof on a 1.7 plan that names no test file: exit $rc, expected 2: $out"
echo "$out" | grep -q 'no test files given' \
|| fail "14: red-proof on a 1.7 plan that names no test file: no 'no test files given' in: $out"
if echo "$out" | grep -Eq '^ tests: [^ ]'; then
fail "14: red-proof on a 1.7 plan that names no test file: a tests line in: $out"
fi
out="$(node lib/verification/red-proof.mjs --red-only --cwd "$R" --base HEAD --cmd 'node --test tests/parser.test.mjs' --plan "$TMP/p14.md" --step 1 --tests tests/parser.test.mjs 2>&1)"
rc=$?
[ "$rc" -eq 0 ] || fail "14: red-proof on a 1.7 plan with --tests the helper wrote: exit $rc, expected 0: $out"
echo "$out" | grep -Fqx ' tests: tests/parser.test.mjs' \
|| fail "14: red-proof on a 1.7 plan with --tests the helper wrote: no line ' tests: tests/parser.test.mjs' in: $out"
echo 'e2e: ok'
exit 0

View file

@ -346,6 +346,60 @@ test('prose: no red-proof call line in either file passes --tests (operator deci
}
});
test('prose: below 1.8 a plan that names no test file gets --tests from the files the helper wrote (operator decision 41, review 7b3b59ed)', () => {
const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const phrase = (s) => new RegExp(s.split(/\s+/).map(esc).join('\\s+'));
const d41 = /\boperator\s+decision\s+41(?!\d)/;
const NTFG = '`no test files given`';
const has = (text, s, where) => assert.match(text, phrase(s), `${where} holds "${s}"`);
// The helper: the sentence that holds `no test files given`.
const h = readRel(HELPER);
const at = h.search(phrase(NTFG));
assert.ok(at >= 0, 'the helper names `no test files given`');
const before = h.slice(0, at);
const ends = [...before.matchAll(/\.\s/g)];
const start = ends.length ? ends[ends.length - 1].index + 1 : 0;
const rest = h.slice(at);
const stop = rest.search(/\.(\s|$)/);
const sentence = h.slice(start, stop < 0 ? undefined : at + stop + 1);
has(sentence, 'below 1.8', 'the helper sentence');
has(sentence, '`--tests <the test files you wrote', 'the helper sentence');
assert.match(sentence, d41, 'the helper sentence names operator decision 41');
const pu = h.match(/plan-unreadable[^.]*do not build/);
assert.ok(pu && pu.index < at, 'the helper sentence stands after the plan-unreadable sentence');
// The executor: the definition line and the exit-2 row.
const lines = readRel(EXEC).split('\n');
const def = lines.find((l) => l.startsWith("**The step's test files** are"));
assert.ok(def, 'the executor definition line is there');
for (const s of [NTFG, 'naming the test files it wrote', 'the `tests:` line the helper copied into its report']) {
has(def, s, 'the executor definition line');
}
assert.match(def, d41, 'the executor definition line names operator decision 41');
assert.ok(def.endsWith(' The'), `the definition line still ends with " The": ${def.slice(-40)}`);
const row = lines.find((l) => l.startsWith('| 2 | `"not-run"`'));
assert.ok(row, 'the exit-2 row is there');
const r = row.search(phrase(NTFG));
assert.ok(r >= 0, 'the exit-2 row names `no test files given`');
const after = row.slice(r);
for (const s of ['is not a finding', 'the `tests:` line in the helper\'s report', 'tolerated as `"not-run"`']) {
has(after, s, 'the exit-2 row after `no test files given`');
}
assert.match(after, d41, 'the exit-2 row names operator decision 41');
// Handover 4, invariant 7.
const ho = readRel(['docs', 'HANDOVER-CONTRACTS.md']);
const a7 = ho.indexOf('7. **Every plan_version');
const a8 = ho.indexOf('8. **', a7);
assert.ok(a7 >= 0 && a8 > a7, 'Handover 4 invariants 7 and 8 are there');
const inv7 = ho.slice(a7, a8);
for (const s of [NTFG, 'naming the test files it wrote', 'the `tests:` line in the helper\'s report']) {
has(inv7, s, 'Handover 4 invariant 7');
}
assert.match(inv7, d41, 'Handover 4 invariant 7 names operator decision 41');
});
test('prose: the helper stops on plan-unreadable before the 1.8 branches and locks the printed list', () => {
const text = readRel(HELPER);
const pu = text.match(/plan-unreadable[^.]*do not build/);