voyage/tests/commands/trekbrief.test.mjs

208 lines
8.5 KiB
JavaScript

// tests/commands/trekbrief.test.mjs
// v5.1 prose-pin tests + v5.1.1 runtime SC1 tests.
//
// Pattern D prose-pins kept as doc-anchors for the .md file. Runtime tests
// added per finding 350853 (BLOCKER SC1) + a7f4f95a (MAJOR Plan Step 5 drift).
//
// SC1 re-interpretation (per plan Step 10 amendment): "asserts on 4
// AskUserQuestion calls" → "asserts resolvePhaseSignal returns non-null for
// all 4 entries in PHASE_SIGNAL_PHASES when applied to a brief with a
// committed phase_signals block." See brief amendment for full rationale.
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, PHASE_SIGNAL_PHASES, EFFORT_LEVELS } from '../../lib/validators/brief-validator.mjs';
import { BASE_ALLOWED_MODELS } from '../../lib/validators/profile-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', 'trekbrief.md');
const REVIEWER_FILE = join(ROOT, 'agents', 'brief-reviewer.md');
const TEMPLATE_FILE = join(ROOT, 'templates', 'trekbrief-template.md');
const FIXTURE = (name) => join(ROOT, 'tests', 'fixtures', name);
function read() {
return readFileSync(COMMAND_FILE, 'utf8');
}
function readFixture(name) {
return readFileSync(FIXTURE(name), 'utf8');
}
function frontmatterOf(text) {
const doc = parseDocument(text);
return doc.parsed && doc.parsed.frontmatter;
}
// --- Pattern D prose-pins (doc-anchors) ---
test('trekbrief — Phase 3.5 heading is present', () => {
const text = read();
assert.match(text, /^## Phase 3\.5 — Per-phase effort dialog$/m,
'Phase 3.5 heading missing from commands/trekbrief.md');
});
test('trekbrief — Phase 3.5 references all 4 downstream phases', () => {
const text = read();
const startIdx = text.indexOf('## Phase 3.5');
assert.ok(startIdx >= 0, 'Phase 3.5 not found');
const section = text.slice(startIdx, text.indexOf('## Phase 4', startIdx));
for (const phase of ['research', 'plan', 'execute', 'review']) {
assert.ok(section.includes(phase),
`Phase 3.5 missing reference to "${phase}"`);
}
});
test('trekbrief — Phase 3.5 documents phase_signals_partial force-stop', () => {
const text = read();
assert.ok(text.includes('phase_signals_partial'),
'phase_signals_partial not mentioned in /trekbrief command prose');
});
// --- v5.1.1 runtime SC1 tests ---
test('trekbrief — SC1: resolvePhaseSignal returns non-null for all 4 phases on committed brief (brief-effort-low)', () => {
const fm = frontmatterOf(readFixture('brief-effort-low.md'));
for (const phase of PHASE_SIGNAL_PHASES) {
const r = resolvePhaseSignal(fm, phase);
assert.ok(r && typeof r === 'object',
`phase=${phase}: resolver must return non-null for committed brief; got ${JSON.stringify(r)}`);
assert.ok(typeof r.effort === 'string',
`phase=${phase}: resolver result must include effort`);
}
});
test('trekbrief — SC1: each of 4 phases has both effort AND model on full-signals fixture', () => {
const fm = frontmatterOf(readFixture('brief-with-phase-signals.md'));
for (const phase of PHASE_SIGNAL_PHASES) {
const r = resolvePhaseSignal(fm, phase);
assert.ok(r && typeof r === 'object', `phase=${phase}: must resolve`);
assert.ok(EFFORT_LEVELS.includes(r.effort),
`phase=${phase}: effort "${r.effort}" not in EFFORT_LEVELS`);
if ('model' in r) {
assert.ok(BASE_ALLOWED_MODELS.includes(r.model),
`phase=${phase}: model "${r.model}" not in [${BASE_ALLOWED_MODELS.join(', ')}]`);
}
}
});
test('trekbrief — SC1: missing phase_signals + brief_version 2.1 triggers 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'),
`gate must fire; errors=${JSON.stringify(r.errors)}`,
);
});
// --- v5.9 — fable tier option in the Phase 3.5 loop ---
test('trekbrief — v5.9 Phase 3.5 canonical mapping contains the fable row and offers 4 options', () => {
const text = read();
const startIdx = text.indexOf('## Phase 3.5');
assert.ok(startIdx >= 0, 'Phase 3.5 not found');
const section = text.slice(startIdx, text.indexOf('## Phase 4', startIdx));
assert.ok(section.includes('fable → {effort: high, model: fable}'),
'Phase 3.5 canonical mapping must contain the fable tier row');
assert.ok(section.includes('with 4 options'),
'Phase 3.5 loop must offer 4 options (AskUserQuestion maxItems: 4)');
});
// --- v5.5 — framing enforcement + TL;DR + memory-alignment prose-pins ---
test('trekbrief — v5.5 Phase 2.5 framing declaration heading present', () => {
const text = read();
assert.match(text, /^## Phase 2\.5 — Framing declaration/m,
'Phase 2.5 framing-declaration heading missing from commands/trekbrief.md');
});
test('trekbrief — v5.5 Phase 2.5 references all four framing values', () => {
const text = read();
const start = text.indexOf('## Phase 2.5');
const section = text.slice(start, text.indexOf('## Phase 3', start));
for (const v of ['preserve', 'refine', 'replace', 'new-direction']) {
assert.ok(section.includes(v), `Phase 2.5 missing framing value "${v}"`);
}
});
test('trekbrief — v5.5 Phase 2.5 runs before any brief prose (precedes Phase 3)', () => {
const text = read();
assert.ok(text.indexOf('## Phase 2.5') < text.indexOf('## Phase 3'),
'Phase 2.5 must come before the completeness loop (before prose)');
assert.ok(text.includes('even in `--quick` mode'),
'framing must be non-skippable even in --quick mode');
});
test('trekbrief — v5.5 Step 4a writes framing + brief_version 2.2 + generates TL;DR', () => {
const text = read();
assert.ok(/brief_version: "2\.2"/.test(text), 'Step 4a must set brief_version 2.2');
assert.ok(/framing: <state\.framing>/.test(text), 'Step 4a must write the committed framing value');
assert.ok(/## TL;DR/.test(text), 'Step 4a must generate the TL;DR section');
});
test('trekbrief — v5.5 Phase 4e gate includes memory_alignment', () => {
const text = read();
assert.ok(/memory_alignment\.score ≥ 4/.test(text),
'Phase 4e gate must require memory_alignment.score ≥ 4');
});
test('trekbrief — v5.5 brief-reviewer declares the memory-alignment dimension', () => {
const reviewer = readFileSync(REVIEWER_FILE, 'utf8');
assert.match(reviewer, /### 6\. Memory alignment/,
'brief-reviewer.md missing dimension 6 (memory alignment)');
assert.ok(reviewer.includes('"memory_alignment"'),
'brief-reviewer.md JSON schema missing memory_alignment key');
assert.ok(/no memory context (is )?supplied/i.test(reviewer),
'brief-reviewer must define the no-memory-context N/A fallback');
});
test('trekbrief — S18 brief-reviewer memory_alignment carries a status field', () => {
const reviewer = readFileSync(REVIEWER_FILE, 'utf8');
assert.ok(reviewer.includes('"status": "verified | n_a | contradictions"'),
'memory_alignment must emit a status enum (verified | n_a | contradictions) so a score-5 N/A (no memory) is distinguishable from a score-5 verified-aligned brief');
});
test('trekbrief — v5.5 template carries framing field, 2.2, and TL;DR section', () => {
const tpl = readFileSync(TEMPLATE_FILE, 'utf8');
assert.ok(/brief_version: "2\.2"/.test(tpl), 'template must declare brief_version 2.2');
assert.match(tpl, /^framing: \{preserve \| refine \| replace \| new-direction\}/m,
'template frontmatter must include the framing field');
assert.match(tpl, /^## TL;DR$/m, 'template must include the ## TL;DR section');
});
test('trekbrief — SC1: phase_signals_partial: true does NOT trigger the gate', () => {
const partial = `---
type: trekbrief
brief_version: "2.1"
created: 2026-05-14
task: "Partial brief"
slug: partial-brief
project_dir: .claude/projects/2026-05-14-partial-brief/
research_topics: 0
research_status: complete
auto_research: false
interview_turns: 2
source: fixture
phase_signals_partial: true
---
# Task
## Intent
Stop early.
## Goal
Test partial mode.
## Success Criteria
- gate does not fire.
`;
const r = validateBriefContent(partial, { strict: true });
assert.equal(r.valid, true, `errors=${JSON.stringify(r.errors)}`);
assert.ok(!r.errors.find(e => e.code === 'BRIEF_V51_MISSING_SIGNALS'));
});