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>
377 lines
20 KiB
JavaScript
377 lines
20 KiB
JavaScript
// tests/commands/trekexecute.test.mjs
|
|
// v5.1 prose-pin tests + v5.1.1 runtime SC4 + SC7 tests for /trekexecute.
|
|
// Low effort runs the plan as one session, step after step; it sets no --gates
|
|
// (fix round 4-5a, #33).
|
|
|
|
import { after, test } from 'node:test';
|
|
import { strict as assert } from 'node:assert';
|
|
import { copyFileSync, 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 { resolvePhaseSignal } from '../../lib/profiles/phase-signal-resolver.mjs';
|
|
import { validateBriefContent } from '../../lib/validators/brief-validator.mjs';
|
|
import { parseDocument } from '../../lib/util/frontmatter.mjs';
|
|
import { findSection } from '../../lib/parsers/plan-schema.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');
|
|
const PHASE = 'execute';
|
|
|
|
function read() { return readFileSync(COMMAND_FILE, 'utf8'); }
|
|
function readFixture(name) { return readFileSync(join(ROOT, 'tests', 'fixtures', name), 'utf8'); }
|
|
function frontmatterOf(text) {
|
|
const doc = parseDocument(text);
|
|
return doc.parsed && doc.parsed.frontmatter;
|
|
}
|
|
|
|
// --- Pattern D prose-pins ---
|
|
|
|
test('trekexecute — sequencing-gate surface mentions BRIEF_V51_MISSING_SIGNALS + phase_signals', () => {
|
|
const text = read();
|
|
assert.ok(text.includes('BRIEF_V51_MISSING_SIGNALS'),
|
|
'/trekexecute must surface the BRIEF_V51_MISSING_SIGNALS sequencing gate');
|
|
assert.ok(text.includes('phase_signals'),
|
|
'/trekexecute must reference phase_signals (v5.1 composition rule)');
|
|
});
|
|
|
|
test('trekexecute — low-effort path runs one session, step after step, and sets no --gates open', () => {
|
|
const text = read();
|
|
const compIdx = text.indexOf('## Composition rule (v5.1)');
|
|
assert.ok(compIdx >= 0, 'Composition rule (v5.1) section missing');
|
|
const section = text.slice(compIdx, compIdx + 2000);
|
|
assert.doesNotMatch(section, /--gates open/, 'Low effort must not set --gates open');
|
|
assert.match(section.replace(/\s+/g, ' '), /`effort == 'low'` runs the plan as one session, step after step \(Phase 2\.5\)/,
|
|
'Low-effort path must run the plan as one session, step after step');
|
|
});
|
|
|
|
// --- v5.1.1 runtime SC4 + SC7 ---
|
|
|
|
test('trekexecute — SC4: low-effort fixture → resolver returns {effort: low, model: sonnet}', () => {
|
|
const fm = frontmatterOf(readFixture('brief-effort-low.md'));
|
|
const r = resolvePhaseSignal(fm, PHASE);
|
|
assert.equal(r.effort, 'low');
|
|
assert.equal(r.model, 'sonnet');
|
|
});
|
|
|
|
test('trekexecute — SC4: standard-effort fixture → resolver returns {effort: standard, model: undefined}', () => {
|
|
const fm = frontmatterOf(readFixture('brief-effort-standard.md'));
|
|
const r = resolvePhaseSignal(fm, PHASE);
|
|
assert.equal(r.effort, 'standard');
|
|
assert.equal(r.model, undefined);
|
|
});
|
|
|
|
test('trekexecute — SC4: high-effort fixture → resolver returns {effort: high, model: opus}', () => {
|
|
const fm = frontmatterOf(readFixture('brief-effort-high.md'));
|
|
const r = resolvePhaseSignal(fm, PHASE);
|
|
assert.equal(r.effort, 'high');
|
|
assert.equal(r.model, 'opus');
|
|
});
|
|
|
|
test('trekexecute — SC7: brief_version 2.1 + no phase_signals + no partial → BRIEF_V51_MISSING_SIGNALS', () => {
|
|
const r = validateBriefContent(readFixture('brief-v21-no-signals.md'), { strict: true });
|
|
assert.equal(r.valid, false);
|
|
assert.ok(
|
|
r.errors.find(e => e.code === 'BRIEF_V51_MISSING_SIGNALS'),
|
|
`sequencing gate must fire; errors=${JSON.stringify(r.errors)}`,
|
|
);
|
|
});
|
|
|
|
// --- 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 text of a heading's section, 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;
|
|
}
|
|
|
|
/** 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 and the execute resolver -------------
|
|
// Phase 1's --project branch runs the two lines /trekplan runs, for the execute
|
|
// phase, and halts only on BRIEF_V51_MISSING_SIGNALS.
|
|
|
|
/** 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 the execute resolver, and halts only on BRIEF_V51_MISSING_SIGNALS', () => {
|
|
const rule = projectRule();
|
|
const validator = libLine(rule, 'validators/brief-validator.mjs', '--soft --json');
|
|
const resolver = libLine(rule, 'profiles/resolver.mjs', '--resolve-phase-model --phase execute');
|
|
assert.equal(validator, 'node ${CLAUDE_PLUGIN_ROOT}/lib/validators/brief-validator.mjs --soft --json "{dir}/brief.md"');
|
|
assert.equal(resolver,
|
|
'node ${CLAUDE_PLUGIN_ROOT}/lib/profiles/resolver.mjs --resolve-phase-model --phase execute --brief-path "{dir}/brief.md" [--profile {profile}] --json');
|
|
const f = flat(rule);
|
|
for (const s of [
|
|
'The validator exits 1 when its `errors` are not empty; read its JSON either way.',
|
|
'Halt only when the validator\'s `errors` hold `BRIEF_V51_MISSING_SIGNALS`, with the message from "Sequencing gate surface" below.',
|
|
'Any other error is printed in one line and the run continues.',
|
|
'A validator or resolver that cannot run (no JSON) is "not measured" in one line, and the run continues.',
|
|
'Capture the resolver\'s JSON as `phase_signal_result` (Composition rule below).',
|
|
'No `{dir}/brief.md`: skip both lines.',
|
|
]) assert.ok(f.includes(s), `the --project rule is missing: ${s}`);
|
|
|
|
// The two lines run as printed: the validator names the one halting code for a
|
|
// 2.1 brief without signals, and the resolver hands Phase 2.5 its low effort.
|
|
const dir = mkdtempSync(join(tmpdir(), 'trekexecute-project-'));
|
|
try {
|
|
copyFileSync(join(ROOT, 'tests', 'fixtures', 'brief-v21-no-signals.md'), join(dir, 'brief.md'));
|
|
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_V51_MISSING_SIGNALS'), v.stdout);
|
|
copyFileSync(join(ROOT, 'tests', 'fixtures', 'brief-effort-low.md'), join(dir, 'brief.md'));
|
|
const r = bash(resolver.split('{dir}').join(dir).replace(' [--profile {profile}]', ''), ROOT);
|
|
assert.equal(r.status, 0, `resolver: ${r.stdout}${r.stderr}`);
|
|
assert.equal(JSON.parse(r.stdout).effort, 'low');
|
|
} 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, run in `repo` through bash(). */
|
|
function runLock(cmd, dir, repo) {
|
|
const filled = cmd.split('{review_path}').join(join(dir, 'review.md')).split('{project_dir}').join(dir);
|
|
return bash(filled, 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();
|
|
for (const cmd of lines) {
|
|
const ok = runLock(cmd, fx.review('ALLOW', fx.head), fx.repo);
|
|
assert.equal(ok.status, 0, `control: an ALLOW review of HEAD opens the lock: ${ok.stdout}${ok.stderr}`);
|
|
const r = runLock(cmd, fx.review('ALLOW', fx.reviewed), 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');
|
|
});
|