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>
190 lines
10 KiB
JavaScript
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]}`);
|
|
});
|