voyage/tests/lib/run-coupling-prose-pins.test.mjs
Kjell Tore Guttormsen 029e83117c
test(r8): move the live tests the move map missed
33 more tests of live behaviour from the deleted round files, each in
the test file named after its source and seen failing under a mutation
of that source (log in the project folder). Two stay dropped: one pinned
the removed gate states, one the old Phase 1.5 file comparison. The two
trekexecute.md sentences that still said /trekcontinue compares the two
prompt files now say it uses the project-folder file (line count kept).

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

190 lines
10 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');
});
// /trekexecute keeps `run_seen` in the progress file (template, field table,
// Phase 7) and Phase 8 reads it from there, so the resume and multi-session
// paths report it; Handover 5 lists it as optional (fix round 4-5b, E14).
/** The text from the line holding `start` up to (not including) the next line holding `stop`; both must exist. */
function between(rel, start, stop) {
const text = read(rel);
const s = text.indexOf(start);
assert.ok(s !== -1, `${rel} must hold "${start}"`);
const e = text.indexOf(stop, s + start.length);
assert.ok(e !== -1, `${rel} must hold "${stop}" after "${start}"`);
return text.slice(s, e);
}
/** Every sentence of `text` (flattened) that holds `needle`. */
function sentencesWith(text, needle) {
return text.replace(/\s+/g, ' ').trim().split(/(?<=\.)\s+(?=[A-Z`"*(])/).filter((s) => s.includes(needle));
}
/** The progress-file template of /trekexecute's Phase 3, parsed as JSON. */
function progressTemplate() {
const block = between(EXEC, '### Progress file schema', '**The step-loop fields');
const m = block.match(/```json\n([\s\S]*?)\n```/);
assert.ok(m, 'the Progress file schema section holds no ```json block');
return JSON.parse(m[1]);
}
test('[E14] the progress template carries run_seen', () => {
const tpl = progressTemplate();
assert.ok(Object.prototype.hasOwnProperty.call(tpl, 'run_seen'),
'the progress template has no top-level run_seen field');
assert.equal(tpl.run_seen, null, `the progress template's run_seen is not null: ${JSON.stringify(tpl.run_seen)}`);
assert.ok(!Object.prototype.hasOwnProperty.call(tpl.steps['1'], 'run_seen'),
'run_seen sits in the per-step record, not at the top level');
});
test('[E14] the field table describes run_seen', () => {
const table = between(EXEC, '**The step-loop fields', '**The coupling fields');
const rows = table.split('\n').filter((l) => l.startsWith('| `run_seen` |'));
assert.equal(rows.length, 1, `the step-loop field table must hold exactly one run_seen row; found ${rows.length}`);
const cells = rows[0].split(' | ');
assert.match(cells[1], /^Phase 7$/, `the run_seen row is not written in Phase 7: ${rows[0]}`);
assert.match(rows[0], /Claude Code's (built-in )?`run`/, `the run_seen row does not say it is what Claude Code's run showed: ${rows[0]}`);
assert.match(rows[0], /`null` = not run/, `the run_seen row does not say null = not run: ${rows[0]}`);
});
test('[E14] Phase 8 reads run_seen from progress.json', () => {
const p7 = between(EXEC, '### End to end — "run the thing"', '### Architecture rules (K5)');
const rec = sentencesWith(p7, 'Record `run_seen`');
assert.equal(rec.length, 1, `Phase 7 must hold one "Record \`run_seen\`" sentence; found ${rec.length}`);
assert.match(rec[0], /\bin the progress file\b|\bto the progress file\b|`progress\.json`/,
`Phase 7 records run_seen without writing it to the progress file: ${rec[0]}`);
const p8 = between(EXEC, '## Phase 8 — Final report', '## Phase 9 — Stats tracking');
const hits = sentencesWith(p8, '`run_seen`');
const ok = hits.find((s) => /\bread\b|\breads\b/.test(s)
&& /progress file|`progress\.json`/.test(s)
&& /\*\*Seen with `run`:\*\*/.test(s)
&& /summary/.test(s)
&& /resume/.test(s) && /multi-session/.test(s));
assert.ok(ok, `no Phase 8 sentence says the Seen with run line and the summary's run_seen are read from the progress file, so the resume and multi-session paths report it: ${hits.join(' | ')}`);
});
test('[E14] Handover 5 lists run_seen as optional', () => {
const h5 = between('docs/HANDOVER-CONTRACTS.md', '## Handover 5 ', '## Handover 6 ');
const rows = h5.split('\n').filter((l) => l.startsWith('| `run_seen` |'));
assert.equal(rows.length, 1, `Handover 5 must hold exactly one run_seen row; found ${rows.length}`);
const cells = rows[0].split(' | ');
assert.equal(cells[2], 'optional', `the run_seen row's Required cell is not "optional": ${rows[0]}`);
assert.match(rows[0], /additive/, `the run_seen row does not say the field is additive: ${rows[0]}`);
assert.match(rows[0], /Phase 7/, `the run_seen row does not say Phase 7 records it: ${rows[0]}`);
});