diff --git a/plugins/voyage/tests/commands/trekbrief.test.mjs b/plugins/voyage/tests/commands/trekbrief.test.mjs index 3db8030..0788f67 100644 --- a/plugins/voyage/tests/commands/trekbrief.test.mjs +++ b/plugins/voyage/tests/commands/trekbrief.test.mjs @@ -1,23 +1,43 @@ // tests/commands/trekbrief.test.mjs -// v5.1 — Pattern D prose-pattern regression tests for /trekbrief Phase 3.5. +// v5.1 prose-pin tests + v5.1.1 runtime SC1 tests. // -// Brief SC1 + SC2: end-of-brief effort dialog covering 4 downstream phases, -// with `phase_signals_partial` as the force-stop record. +// 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 { 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 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, @@ -40,3 +60,71 @@ test('trekbrief — Phase 3.5 documents phase_signals_partial force-stop', () => 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(['sonnet', 'opus'].includes(r.model), + `phase=${phase}: model "${r.model}" not in [sonnet, opus]`); + } + } +}); + +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)}`, + ); +}); + +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')); +});