voyage/tests/lib/executor-prose-pins.test.mjs
Kjell Tore Guttormsen 44dcc779a9
fix(execute): the step loop survives an adversarial review
An independent review of abc3083 found 15 defects, the two worst verified
in scratch repos. Each fix started with a test that failed.

- Proof of red could be "proven" by the environment: a start point
  unpacked with git archive had no node_modules and no .git, so any test
  failed there. The start point is now a git clone --shared checked out at
  start_sha, gitignored top-level entries are linked in, and a control run
  (start point + the change) must pass, or the result is not-run.
- The Test first file was never committed (the template lists it apart
  from Files:) and leaked into the next step's diff. The step's scope is
  now Files: + test files, staged at the checkpoint.
- New files outside Files: were invisible to every check. Every untracked
  file is registered with --intent-to-add; a revert unregisters them.
- The floor guard paired renames (foo.test.js -> .bak read as clean) and
  missed skipIf and node's t.skip/t.todo; prose files no longer count as
  stubs.
- Phase 7.5 audited from the last step's start_sha; it now uses the run's
  session_start_sha.
- The done line is written after the checkpoint and checked with --commit.
- fix_rounds never passes 5; attempts are never reset; re-control keeps
  the spec/quality verdicts and earlier floor-guard rulings; the closed
  stop list maps every existing stop; failed vs stopped made consistent;
  the controller's effort is said, not assumed.

Tests: 1470 (1468 pass, 0 fail, 2 skipped).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 20:17:39 +02:00

244 lines
12 KiB
JavaScript

// tests/lib/executor-prose-pins.test.mjs
// Wave 1, process 1 (the executor): prose pins for P8 (fresh helper per step,
// one controller with two verdicts, fix rounds that go up), P9 on the executor
// side (proof of red, the floor guard, the done line, end to end, back to the
// architecture after three failed fixes) and P4 (the closed stop list and the
// decision log). The mechanisms themselves are tested as code in
// tests/lib/{red-proof,test-integrity,done-line}.test.mjs; these pins hold the
// command prose to them. Fix the SOURCE a pin guards, not the pin.
import { test } from 'node:test';
import { strict as assert } from 'node:assert';
import { existsSync, readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const HERE = dirname(fileURLToPath(import.meta.url));
const ROOT = join(HERE, '..', '..');
const read = (rel) => readFileSync(join(ROOT, rel), 'utf8');
const CMD = 'commands/trekexecute.md';
const AGENT = 'agents/step-implementer.md';
function frontmatter(text) {
const m = text.match(/^---\n([\s\S]*?)\n---/);
assert.ok(m, 'frontmatter missing');
return m[1];
}
const fmValue = (fm, key) => (fm.match(new RegExp(`^${key}:\\s*(.+)$`, 'm')) || [])[1];
/** The text between `## {start}` (or `#### {start}`) and the next heading of the same or higher level. */
function section(text, heading) {
const at = text.indexOf(heading);
assert.ok(at >= 0, `${CMD} must have "${heading}"`);
const level = heading.match(/^#+/)[0].length;
const rest = text.slice(at + heading.length);
const next = rest.search(new RegExp(`\\n#{1,${level}} `));
return heading + (next >= 0 ? rest.slice(0, next) : rest);
}
// --- P8: the Agent tool is back, for one purpose -------------------------------
test('P8: /trekexecute may spawn - Agent is allowed and no longer disallowed', () => {
const fm = frontmatter(read(CMD));
assert.match(fmValue(fm, 'allowed-tools'), /\bAgent\b/, 'allowed-tools must list Agent');
const disallowed = fmValue(fm, 'disallowed-tools') || '';
assert.doesNotMatch(disallowed, /\bAgent\b/, 'disallowed-tools must no longer remove Agent');
assert.match(disallowed, /\bTeamCreate\b/, 'TeamCreate stays removed - helpers, never a team');
});
test('P8: Hard rule 10 is rewritten - one fresh helper per plan step, and helpers never start helpers', () => {
const text = read(CMD);
assert.doesNotMatch(text, /\*\*No sub-agents\.\*\*/, 'the old "No sub-agents" rule must be gone');
const rule = text.slice(text.indexOf('10. **'), text.indexOf('11. **'));
assert.match(rule, /fresh helper per plan step/i);
assert.match(rule, /voyage:step-implementer/);
assert.match(rule, /helpers never start helpers/i);
assert.match(rule, /controller/i);
});
test('P8: the step agent exists, is opus at effort >= high, and cannot spawn', () => {
assert.ok(existsSync(join(ROOT, AGENT)), `${AGENT} must exist`);
const fm = frontmatter(read(AGENT));
assert.equal(fmValue(fm, 'name'), 'step-implementer');
assert.equal(fmValue(fm, 'model'), 'opus');
assert.ok(['high', 'xhigh', 'max'].includes(fmValue(fm, 'effort')), 'effort must be >= high');
assert.doesNotMatch(fmValue(fm, 'tools') || '', /\bAgent\b/, 'a helper must not hold the Agent tool');
// Measured 2026-10-06 (CC 2.1.291): a worktree-isolated helper branched from
// the default branch (02f320f), not from the executor's HEAD (2c89a6b), and so
// did not see the earlier steps. A plugin cannot set worktree.baseRef. So no
// isolation: the helper works in the executor's tree, from a recorded start_sha.
assert.doesNotMatch(fm, /^isolation:/m, 'no isolation: worktree on the step agent (measured: wrong base)');
});
test('P8: the helper report contract is ran / saw / did NOT check', () => {
const body = read(AGENT);
for (const h of ['### Ran', '### Saw', '### Did NOT check']) assert.ok(body.includes(h), `${AGENT} report must have "${h}"`);
assert.ok(read(CMD).includes('Did NOT check'), 'the command must name the report contract too');
});
test('P8: hand-over is in files, from a recorded start point', () => {
const text = read(CMD);
assert.match(text, /start_sha/);
assert.match(text, /report_path/);
assert.match(text, /git diff \{start_sha\}/, 'the review package is the diff from the recorded start point');
});
test('P8: one controller, two verdicts, reads the diff not the summary, re-checks only what was fixed', () => {
const ctl = section(read(CMD), '#### Sub-step E — Control');
assert.match(ctl, /one controller/i);
assert.match(ctl, /two verdicts/i);
assert.match(ctl, /"spec"/);
assert.match(ctl, /"quality"/);
assert.match(ctl, /reads the diff, not the helper's (summary|report)/i);
assert.match(ctl, /only what (was|the round) fixed/i);
assert.match(ctl, /general-purpose/, 'the controller is a general helper with a prompt from the command, not agent no. 2');
});
test('P8: fix rounds go up - maximum 5, plan then operator, never a cheaper-model ladder', () => {
const fix = section(read(CMD), '#### Sub-step F — Fix rounds');
assert.match(fix, /maximum 5 fix rounds[^\n]*upward/i);
assert.match(fix, /back to (the )?planning/i);
assert.match(fix, /operator/i);
assert.match(fix, /does not improvise/i);
assert.match(fix, /no ladder of cheaper models/i);
// P9 (7): after three failed fixes the question is the architecture, not a fourth fix.
assert.match(fix, /three failed fix(es| rounds)[^\n]*architecture/i);
});
test('P8: the no-name rule covers the step helper and the controller', () => {
const text = read(CMD);
assert.ok(text.includes('in_process_teammate'));
assert.match(text, /never pass .*`name`/i);
assert.match(text, /^Launch\b/m, 'the spawn site starts with "Launch" (the S85 detector reads it)');
});
// --- P9 (executor side) ---------------------------------------------------------
test('P9 (1): proof of red is a command and it blocks - the old "do not block" sentence is gone', () => {
const text = read(CMD);
assert.doesNotMatch(text, /Do not block on test-first failures/);
assert.match(text, /lib\/verification\/red-proof\.mjs/);
const red = section(text, '#### Sub-step B — Test first and proof of red');
assert.match(red, /must FAIL/);
assert.match(red, /must PASS/);
assert.match(red, /not-run/);
assert.match(red, /never (a pass|green|proven)/i);
});
test('K4: Test first is required from plan_version 1.8; Interface in/out and Review focus are read, not required', () => {
const text = read(CMD);
assert.match(text, /plan_version ≥ 1\.8[^\n]*`Test first:`|`Test first:`[^\n]*plan_version ≥ 1\.8/);
for (const f of ['Interface in:', 'Interface out:', 'Review focus:']) assert.ok(text.includes(f), `${CMD} must read ${f}`);
assert.match(text, /tolerat(e|es) (their|its) absence|never required/i);
});
test('K8: the test lock is used through its CLI, and a missing CLI is said out loud', () => {
const text = read(CMD);
assert.match(text, /lib\/util\/test-lock\.mjs lock --files/);
assert.match(text, /test-lock\.mjs unlock/);
assert.match(text, /"not-locked"/);
assert.match(text, /never silent/i);
});
test('P9 (2): the floor guard runs on the step diff and exit 2 is never green', () => {
const d3 = section(read(CMD), '#### Sub-step D3 — Floor guard');
assert.match(d3, /lib\/verification\/test-integrity\.mjs --base \{start_sha\}/);
assert.match(d3, /exit 2 is never green/i);
assert.match(d3, /justified/);
});
test('P9 (4): the done line is written by a script, only when the test command exits 0, at the step commit', () => {
const g = section(read(CMD), '#### Sub-step G — Checkpoint, done line, unlock');
assert.match(g, /lib\/verification\/done-line\.mjs --step \{N\}/);
assert.match(g, /done-line\.mjs --check --step \{N\} --report \{report_path\} --commit \{hash\}/);
assert.match(g, /writes its line only when the test command\s+exits 0/i);
// The checkpoint comes first, so the line names the step's own commit.
assert.ok(g.indexOf('**Checkpoint.**') < g.indexOf('**The done line**'), 'checkpoint before the done line');
});
// --- the independent review of 2026-10-06 (15 findings): what the fixes must keep
test('review: the step test files are part of the scope and committed at the checkpoint', () => {
const text = read(CMD);
assert.match(section(text, '#### Sub-step B — Test first and proof of red'), /scope = its `Files:` \+ its test files/);
assert.match(section(text, '#### Sub-step G — Checkpoint, done line, unlock'), /its `Files:` and its test files/);
});
test('review: every new file is registered (not only listed ones), so no check is blind to it', () => {
const c = section(read(CMD), '#### Sub-step C — Implement through one fresh helper');
assert.match(c, /git ls-files --others --exclude-standard/);
assert.match(c, /git add --intent-to-add -- \{path\}/);
});
test('review: proof of red has a control run and a real git checkout as its start point', () => {
const b = section(read(CMD), '#### Sub-step B — Test first and proof of red');
assert.match(b, /control run/);
assert.match(b, /git clone --shared/);
});
test('review: Phase 7.5 audits the run from session_start_sha, never a step start_sha', () => {
const text = read(CMD);
const audit = text.slice(text.indexOf('## Phase 7.5'), text.indexOf('### Machine-verifiable completion gate'));
assert.match(audit, /\{session_start_sha\}\.\.HEAD/);
assert.doesNotMatch(audit, /\{start_sha\}/);
});
test('review: no sixth round, attempts never reset, a step at 5 is never built again', () => {
const text = read(CMD);
assert.match(section(text, '#### Sub-step F — Fix rounds'), /no sixth round/);
const c = section(text, '#### Sub-step C — Implement through one fresh helper');
assert.match(c, /never\s+reset/);
assert.match(c, /A step at `fix_rounds` 5 is never built again/);
});
test('review: the helper appends its report, and passes tests as one comma-separated list', () => {
const agent = read(AGENT);
assert.match(agent, /APPEND it to `report_path`/);
assert.match(agent, /ONE comma-separated argument/);
assert.match(agent, /A step with no `Test first:`/);
});
test('P9 (6): without a run-the-thing check the result is "not verified end to end", never green', () => {
const text = read(CMD);
assert.match(text, /not verified end to end/);
assert.match(text, /end_to_end/);
});
test('P9: the three new CLIs the prose names exist', () => {
for (const f of ['red-proof.mjs', 'test-integrity.mjs', 'done-line.mjs']) {
assert.ok(existsSync(join(ROOT, 'lib', 'verification', f)), `lib/verification/${f}`);
}
});
// --- P4: the closed stop list and the decision log ------------------------------
test('P4: the run stops only on the closed list of four, and the final report shows it', () => {
const text = read(CMD);
const list = section(text, '### Closed stop list');
for (const cls of [/cannot be undone/i, /security-sensitive/i, /outside the working copy/i, /so broken that (every|any)thing (after it )?is guesswork/i]) {
assert.match(list, cls);
}
assert.match(list, /closed/i);
// Phase 8's report template is a fence that holds its own "## " heading, so
// the phase is read up to Phase 9, not up to the next heading.
const report = text.slice(text.indexOf('## Phase 8 — Final report'), text.indexOf('## Phase 9'));
assert.match(report, /### Stops/);
assert.match(report, /### Decisions taken/);
});
test('P4: every decision the executor took is logged with what it costs if it is wrong', () => {
const text = read(CMD);
assert.match(text, /cost_if_wrong/);
assert.match(text, /"decisions"/);
});
// --- Credits: every borrowing is named, never silent ----------------------------
test('every borrowing in the executor is credited', () => {
const text = read(CMD);
const credits = section(text, '## Credits');
for (const src of [/Superpowers/, /subagent-driven-development/, /ai-native-sdlc/, /Kiro/, /Spec Kit/, /agent-skills/, /Cherny/]) {
assert.match(credits, src);
}
assert.match(credits, /MIT/);
});