voyage/tests/lib/run-coupling-prose-pins.test.mjs
Kjell Tore Guttormsen 651be3468b
test(r8): move the live tests out of the round files
R8 of cleanup R-a (plan .claude/projects/2026-11-07-cleanup-r-a, appendix A).
The live tests of the round files move into the test named after their
source; each moved test was seen red under a mutation of its source.
New: tests/lib/lib-imports.test.mjs, tests/lib/timing.test.mjs (from
timing-under-load). The [#1] --gates rule and shim-line pins move into
tests/lib/autonomy-gate.test.mjs.

Removed: the round test files (retterunde, retterunde-prose-pins,
trial-mechanical-fixes, trial2-fixes, trial2-minor, trial2-minor-prose-pins,
fix-round-4-5a-prose, fix-round-4-5b2-b-prose, timing-under-load), the 8 R8
e2e scripts, and seven round test files on the R-a removal list that pinned
them (fix-round-4-5b-prose, fix-round-4-5b-part-2/3/4-scripts,
fix-round-4-5b-part-4-prose, fix-round-4-5b2-b/c-scripts). The diff-mode e2e
pin (test 24) and the red-script round-wrapper cases go too.

tests/synthetic/plan-run-C.md becomes tests/fixtures/plan-run-c.md,
regenerated from buildPlan({ version: '1.9' }). The red judge, reporter and
judge test headers now name the shared red script.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 10:19:49 +01:00

118 lines
6.1 KiB
JavaScript

// tests/lib/run-coupling-prose-pins.test.mjs
// The coupling to Claude Code's built-in `run` skill (wave 4, order A2,
// decisions 54 and 64). The plan's "run the thing" step, the executor's
// `### End to end` and the plan template's run step each say in one sentence
// that the app is seen and driven with `run`, and that the verdict is still
// the `End-to-end:` command, because `run` gives no exit code. Each part is
// read by its heading or marker, never by line number.
import { test } from 'node:test';
import { strict as assert } from 'node:assert';
import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { findSection } from '../../lib/parsers/plan-schema.mjs';
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
const read = (rel) => readFileSync(join(ROOT, rel), 'utf8');
/** From the line holding `marker` to the first line where `stop` is true. */
function partAt(rel, marker, stop) {
const lines = read(rel).split('\n');
const start = lines.findIndex((l) => l.includes(marker));
assert.ok(start !== -1, `${rel} must hold "${marker}"`);
let end = lines.length;
for (let i = start + 1; i < lines.length; i++) if (stop(lines[i])) { end = i; break; }
return lines.slice(start, end).join(' ').replace(/\s+/g, ' ');
}
const PARTS = [
// An indented paragraph of a numbered list item: it ends at a blank or unindented line.
['commands/trekplan.md', '**The "run the thing" step.**', (l) => l.trim() === '' || /^\S/.test(l)],
['commands/trekexecute.md', '### End to end — "run the thing" (P9 (6))', (l) => l.startsWith('### ')],
['templates/plan-template.md', '### Step 2: {description — the "run the thing" step}', (l) => l.startsWith('### ')],
];
// The template quotes the names instead of backticking them: every backticked
// span on a template step bullet is read as a runnable example
// (tests/lib/skills-criteria-prose-pins.test.mjs), and `run` is no command.
const RUN = '(?:`run`|"run")';
for (const [rel, marker, stop] of PARTS) {
test(`run coupling: ${rel} names Claude Code's run and keeps the verdict on the End-to-end: command`, () => {
const p = partAt(rel, marker, stop);
assert.match(p, new RegExp(`Claude Code's built-in ${RUN} skill`), `${rel}: the run step must name Claude Code's built-in run`);
assert.match(p, /the verdict is still the `?End-to-end:`? command/, `${rel}: the verdict must stay the End-to-end: command`);
assert.match(p, new RegExp(`${RUN} gives no exit code`), `${rel}: the reason must be said`);
});
}
test('run coupling: the run step still says what it runs, through the criteria runner', () => {
const p = partAt(...PARTS[0]);
assert.match(p, /criteria runner's allowlist/);
assert.match(p, /End-to-end: \\`bash tests\/e2e\.sh\\`/);
});
test('run coupling: the writing-criteria quiet case is owned by run, with no stand-in left in its description', () => {
const md = read('evals/skill-writing-criteria-quiet-2/prompt.md');
assert.match(md, /^tags: \[[^\]]*\bowner=run\b[^\]]*\]$/m);
assert.doesNotMatch(md, /stands in/);
assert.doesNotMatch(md, /owner=trekexecute/);
});
// The final report says whether the app was seen with `run`, for every run,
// also one with no app to launch (the "no — {reason}" form).
const EXEC = 'commands/trekexecute.md';
/**
* The text of a heading's section (level from its #-count), up to the next
* heading of the same or a higher level outside a fence. The heading must exist.
*/
function sectionText(text, heading) {
const level = heading.match(/^#+/)[0].length;
const s = findSection(text, heading.replace(/^#+ /, ''), level);
assert.ok(s, `missing ${heading}`);
const lines = s.text.split('\n');
let fenced = false;
for (let i = 1; i < lines.length; i++) {
if (/^\s{0,3}(`{3,}|~{3,})/.test(lines[i])) fenced = !fenced;
const m = !fenced && lines[i].match(/^(#+) /);
if (m && m[1].length < level) return lines.slice(0, i).join('\n');
}
return s.text;
}
/** Phase 8 of /trekexecute, read up to Phase 9 (its report fence holds its own `## ` heading). */
function phase8(text) {
const start = text.indexOf('## Phase 8 — Final report');
const end = text.indexOf('## Phase 9');
assert.ok(start !== -1 && end > start, 'Phase 8 — Final report and Phase 9 must both exist, in that order');
return text.slice(start, end);
}
test('run_seen: the Phase 8 report template says, right after End to end, whether the app was seen with `run`', () => {
const lines = phase8(read(EXEC)).split('\n');
const e2e = lines.findIndex((l) => l.startsWith('**End to end:**'));
assert.ok(e2e !== -1, 'the report template has no **End to end:** line');
assert.equal(lines[e2e + 1], '**Seen with `run`:** {yes | no — {reason}}',
'the line after **End to end:** is not the "Seen with `run`" line in its yes / no — {reason} form');
});
test('run_seen: the summary JSON carries "run_seen" right after "end_to_end"', () => {
const text = read(EXEC);
const sumIdx = text.indexOf('"trekexecute_summary": {');
assert.ok(sumIdx !== -1, 'summary JSON block missing');
const lines = text.slice(sumIdx, text.indexOf('\n```', sumIdx)).split('\n').map((l) => l.trim());
const e2e = lines.findIndex((l) => l.startsWith('"end_to_end":'));
assert.ok(e2e !== -1, 'the summary JSON has no "end_to_end" key');
assert.equal(lines[e2e + 1], '"run_seen": "{yes | no — reason}",',
'the key after "end_to_end" is not "run_seen" with its yes / no — reason value');
});
test('run_seen: the End to end section says to record run_seen, also when no app was seen, and keeps the End-to-end: command as the verdict', () => {
const s = sectionText(read(EXEC), PARTS[1][1]).replace(/\s+/g, ' ');
assert.match(s, /Record `run_seen`: `yes` when the app was seen and driven with Claude Code's built-in `run` in this run, else `no — \{reason\}`/,
'the End to end section does not say to record run_seen as yes, or no with a reason');
assert.match(s, /no app to launch/, 'the no-form does not name the run with no app to launch');
assert.match(s, /The verdict is still the `End-to-end:` command\./,
'the End to end section no longer says the End-to-end: command is the verdict');
});