// 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}`); }); }