voyage/tests/commands/trekexecute.test.mjs
Kjell Tore Guttormsen 2a35d067d9
chore(cleanup): R-b R3 — profiles, phase signals and the effort dialog removed, one fixed path per command
The four profiles, the resolver, the profile validator and docs/profiles.md go;
/trekbrief Phase 3.5 and the brief's phase_signals checks go (an older brief's
key is ignored); MANIFEST_PROFILE_DRIFT and profile_used go. Every Profile,
Composition rule, Sequencing gate and High-effort section leaves the commands.
Decision 209: /trekresearch runs the full swarm with contrarian-researcher as
wave 2 every run; /trekreview and the coordinator run all four passes with no
high-effort normalization. Voyage agents are spawned with no model (their own
model: opus applies); the two built-in spawns pass model "opus". --profile
stays in the parser as a retired flag that swallows its value. §Model & effort
moves to docs/architecture.md; the sequencing-gate eval case goes.

Files removed: 44 (43 from the R3 list + tests/commands/trekreview.test.mjs,
whose every test pinned removed text). Files edited: 43.
Tests: 2467 (2466 pass / 0 fail / 1 skipped) before, 2309 (2309 pass / 0 fail /
0 skipped) after. Durable check: all three groups green, none pending.

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

341 lines
18 KiB
JavaScript

// tests/commands/trekexecute.test.mjs
// Prose-pin and runtime tests for /trekexecute.
import { after, test } from 'node:test';
import { strict as assert } from 'node:assert';
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { spawnSync } from 'node:child_process';
import { sectionText } from '../helpers/md-section.mjs';
import { childEnv } from '../helpers/child-env.mjs';
const HERE = dirname(fileURLToPath(import.meta.url));
const ROOT = join(HERE, '..', '..');
const COMMAND_FILE = join(ROOT, 'commands', 'trekexecute.md');
function read() { return readFileSync(COMMAND_FILE, 'utf8'); }
// --- S38 loop-discipline hardening: completion gate (Step 2) ---
test('trekexecute — machine-verifiable completion gate + stop-signal contract literals present (SC a)', () => {
const text = read();
assert.ok(text.includes('machine-verifiable completion gate'),
'completion-gate literal must be grep-able');
assert.ok(text.includes('stop-signal contract'),
'stop-signal contract literal must be grep-able');
});
test('trekexecute — completion gate anchors result:completed to Phase 7.5 audit + DONE-after-check (SC a)', () => {
const text = read();
const gateIdx = text.indexOf('machine-verifiable completion gate');
assert.ok(gateIdx >= 0, 'gate literal missing');
const section = text.slice(gateIdx, gateIdx + 1400);
assert.match(section, /Phase 7\.5/, 'gate must name Phase 7.5 as the objective predicate');
assert.match(section, /DONE/, 'gate must reference the DONE stop-signal token');
assert.match(section, /emitted AFTER/, 'gate must require DONE emitted AFTER the audit ran');
});
// --- S38 loop-discipline hardening: caps + global recovery budget (Step 3) ---
test('trekexecute — global recovery/retry budget TREKEXECUTE_MAX_RECOVERY_ITERATIONS default 25 (SC b)', () => {
const text = read();
assert.ok(text.includes('TREKEXECUTE_MAX_RECOVERY_ITERATIONS'),
'global recovery/retry budget constant must be documented');
assert.match(
text,
/TREKEXECUTE_MAX_RECOVERY_ITERATIONS[^\n]{0,40}\b25\b|default[^\n]{0,20}\b25\b[^\n]{0,40}aggregate/i,
'global budget must document numeric default 25 adjacent to the constant',
);
});
test('trekexecute — every recovery/retry loop bounded + 3-axis cap hierarchy (SC b)', () => {
const text = read();
// Wave 1 (P8): the per-step cap is at most 5 fix rounds, and they go UP (to the
// plan, then to the operator) - never sideways into a fourth guess, never down
// a ladder of cheaper models. The old "3 attempts" cap is gone.
assert.match(text, /maximum 5 fix rounds[^\n]*upward/i, 'per-step cap must be "maximum 5 fix rounds", escalating upward');
assert.doesNotMatch(text, /maximum 2 retries|Retry cap = 3 attempts/, 'the old 3-attempt retry cap must be gone');
assert.match(text, /recovery_depth < 2/, 'recovery-dispatch cap must be explicit');
const capIdx = text.indexOf('Iteration caps');
assert.ok(capIdx >= 0, 'cap-hierarchy section ("Iteration caps") must exist');
const section = text.slice(capIdx, capIdx + 1500);
assert.match(section, /fix_rounds/, 'hierarchy must name the per-step fix_rounds axis');
assert.match(section, /attempts/, 'hierarchy must name per-step attempts axis');
assert.match(section, /recovery_depth/, 'hierarchy must name per-session recovery_depth axis');
assert.match(section, /TREKEXECUTE_MAX_RECOVERY_ITERATIONS/, 'hierarchy must name the global budget axis');
});
// --- S38 loop-discipline hardening: iterations_remaining budget signal (Step 4) ---
test('trekexecute — iterations_remaining surfaced in progress schema + summary JSON, no (if wired) (SC c)', () => {
const text = read();
assert.ok(text.includes('iterations_remaining'), 'iterations_remaining must be present (SC c)');
assert.ok(!/iterations_remaining[^\n]*\(if wired\)/.test(text),
'no "(if wired)" conditional — field present in the spec at minimum');
const schemaIdx = text.indexOf('### Progress file schema');
assert.ok(schemaIdx >= 0, 'progress schema section missing');
assert.match(text.slice(schemaIdx, schemaIdx + 1400), /iterations_remaining/,
'progress schema must include iterations_remaining');
const sumIdx = text.indexOf('"trekexecute_summary": {');
assert.ok(sumIdx >= 0, 'summary JSON block missing');
assert.match(text.slice(sumIdx, sumIdx + 900), /iterations_remaining/,
'summary JSON must include iterations_remaining (observable)');
});
test('trekexecute — iterations_remaining decrement + backfill + gate cross-check documented (SC c)', () => {
const text = read();
assert.match(text, /decrement[^.\n]*iteration|every recovery\/retry iteration/i,
'decrement-per-iteration rule must be documented');
assert.match(text, /backfill|seed (it )?to (the )?cap/i,
'backfill-on-absent (legacy resume) rule must be documented');
const gateIdx = text.indexOf('machine-verifiable completion gate');
const gate = text.slice(gateIdx, gateIdx + 2200);
assert.match(gate, /iterations_remaining/, 'gate must cross-check iterations_remaining');
assert.match(gate, /recovery_depth/, 'gate cross-check must reconcile against recovery_depth');
assert.match(gate, /attempts/, 'gate cross-check must reconcile against attempts');
});
// --- helpers for the run-the-line tests below -------------------------------
/** Whitespace (line breaks too) collapsed to one space. */
const flat = (s) => s.replace(/\s+/g, ' ').trim();
/** The single line in `text` that runs `lib/<mod>` (and holds `verb`, when given). */
function libLine(text, mod, verb = '') {
const esc = mod.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&');
const re = new RegExp(`^\\s*node \\$\\{CLAUDE_PLUGIN_ROOT\\}/lib/${esc}\\b`);
const hits = text.split('\n').filter((l) => re.test(l) && l.includes(verb));
assert.equal(hits.length, 1, `expected exactly one \`${mod}${verb ? ` ${verb}` : ''}\` line; found ${hits.length}`);
return hits[0].trim();
}
/** Run a command line from the prose with the plugin root filled in. */
function bash(cmd, cwd) {
return spawnSync('bash', ['-c', cmd.split('${CLAUDE_PLUGIN_ROOT}').join(ROOT)], { cwd, encoding: 'utf8' });
}
// --- Phase 8: the state-file block runs from any working directory ----------
test('trekexecute Phase 8: the state-file block imports through the plugin root and runs outside it', () => {
const md = read();
assert.doesNotMatch(md, /from '\.\/lib\//, 'a relative import only works with cwd = plugin root');
const start = md.indexOf('node --input-type=module -e "\nimport path from \'node:path\';');
assert.ok(start > 0, 'the state-file block');
const end = md.indexOf('\n```', start);
const block = md.slice(start, end)
.replaceAll('${CLAUDE_PLUGIN_ROOT}', ROOT)
.replace("'{project_dir}' '{next_session_brief_path}' '{next_session_label}' '{status}'", '"$1" brief.md "Session 2" ready');
const elsewhere = mkdtempSync(join(tmpdir(), 'voyage-state-'));
try {
const r = spawnSync('/bin/bash', ['-c', block, 'x', elsewhere], { cwd: elsewhere, encoding: 'utf8' });
assert.equal(r.status, 0, r.stderr);
assert.ok(existsSync(join(elsewhere, '.session-state.local.json')));
assert.match(readFileSync(join(elsewhere, 'NEXT-SESSION-PROMPT.local.md'), 'utf8'), /produced_by: trekexecute/);
} finally {
rmSync(elsewhere, { recursive: true, force: true });
}
});
// --- Phase 1 --project: the brief check -------------------------------------
// Phase 1's --project branch runs the brief-validator line /trekplan runs, and
// never halts on what it reports.
/** Phase 1's rule 1 (the --project branch), as lines. */
function projectRule() {
const sec = sectionText(read(), '## Phase 1 — Parse mode and validate input').split('\n');
const start = sec.findIndex((l) => l.startsWith('1. If arguments contain `--project <dir>`'));
assert.ok(start !== -1, 'Phase 1 must still open with the --project rule');
const end = sec.findIndex((l, i) => i > start && /^2\. /.test(l));
assert.ok(end !== -1, 'Phase 1 must still have a rule 2');
return sec.slice(start, end).join('\n');
}
test('trekexecute Phase 1 --project runs brief-validator --soft --json and never halts on its errors', () => {
const rule = projectRule();
const validator = libLine(rule, 'validators/brief-validator.mjs', '--soft --json');
assert.equal(validator, 'node ${CLAUDE_PLUGIN_ROOT}/lib/validators/brief-validator.mjs --soft --json "{dir}/brief.md"');
const f = flat(rule);
for (const s of [
'The validator exits 1 when its `errors` are not empty; read its JSON either way.',
'Any error is printed in one line and the run continues.',
'A validator that cannot run (no JSON) is "not measured" in one line, and the run continues.',
'No `{dir}/brief.md`: skip the line.',
]) assert.ok(f.includes(s), `the --project rule is missing: ${s}`);
// The line runs as printed: a brief with an error exits 1 and still prints JSON.
const dir = mkdtempSync(join(tmpdir(), 'trekexecute-project-'));
try {
writeFileSync(join(dir, 'brief.md'), '---\ntype: trekbrief\n---\n\n# Task\n');
const v = bash(validator.split('{dir}').join(dir), ROOT);
assert.equal(v.status, 1, `validator: ${v.stdout}${v.stderr}`);
assert.ok(JSON.parse(v.stdout).errors.some((e) => e.code === 'BRIEF_MISSING_FIELD'), v.stdout);
} finally { rmSync(dir, { recursive: true, force: true }); }
});
// --- The merge lock refuses a review of an older commit ---------------------
// The merge lock (Phase 7.2 step 4 and the Phase 8 Main-merge gate) runs the
// verdict gate with `--repo .`, so a review of an older commit than the one
// being merged is refused (`review-stale`), and only the closing run's own
// review opens the merge.
const MERGE_GATE = '### Main-merge gate (MAIN_MERGE_GATE — always pauses)';
const PHASE_72 = '## Phase 7.2 — Simplify, review, verdict (P11)';
const LOCK_LINE = 'node ${CLAUDE_PLUGIN_ROOT}/lib/review/verdict-gate.mjs --json "{review_path}" --repo .';
const STEP4_LINE = 'node ${CLAUDE_PLUGIN_ROOT}/lib/review/verdict-gate.mjs --json "{project_dir}/review.md" --repo .';
/** A review_version 1.1 review.md of `sha`: ALLOW with no findings, or BLOCK over one BLOCKER. */
function lockReview(verdict, sha) {
const id = 'd'.repeat(40);
const findings = verdict === 'BLOCK'
? [{ id, severity: 'BLOCKER', rule_key: 'MISSING_TEST', file: 'a.mjs', line: 1, title: 't', detail: 'd', recommended_action: 'Add a test.' }]
: [];
const counts = { BLOCKER: findings.length, MAJOR: 0, MINOR: 0, SUGGESTION: 0 };
const block = JSON.stringify({ verdict, counts, allow_blocked_by: [], findings }, null, 2);
return [
'---', 'type: trekreview', 'review_version: "1.1"', 'task: "t"', 'slug: s',
'project_dir: .claude/projects/2026-10-23-s/', 'brief_path: .claude/projects/2026-10-23-s/brief.md',
`scope_sha_end: ${sha}`, 'reviewed_files_count: 1', `verdict: ${verdict}`,
findings.length ? `findings:\n - ${id}` : 'findings: []', '---', '', '# Review: t', '',
'## Executive Summary', '', 'x', '', '## Coverage', '', '| File | Treatment | Reason |', '|---|---|---|',
'| a.mjs | summary-only | default |', '', '## Remediation Summary', '', 'x', '', '```json', block, '```', '',
].join('\n');
}
/**
* A throwaway repo (no git hooks): the reviewed commit, then a later commit
* that changes source. `review(verdict, sha)` writes review.md outside the
* repo and returns its folder.
*/
function staleRepo() {
const base = mkdtempSync(join(tmpdir(), 'trekexecute-lock-'));
const repo = join(base, 'repo');
const out = join(base, 'project');
mkdirSync(repo);
mkdirSync(out);
mkdirSync(join(base, 'hooks-none'));
const g = (...args) => {
const r = spawnSync('git', args, { cwd: repo, encoding: 'utf8', env: childEnv() });
assert.equal(r.status, 0, `git ${args.join(' ')}: ${r.stderr}`);
return r.stdout.trim();
};
g('init', '-q', '-b', 'main');
g('config', 'user.name', 'Test');
g('config', 'user.email', 'test@example.invalid');
g('config', 'commit.gpgsign', 'false');
g('config', 'core.hooksPath', join(base, 'hooks-none'));
writeFileSync(join(repo, 'a.mjs'), 'export const a = 1;\n');
g('add', 'a.mjs');
g('commit', '-q', '-m', 'feat: the reviewed work');
const reviewed = g('rev-parse', 'HEAD');
writeFileSync(join(repo, 'a.mjs'), 'export const a = 2;\n');
g('add', 'a.mjs');
g('commit', '-q', '-m', 'feat: a later source change');
const head = g('rev-parse', 'HEAD');
const review = (verdict, sha) => {
writeFileSync(join(out, 'review.md'), lockReview(verdict, sha));
return out;
};
return { repo, reviewed, head, review, cleanup: () => rmSync(base, { recursive: true, force: true }) };
}
let sharedStale = null;
after(() => sharedStale?.cleanup());
/** The file's one staleRepo, built on first use: its tests only rewrite review.md, outside the repo. */
function staleFixture() {
if (!sharedStale) sharedStale = staleRepo();
return sharedStale;
}
/** The lock line with its review path filled in for review.md in `dir`. */
const fillLock = (cmd, dir) => cmd.split('{review_path}').join(join(dir, 'review.md')).split('{project_dir}').join(dir);
/** The lock line, filled for `dir`, run in `repo` through bash(). */
const runLock = (cmd, dir, repo) => bash(fillLock(cmd, dir), repo);
test('trekexecute merge lock: the Main-merge gate and Phase 7.2 step 4 each hold exactly one verdict-gate.mjs line, with --repo .', () => {
const text = read();
assert.equal(libLine(sectionText(text, MERGE_GATE), 'review/verdict-gate.mjs'), LOCK_LINE,
'the Phase 8 lock does not run the verdict gate against the repo being merged');
assert.equal(libLine(sectionText(text, PHASE_72), 'review/verdict-gate.mjs'), STEP4_LINE,
'Phase 7.2 step 4 does not run the verdict gate against the repo being merged');
});
test('trekexecute merge lock: the extracted lock lines, run on an ALLOW review of an older commit with a later source commit, exit 2 with review-stale', () => {
const text = read();
const lines = [libLine(sectionText(text, MERGE_GATE), 'review/verdict-gate.mjs'), libLine(sectionText(text, PHASE_72), 'review/verdict-gate.mjs')];
const fx = staleFixture();
const filled = lines.map((cmd) => fillLock(cmd, fx.review('ALLOW', fx.head)));
assert.equal(filled[0], filled[1], 'both lock lines fill to the same command');
for (const cmd of new Set(filled)) {
fx.review('ALLOW', fx.head);
const ok = bash(cmd, fx.repo);
assert.equal(ok.status, 0, `control: an ALLOW review of HEAD opens the lock: ${ok.stdout}${ok.stderr}`);
fx.review('ALLOW', fx.reviewed);
const r = bash(cmd, fx.repo);
assert.equal(r.status, 2, `${cmd}: an ALLOW review of an older commit must exit 2: ${r.stdout}${r.stderr}`);
const reason = JSON.parse(r.stdout).reason;
assert.ok(reason.startsWith('review-stale'), `the reason must start review-stale: ${reason}`);
}
});
test('trekexecute merge lock: Phase 7.2 step 4 run on a BLOCK review of an older commit still exits 1, so the fix loop opens', () => {
const step4 = libLine(sectionText(read(), PHASE_72), 'review/verdict-gate.mjs');
const fx = staleFixture();
const r = runLock(step4, fx.review('BLOCK', fx.reviewed), fx.repo);
assert.equal(r.status, 1, `a BLOCK review must exit 1 before any staleness check: ${r.stdout}${r.stderr}`);
assert.equal(JSON.parse(r.stdout).verdict, 'BLOCK');
});
// --- No gates escalation (fix round 4-5a, #33).
test('[#33] no gates_mode = \'closed\', "strictest" or --event gates_mode_flag_override is left', () => {
const text = read();
const f = flat(text);
assert.doesNotMatch(f, /gates_mode = 'closed'/);
assert.doesNotMatch(f, /strictest/);
assert.doesNotMatch(f, /halts immediately/);
assert.doesNotMatch(text, /--event gates_mode_flag_override/);
assert.doesNotMatch(f, /gates_mode = '(adaptive|open)'/);
});
// --- Phase 8 writes the merge evidence file before the merge call (4.5b2, MEV) --
/** The text from `heading` up to the next `## ` heading; the heading must exist. */
function untilNextH2(text, heading) {
const start = text.indexOf(heading);
assert.ok(start >= 0, `heading missing: ${heading}`);
const next = text.indexOf('\n## ', start + heading.length);
return text.slice(start, next === -1 ? undefined : next);
}
/** Every sentence of `text` (flattened). */
const sentences = (text) => flat(text).split(/(?<=\.)\s+(?=[A-Z`"*(])/);
const PHASE_8 = '## Phase 8 — Final report';
const EVIDENCE_CMD = 'node ${CLAUDE_PLUGIN_ROOT}/lib/review/merge-evidence.mjs write --repo . --branch "{run branch}" --project "{project_dir}" --json';
test('[MEV] Phase 8 writes the evidence file before the merge call', () => {
const p8 = untilNextH2(read(), PHASE_8);
const open = p8.indexOf('**2. Open: the merge, and the one question.**');
assert.ok(open >= 0, 'Phase 8 has its "2. Open" part');
const lines = p8.split('\n');
const at = lines.findIndex((l) => l.trim() === EVIDENCE_CMD);
assert.ok(at >= 0, `Phase 8 runs the evidence writer: ${EVIDENCE_CMD}`);
// A Bash call of its own: the command is the one line of its fenced block.
assert.match(lines[at - 1].trim(), /^```bash$/, 'the writer opens its own bash block');
assert.equal(lines[at + 1].trim(), '```', 'the writer is the only line of its block');
const writer = p8.indexOf(EVIDENCE_CMD);
assert.ok(writer > open, 'the writer runs in "2. Open"');
const merges = [p8.indexOf('merge --no-ff "{run branch}"'), p8.indexOf('git switch "{main}"')];
for (const m of merges) assert.ok(m > writer, 'the writer runs before each merge block');
assert.ok(sentences(untilNextH2(read(), PHASE_8)).some((s) => s.includes('On `"written": false` (exit 1), print its `reason` in one line and continue')
&& /no run evidence/.test(s)), 'a refused write is said in one line and the run goes on; the question then says "no run evidence"');
// Control: the merge blocks are unchanged.
assert.ok(p8.includes(' if git -C "{main worktree path}" merge --no-ff "{run branch}" -m "merge: trekexecute {slug}"; then\n echo "MERGED"'),
'Control: the worktree merge block is unchanged');
assert.ok(p8.includes(' git switch "{main}" || { echo "MERGE_REFUSED"; exit 1; }\n if git merge --no-ff "{run branch}" -m "merge: trekexecute {slug}"; then'),
'Control: the switch merge block is unchanged');
});