voyage/tests/commands/trekplan-explorer-prompts.test.mjs
2026-10-12 19:45:40 +02:00

99 lines
4.5 KiB
JavaScript

// tests/commands/trekplan-explorer-prompts.test.mjs
// Prose pins: every agent /trekplan sends to explore or review is told to read
// local files only inside the repo under exploration (plus the paths its prompt
// gives it), and every exploring agent gets the brief path.
// Blocks are found by a named list; a block that cannot be found fails, so an
// empty scan is never green.
import { test } from 'node:test';
import { strict as assert } from 'node:assert';
import { readFileSync } from 'node:fs';
import { join, dirname } 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 PLAN_CMD = readFileSync(join(ROOT, 'commands', 'trekplan.md'), 'utf8');
const STAY_LINE =
'Read local files only inside the repo under exploration, plus paths this prompt gives you (the brief, the plan, and templates it names).';
/** The text of a level-2 section, up to the next `## ` heading outside a fence. */
function section(heading) {
const s = findSection(PLAN_CMD, heading.replace(/^## /, ''), 2);
assert.ok(s, `missing ${heading}`);
return s.text;
}
/** Collapse line wraps so a sentence wrapped across lines still matches. */
const flat = (s) => s.replace(/\s+/g, ' ');
/**
* The quoted prompt that follows `marker` inside `text`: from `Prompt: "` to
* the next closing `"`. Fails when the marker or the prompt is missing, or
* when the first `Prompt: "` after the marker sits in a later paragraph.
*/
function promptAfter(text, marker, label) {
const at = text.indexOf(marker);
assert.ok(at !== -1, `${label}: marker ${JSON.stringify(marker)} not found`);
const open = text.indexOf('Prompt: "', at);
assert.ok(open !== -1, `${label}: no Prompt: after the marker`);
assert.ok(!text.slice(at, open).includes('\n\n'),
`${label}: the next Prompt: lies in another paragraph, not in this block`);
const start = open + 'Prompt: "'.length;
const close = text.indexOf('"', start);
assert.ok(close !== -1, `${label}: prompt never closes`);
return text.slice(start, close);
}
const P4B = section('## Phase 4b — Brief review');
const P5 = section('## Phase 5 — Parallel exploration (specialized agents + research)');
const P6 = section('## Phase 6 — Targeted deep-dives');
const P9 = section('## Phase 9 — Adversarial review (bounded critic loop)');
function phase6Rule() {
const start = P6.indexOf('spawn a targeted deep-dive agent');
const end = P6.indexOf('Launch up to 3 deep-dive agents');
assert.ok(start !== -1 && end > start, 'Phase 6 deep-dive rule not found');
return P6.slice(start, end);
}
// [label, block text getter, carries {brief_path}?]
const BLOCKS = [
['architecture-mapper', () => promptAfter(P5, '**architecture-mapper**', 'architecture-mapper'), true],
['dependency-tracer', () => promptAfter(P5, '**dependency-tracer**', 'dependency-tracer'), true],
['risk-assessor', () => promptAfter(P5, '**risk-assessor**', 'risk-assessor'), true],
['task-finder', () => promptAfter(P5, '**task-finder**', 'task-finder'), true],
['test-strategist', () => promptAfter(P5, '**test-strategist**', 'test-strategist'), true],
['git-historian', () => promptAfter(P5, '**git-historian**', 'git-historian'), true],
['convention-scanner', () => promptAfter(P5, '**Convention Scanner**', 'convention-scanner'), true],
['research-scout', () => promptAfter(P5, 'launch **research-scout**', 'research-scout'), true],
['Phase 6 deep-dive rule', phase6Rule, true],
['brief-reviewer', () => promptAfter(P4B, '**brief-reviewer**', 'brief-reviewer'), false],
['plan-critic', () => promptAfter(P9, '**plan-critic**', 'plan-critic'), false],
['scope-guardian', () => promptAfter(P9, '**scope-guardian**', 'scope-guardian'), false],
];
test('all 12 named blocks are found (an empty scan fails)', () => {
assert.equal(BLOCKS.length, 12);
let found = 0;
for (const [label, get] of BLOCKS) {
const text = get();
assert.ok(text.trim().length > 0, `${label}: empty block`);
found += 1;
}
assert.equal(found, 12);
});
for (const [label, get] of BLOCKS) {
test(`${label} carries the stay-in-repo line`, () => {
assert.ok(flat(get()).includes(STAY_LINE), `${label} does not carry: ${STAY_LINE}`);
});
}
for (const [label, get, needsBrief] of BLOCKS) {
if (!needsBrief) continue;
test(`${label} is given the brief path`, () => {
assert.ok(get().includes('{brief_path}'), `${label} is not given {brief_path}`);
});
}