99 lines
4.5 KiB
JavaScript
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}`);
|
|
});
|
|
}
|