// tests/commands/trekresearch.test.mjs // v5.1 prose-pin tests + v5.1.1 runtime SC4 + SC7 tests for /trekresearch. import { test } from 'node:test'; import { strict as assert } from 'node:assert'; import { readFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { resolvePhaseSignal } from '../../lib/profiles/phase-signal-resolver.mjs'; import { validateBriefContent } from '../../lib/validators/brief-validator.mjs'; import { parseDocument } from '../../lib/util/frontmatter.mjs'; const HERE = dirname(fileURLToPath(import.meta.url)); const ROOT = join(HERE, '..', '..'); const COMMAND_FILE = join(ROOT, 'commands', 'trekresearch.md'); const PHASE = 'research'; 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('trekresearch — sequencing-gate surface mentions BRIEF_V51_MISSING_SIGNALS + phase_signals', () => { const text = read(); assert.ok(text.includes('BRIEF_V51_MISSING_SIGNALS'), '/trekresearch must surface the BRIEF_V51_MISSING_SIGNALS sequencing gate'); assert.ok(text.includes('phase_signals'), '/trekresearch must reference phase_signals (v5.1 composition rule)'); }); test('trekresearch — low-effort path references --quick equivalent', () => { const text = read(); // Bound the Composition rule section by the next `###` heading rather than a // magic 2000-character window: a fixed count silently drops the match as soon // as prose is inserted above it, turning a real pin into a no-op. const sectionOf = (doc) => { const compIdx = doc.indexOf('## Composition rule (v5.1)'); assert.ok(compIdx >= 0, 'Composition rule (v5.1) section missing'); const nextHeading = doc.indexOf('\n### ', compIdx); return nextHeading > compIdx ? doc.slice(compIdx, nextHeading) : doc.slice(compIdx); }; // (a) positive: the low-effort path is documented inside the bounded section. assert.match(sectionOf(text), /--quick/, 'Low-effort path must mention --quick equivalent'); // (b) negative: an actual removal must still be caught — a bound that can // never fail proves nothing. const mutated = text.replace(/--quick/g, '--removed'); assert.doesNotMatch(sectionOf(mutated), /--quick/, 'heading-bounded slice must still fail on a genuine removal'); }); // --- Step 7: Phase 5 bounded conversation loop (heading-bounded slices) --- // Same bounding discipline as the Composition-rule pin above: slice from the // phase heading to the NEXT phase heading, never a fixed character window. function phaseSlice(doc, startHeading, endHeading) { const start = doc.indexOf(startHeading); assert.ok(start >= 0, `${startHeading} missing`); const end = doc.indexOf(endHeading, start); assert.ok(end > start, `${endHeading} missing — could not bound ${startHeading}`); return doc.slice(start, end); } function phase5(doc) { return phaseSlice(doc, '## Phase 5 —', '## Phase 6 —'); } test('trekresearch — Phase 5 loop is gated on effort == high and names both primitives', () => { const p5 = phase5(read()); assert.match(p5, /effort == 'high'/, 'Phase 5 loop must be gated on effort == \'high\''); assert.match(p5, /research-loop-cap\.mjs/, 'Phase 5 must call the loop-cap shim per turn'); assert.match(p5, /query-privacy-gate\.mjs/, 'Phase 5 must route outbound queries through the privacy gate'); assert.match(p5, /\$\{CLAUDE_PLUGIN_ROOT\}/, 'shim invocations must use the ${CLAUDE_PLUGIN_ROOT} path form'); }); test('trekresearch — Phase 5 declares the loop bound and all three exits', () => { const p5 = phase5(read()); assert.match(p5, /### Loop bound/, 'Phase 5 must carry a `### Loop bound` sub-heading'); assert.match( p5, /\*\*Maximum 3 turns per under-illuminated dimension\.\*\*/, 'the bound must be stated verbatim', ); // Three exits — converged / cap exhausted / operator stop. assert.match(p5, /converged/i, 'exit 1 (converged) must be documented'); assert.match(p5, /exhaust/i, 'exit 2 (cap exhausted) must be documented'); assert.match(p5, /operator stop/i, 'exit 3 (operator stop) must be documented'); // Exhaustion must reach the operator — a silent cap is indistinguishable // from convergence, which is the failure this loop exists to avoid. assert.match( p5, /visibl|visible|print/i, 'cap exhaustion must be written visibly to the operator', ); }); test('trekresearch — Phase 5 marks empty turns without re-targeting the same dimension', () => { const p5 = phase5(read()); assert.match(p5, /`empty`/, 'a finding-less or citation-less turn must be marked `empty`'); assert.match(p5, /empty_turns/, 'empty turns must be counted (empty_turns)'); assert.match( p5, /does NOT re-target|not re-target/i, 'an empty turn must not re-target the same dimension', ); }); test('trekresearch — Phase 5 states the no-brief default and the moot precedence matrix', () => { const p5 = phase5(read()); // (a) no-brief default assert.match(p5, /effort = 'standard'/, 'no-brief default effort must be stated'); assert.match( p5, /--project/, 'the no-brief default must be anchored to the absence of --project/brief.md', ); // (b) precedence matrix — each entry independently makes the loop moot, // mirroring the --engine moot gate in Phase 4. for (const token of ['--quick', '--local', 'external_research_enabled']) { assert.ok(p5.includes(token), `moot matrix must name ${token}`); } assert.match(p5, /moot/i, 'the matrix must use the same moot vocabulary as the engine gate'); // (c) interaction rule — effort: high without model under a cheap profile. assert.match( p5, /effort: high/, 'the interaction rule for a brief carrying effort: high without model must be stated', ); }); test('trekresearch — Phase 5 restates the honesty rule for loop output', () => { const p5 = phase5(read()); // Whitespace-tolerant: the pin is on the sentence, not on where the // paragraph happens to wrap. assert.match( p5, /more\s+turns\s+do\s+not\s+make\s+a\s+finding\s+more\s+credible/i, 'the honesty hard rule must be restated for the loop output', ); }); test('trekresearch — Phase 5 pins survive only while the prose does (mutation control)', () => { const text = read(); const mutated = text.replace(/research-loop-cap\.mjs/g, 'removed-cap.mjs'); assert.doesNotMatch( phase5(mutated), /research-loop-cap\.mjs/, 'heading-bounded Phase 5 slice must still fail on a genuine removal', ); }); test('trekresearch — High-effort behavior keeps the standard/low effort sentences verbatim', () => { const text = read(); assert.ok( text.includes('Standard effort (or absent): use the existing conditional triggers.'), 'the standard-effort sentence must survive the Phase 5 rewrite verbatim', ); assert.ok( text.includes('Low effort: inline research only, no agent swarm'), 'the low-effort sentence must survive the Phase 5 rewrite verbatim', ); }); // --- Step 8: Phase 4.5 dimension discovery + Independence amendment --- const ORCHESTRATOR_FILE = join(ROOT, 'agents', 'research-orchestrator.md'); function readOrchestrator() { return readFileSync(ORCHESTRATOR_FILE, 'utf8'); } test('trekresearch — Phase 4.5 exists between Phase 4 and Phase 5 with the effort skip-guard', () => { const text = read(); const p45 = text.indexOf('## Phase 4.5 —'); assert.ok(p45 >= 0, 'Phase 4.5 heading missing'); const p4 = text.indexOf('## Phase 4 —'); const p5 = text.indexOf('## Phase 5 —'); assert.ok(p4 >= 0 && p5 > p45 && p45 > p4, 'Phase 4.5 must sit between Phase 4 and Phase 5'); const slice = text.slice(p45, p5); assert.match( slice, /\*\*Skip this phase entirely unless `phase_signal_result\.effort == 'high'`\.\*\*/, 'Phase 4.5 must carry the bolded skip-guard in the Phase 3.5 form', ); assert.match(slice, /query-privacy-gate\.mjs/, 'Phase 4.5 must name the privacy gate as its compensating control'); assert.match(slice, /maxDimensions: 8|maxDimensions` *: *8/, 'Phase 4.5 must augment under the existing maxDimensions ceiling, not raise it'); }); test('trekresearch — Independence hard rule carries an explicit Phase 4.5 amendment', () => { const text = read(); const rulesIdx = text.indexOf('## Hard rules'); assert.ok(rulesIdx >= 0, 'Hard rules section missing'); const rules = text.slice(rulesIdx); const indIdx = rules.indexOf('**Independence:**'); assert.ok(indIdx >= 0, 'Independence hard rule missing'); // Bound the rule at the next bullet so the amendment must live inside it. const nextBullet = rules.indexOf('\n- **', indIdx); const independence = nextBullet > indIdx ? rules.slice(indIdx, nextBullet) : rules.slice(indIdx); assert.match(independence, /Amend(ed|ment)/i, 'Independence must be explicitly amended, not silently contradicted'); assert.match(independence, /Phase 4\.5/, 'the amendment must name Phase 4.5 as the crossing'); assert.match(independence, /query-privacy-gate\.mjs/, 'the amendment must name the compensating control'); }); test('trekresearch — orchestrator phase map is correct, has no Phase 9, and carries Phase 4.5', () => { const doc = readOrchestrator(); const start = doc.indexOf('', start); assert.ok(end > start, 'phase mapping comment not terminated'); const map = doc.slice(start, end); assert.doesNotMatch(map, /Command Phase 9/, 'the command ends at Phase 8 — a Command Phase 9 row is a fiction'); // Six orchestrator rows, each pointing at the phase the command actually has. const expected = [ [1, '4'], [2, '4'], [3, '5'], [4, '6'], [5, '7'], [6, '8'], ]; for (const [orch, cmd] of expected) { const re = new RegExp(`Orchestrator Phase ${orch}\\s+= Command Phase ${cmd.replace('.', '\\.')}\\b`); assert.match(map, re, `map row for Orchestrator Phase ${orch} must point at Command Phase ${cmd}`); } assert.match(map, /Command Phase 4\.5/, 'the map must carry the new Phase 4.5 row'); }); // --- v5.1.1 runtime SC4 + SC7 --- test('trekresearch — 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('trekresearch — 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('trekresearch — 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('trekresearch — 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)}`, ); });