/trekresearch loses Phase 4.5, the bounded Phase 5 loop (Phase 5 keeps its single pass of at most 2 follow-ups), the --engine flag, the deep-research adapter and its output self-check. hooks.json loses the pre-agent-cap PreToolUse entry (README: 9 hooks = 9 scripts). The STORM env vars leave the docs; query-privacy-gate.mjs stays, called by no command, and says so. Files removed: 13 (4 docs, 1 hook, 1 lib module, 1 script, 5 tests, 1 fixture). Files edited: 17. Tests: 2600 (2598 pass / 0 fail / 2 skipped) before, 2467 (2466 pass / 0 fail / 1 skipped, R3 pending) after. Durable check: group R2 green and no longer pending. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
150 lines
6.7 KiB
JavaScript
150 lines
6.7 KiB
JavaScript
// 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');
|
|
});
|
|
|
|
// --- Phase 5 slice (heading-bounded) ---
|
|
|
|
// 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 — High-effort behavior gives standard effort the wave-2 sentence and keeps the low effort sentence verbatim', () => {
|
|
const text = read();
|
|
const flatText = text.replace(/\s+/g, ' ');
|
|
assert.ok(
|
|
!flatText.includes('use the existing conditional triggers'),
|
|
'standard effort must not point at a conditional trigger that nothing defines',
|
|
);
|
|
assert.ok(
|
|
flatText.includes('Standard effort (or absent): the selected agents run as in Phase 4, and `contrarian-researcher`, when selected, runs as wave 2 after the other agents return (see Launch rules).'),
|
|
'the standard-effort sentence must say the contrarian runs as wave 2 when selected',
|
|
);
|
|
assert.ok(
|
|
text.includes('Low effort: inline research only, no agent swarm'),
|
|
'the low-effort sentence must survive the Phase 5 rewrite verbatim',
|
|
);
|
|
});
|
|
|
|
// --- 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)}`,
|
|
);
|
|
});
|
|
|
|
// --- Spawn model, turn budget and stats writer ---
|
|
|
|
/** Whitespace (line breaks too) collapsed to one space. */
|
|
const flat = (s) => s.replace(/\s+/g, ' ').trim();
|
|
|
|
/** From the line holding `marker` to the first blank line, flattened. */
|
|
function paragraphAt(text, marker) {
|
|
const lines = text.split('\n');
|
|
const start = lines.findIndex((l) => l.includes(marker));
|
|
assert.ok(start !== -1, `commands/trekresearch.md must hold "${marker}"`);
|
|
let end = lines.length;
|
|
for (let i = start + 1; i < lines.length; i++) if (lines[i].trim() === '') { end = i; break; }
|
|
return flat(lines.slice(start, end).join(' '));
|
|
}
|
|
|
|
test('trekresearch — Phase 5 follow-ups take phase_signal_result.model, else opus', () => {
|
|
const text = read();
|
|
assert.doesNotMatch(flat(phase5(text)), /model: "opus"/);
|
|
const part = paragraphAt(phase5(text), 'launch a targeted follow-up agent');
|
|
assert.match(part, /Maximum 2 follow-ups\./);
|
|
assert.match(part, /launch a targeted follow-up agent \(model: phase_signal_result\.model, else opus — a follow-up may be one of Claude Code's built-in agents, which have no Voyage frontmatter to fall back to; see Launch rules\)/);
|
|
assert.match(flat(text), /captured as `phase_signal_result` and used at Agent-spawn sites in Phase 4 and Phase 5 to inject the resolved model/);
|
|
});
|
|
|
|
test('trekresearch — promises no maxTurns budget', () => {
|
|
const text = read();
|
|
assert.doesNotMatch(text, /maxTurns/);
|
|
assert.match(text, /- convention-scanner: medium\+ codebases only \(50\+ files\)/);
|
|
});
|