import { test } from 'node:test'; import { strict as assert } from 'node:assert'; import { execFileSync } from 'node:child_process'; import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { validateBriefContent } from '../../lib/validators/brief-validator.mjs'; const GOOD_BRIEF = `--- type: trekbrief brief_version: "2.0" created: 2026-04-30 task: "Add JWT auth to API" slug: jwt-auth project_dir: .claude/projects/2026-04-30-jwt-auth/ research_topics: 2 research_status: pending auto_research: false interview_turns: 5 source: interview --- # Task: JWT auth ## Intent Why this matters. ## Goal What success looks like. ## Success Criteria - All tests pass. `; test('validateBrief — happy path', () => { const r = validateBriefContent(GOOD_BRIEF, { strict: true }); assert.equal(r.valid, true, JSON.stringify(r.errors)); }); test('validateBrief — wrong type rejected', () => { const t = GOOD_BRIEF.replace('type: trekbrief', 'type: notabrief'); const r = validateBriefContent(t); assert.equal(r.valid, false); assert.ok(r.errors.find(e => e.code === 'BRIEF_WRONG_TYPE')); }); test('validateBrief — missing required field', () => { const t = GOOD_BRIEF.replace(/^research_topics: 2\n/m, ''); const r = validateBriefContent(t); assert.equal(r.valid, false); assert.ok(r.errors.find(e => e.code === 'BRIEF_MISSING_FIELD' && /research_topics/.test(e.message))); }); test('validateBrief — bad research_status value', () => { const t = GOOD_BRIEF.replace('research_status: pending', 'research_status: maybe'); const r = validateBriefContent(t); assert.equal(r.valid, false); assert.ok(r.errors.find(e => e.code === 'BRIEF_BAD_STATUS')); }); test('validateBrief — state machine: research_topics > 0 + skipped without partial = error', () => { const t = GOOD_BRIEF.replace('research_status: pending', 'research_status: skipped'); const r = validateBriefContent(t); assert.equal(r.valid, false); assert.ok(r.errors.find(e => e.code === 'BRIEF_STATE_INCOHERENT')); }); test('validateBrief — state machine: skipped + brief_quality: partial = warning only', () => { const t = GOOD_BRIEF .replace('research_status: pending', 'research_status: skipped') .replace('source: interview', 'source: interview\nbrief_quality: partial'); const r = validateBriefContent(t); assert.equal(r.valid, true, JSON.stringify(r.errors)); assert.ok(r.warnings.find(w => w.code === 'BRIEF_PARTIAL_SKIPPED')); }); test('validateBrief — strict requires body sections', () => { const t = GOOD_BRIEF.replace(/## Intent\n\nWhy this matters\.\n\n/, ''); const r = validateBriefContent(t, { strict: true }); assert.equal(r.valid, false); assert.ok(r.errors.find(e => e.code === 'BRIEF_MISSING_SECTION')); }); test('validateBrief — soft demotes section errors to warnings', () => { const t = GOOD_BRIEF.replace(/## Goal\n\nWhat success looks like\.\n\n/, ''); const r = validateBriefContent(t, { strict: false }); assert.equal(r.valid, true); assert.ok(r.warnings.find(w => w.code === 'BRIEF_MISSING_SECTION')); }); test('validateBrief — missing frontmatter is hard error', () => { const r = validateBriefContent('# just markdown\n\nno frontmatter\n'); assert.equal(r.valid, false); assert.ok(r.errors.find(e => e.code === 'FM_MISSING')); }); const REVIEW_AS_BRIEF = `--- type: trekreview task: "Review delivered trekreview v1.0" slug: trekreview project_dir: .claude/projects/2026-05-01-trekreview/ findings: - 0123456789abcdef0123456789abcdef01234567 - fedcba9876543210fedcba9876543210fedcba98 --- # Review brief ## Intent Adversarial review of delivered trekreview v1.0. ## Goal Find what was missed. ## Success Criteria - All BLOCKER findings get a fix-plan. `; test('validateBrief — trekreview type accepted with findings array', () => { const r = validateBriefContent(REVIEW_AS_BRIEF, { strict: true }); assert.equal(r.valid, true, JSON.stringify(r.errors)); }); test('validateBrief — trekreview without findings rejected (BRIEF_MISSING_FIELD)', () => { const t = REVIEW_AS_BRIEF.replace(/findings:\n - 0123[\s\S]*?- fedcba[0-9a-f]+\n/, ''); const r = validateBriefContent(t); assert.equal(r.valid, false); assert.ok( r.errors.find(e => e.code === 'BRIEF_MISSING_FIELD' && /findings/.test(e.message)), `expected BRIEF_MISSING_FIELD for findings; got ${JSON.stringify(r.errors)}`, ); }); test('validateBrief — trekreview with findings as scalar (not array) rejected (BRIEF_BAD_FINDINGS_TYPE)', () => { const t = REVIEW_AS_BRIEF.replace( /findings:\n - 0123[\s\S]*?- fedcba[0-9a-f]+/, 'findings: not-an-array', ); const r = validateBriefContent(t); assert.equal(r.valid, false); assert.ok(r.errors.find(e => e.code === 'BRIEF_BAD_FINDINGS_TYPE')); }); test('validateBrief — wrong-type error message includes accepted set', () => { const t = REVIEW_AS_BRIEF.replace('type: trekreview', 'type: somethingelse'); const r = validateBriefContent(t); assert.equal(r.valid, false); const wrongType = r.errors.find(e => e.code === 'BRIEF_WRONG_TYPE'); assert.ok(wrongType); assert.ok(/trekbrief/.test(wrongType.message)); assert.ok(/trekreview/.test(wrongType.message)); }); // --- v5.1 — phase_signals additive field + sequencing gate --- const SIGNALS_BLOCK = `phase_signals: - phase: research effort: standard - phase: plan effort: high model: opus - phase: execute effort: low model: sonnet - phase: review effort: standard `; test('validateBrief — v5.1 well-formed phase_signals accepted', () => { const t = GOOD_BRIEF .replace('brief_version: "2.0"', 'brief_version: "2.1"') .replace('source: interview\n', `source: interview\n${SIGNALS_BLOCK}`); const r = validateBriefContent(t, { strict: true }); assert.equal(r.valid, true, JSON.stringify(r.errors)); }); test('validateBrief — pre-v5.1 brief without phase_signals accepted (backward-compat)', () => { const r = validateBriefContent(GOOD_BRIEF, { strict: true }); assert.equal(r.valid, true, JSON.stringify(r.errors)); assert.ok(!r.errors.find(e => e.code === 'BRIEF_V51_MISSING_SIGNALS')); }); test('validateBrief — v5.1+ brief missing phase_signals + partial emits BRIEF_V51_MISSING_SIGNALS', () => { const t = GOOD_BRIEF.replace('brief_version: "2.0"', 'brief_version: "2.1"'); const r = validateBriefContent(t, { strict: true }); assert.equal(r.valid, false); assert.ok(r.errors.find(e => e.code === 'BRIEF_V51_MISSING_SIGNALS')); }); test('validateBrief — v5.1+ brief with phase_signals_partial: true accepted', () => { const t = GOOD_BRIEF .replace('brief_version: "2.0"', 'brief_version: "2.1"') .replace('source: interview\n', 'source: interview\nphase_signals_partial: true\n'); const r = validateBriefContent(t, { strict: true }); assert.equal(r.valid, true, JSON.stringify(r.errors)); }); test('validateBrief — phase_signals + phase_signals_partial both set rejected (mutually exclusive)', () => { const t = GOOD_BRIEF .replace('brief_version: "2.0"', 'brief_version: "2.1"') .replace('source: interview\n', `source: interview\nphase_signals_partial: true\n${SIGNALS_BLOCK}`); const r = validateBriefContent(t, { strict: true }); assert.equal(r.valid, false); assert.ok(r.errors.find(e => e.code === 'BRIEF_SIGNALS_MUTUALLY_EXCLUSIVE')); }); test('validateBrief — phase_signals with unknown phase rejected', () => { const BAD_SIGNALS = `phase_signals: - phase: nonsense effort: standard `; const t = GOOD_BRIEF .replace('brief_version: "2.0"', 'brief_version: "2.1"') .replace('source: interview\n', `source: interview\n${BAD_SIGNALS}`); const r = validateBriefContent(t, { strict: true }); assert.equal(r.valid, false); assert.ok(r.errors.find(e => e.code === 'BRIEF_INVALID_PHASE_SIGNAL_PHASE')); }); // --- v5.1.1 regression: YAML-number bypass closed --- // Findings 3c834097 + df1435a2: v5.1.0 shipped with an unquoted `brief_version: 2.1` // template. parseScalar coerces unquoted "2.1" to Number 2.1, and the original gate // guarded `typeof === 'string'`, silently bypassing the sequencing check. v5.1.1 // coerces via String() so both shapes trigger the gate. test('validateBrief — v5.1.1: UNQUOTED brief_version 2.1 without signals triggers gate', () => { const t = GOOD_BRIEF.replace('brief_version: "2.0"', 'brief_version: 2.1'); const r = validateBriefContent(t, { strict: true }); assert.equal(r.valid, false); assert.ok( r.errors.find(e => e.code === 'BRIEF_V51_MISSING_SIGNALS'), `gate must fire for unquoted brief_version: 2.1 (YAML Number); errors=${JSON.stringify(r.errors)}`, ); }); test('validateBrief — v5.1.1: QUOTED brief_version "2.1" without signals triggers gate (regression guard)', () => { const t = GOOD_BRIEF.replace('brief_version: "2.0"', 'brief_version: "2.1"'); const r = validateBriefContent(t, { strict: true }); assert.equal(r.valid, false); assert.ok(r.errors.find(e => e.code === 'BRIEF_V51_MISSING_SIGNALS')); }); test('validateBrief — v5.1.1: UNQUOTED brief_version 2.1 WITH phase_signals is valid (positive case)', () => { const t = GOOD_BRIEF .replace('brief_version: "2.0"', 'brief_version: 2.1') .replace('source: interview\n', `source: interview\n${SIGNALS_BLOCK}`); const r = validateBriefContent(t, { strict: true }); assert.equal(r.valid, true, JSON.stringify(r.errors)); assert.ok(!r.errors.find(e => e.code === 'BRIEF_V51_MISSING_SIGNALS')); }); // --- v5.9 — fable model tier (BASE_ALLOWED_MODELS widened to three values) --- test('validateBrief — v5.9: fable phase_signals fixture accepted (no BRIEF_INVALID_MODEL)', () => { const t = readFileSync(new URL('../fixtures/brief-effort-fable.md', import.meta.url), 'utf-8'); const r = validateBriefContent(t, { strict: true }); assert.equal(r.valid, true, JSON.stringify(r.errors)); assert.ok(!r.errors.find(e => e.code === 'BRIEF_INVALID_MODEL')); }); test('validateBrief — v5.9: unknown model gpt5 in phase_signals rejected with BRIEF_INVALID_MODEL', () => { const t = GOOD_BRIEF .replace('brief_version: "2.0"', 'brief_version: "2.1"') .replace('source: interview\n', `source: interview\n${SIGNALS_BLOCK.replace('model: opus', 'model: gpt5')}`); const r = validateBriefContent(t, { strict: true }); assert.equal(r.valid, false); assert.ok(r.errors.find(e => e.code === 'BRIEF_INVALID_MODEL'), `expected BRIEF_INVALID_MODEL for gpt5, got: ${JSON.stringify(r.errors)}`); }); // --- v5.5 — framing enforcement + obligatory TL;DR (gated at brief_version ≥ 2.2) --- // Operator decision (S6, option A1): framing + TL;DR are hard BLOCKERs for briefs // declaring brief_version ≥ 2.2; existing 2.0/2.1 briefs stay valid (forward-compat, // mirroring the phase_signals ≥ 2.1 precedent). The framing ENUM check fires on any // version when the field is present but malformed. const GOOD_BRIEF_22 = `--- type: trekbrief brief_version: "2.2" created: 2026-06-18 task: "Add JWT auth to API" slug: jwt-auth project_dir: .claude/projects/2026-06-18-jwt-auth/ research_topics: 0 research_status: complete auto_research: false interview_turns: 5 source: interview framing: new-direction phase_signals_partial: true --- # Task: JWT auth ## TL;DR Net-new JWT auth; no prior brief to anchor against. ## Intent Why this matters. ## Goal What success looks like. ## Success Criteria - All tests pass. `; test('validateBrief — v5.5 well-formed 2.2 brief (framing + TL;DR) accepted', () => { const r = validateBriefContent(GOOD_BRIEF_22, { strict: true }); assert.equal(r.valid, true, JSON.stringify(r.errors)); }); test('validateBrief — v5.5 framing enum: invalid value rejected on any version', () => { const t = GOOD_BRIEF.replace('source: interview\n', 'source: interview\nframing: sideways\n'); const r = validateBriefContent(t, { strict: true }); assert.equal(r.valid, false); assert.ok(r.errors.find(e => e.code === 'BRIEF_INVALID_FRAMING')); }); test('validateBrief — v5.5 framing enum: all four canonical values accepted', () => { for (const v of ['preserve', 'refine', 'replace', 'new-direction']) { const t = GOOD_BRIEF_22.replace('framing: new-direction', `framing: ${v}`); const r = validateBriefContent(t, { strict: true }); assert.equal(r.valid, true, `framing=${v}: ${JSON.stringify(r.errors)}`); } }); test('validateBrief — v5.5 brief_version 2.2 missing framing rejected (BRIEF_MISSING_FRAMING)', () => { const t = GOOD_BRIEF_22.replace('framing: new-direction\n', ''); const r = validateBriefContent(t, { strict: true }); assert.equal(r.valid, false); assert.ok(r.errors.find(e => e.code === 'BRIEF_MISSING_FRAMING')); }); test('validateBrief — v5.5 brief_version 2.2 missing ## TL;DR rejected (strict)', () => { const t = GOOD_BRIEF_22.replace(/## TL;DR\n\n[^\n]*\n\n/, ''); const r = validateBriefContent(t, { strict: true }); assert.equal(r.valid, false); assert.ok(r.errors.find(e => e.code === 'BRIEF_MISSING_SECTION' && /TL;DR/.test(e.message))); }); test('validateBrief — v5.5 brief_version 2.2 missing ## TL;DR demoted to warning (soft)', () => { const t = GOOD_BRIEF_22.replace(/## TL;DR\n\n[^\n]*\n\n/, ''); const r = validateBriefContent(t, { strict: false }); assert.ok(r.warnings.find(w => w.code === 'BRIEF_MISSING_SECTION' && /TL;DR/.test(w.message))); }); test('validateBrief — v5.5 TL;DR exceeding 5 lines emits BRIEF_TLDR_TOO_LONG warning', () => { const longTldr = ['l1', 'l2', 'l3', 'l4', 'l5', 'l6'].join('\n'); const t = GOOD_BRIEF_22.replace('Net-new JWT auth; no prior brief to anchor against.', longTldr); const r = validateBriefContent(t, { strict: true }); assert.ok( r.warnings.find(w => w.code === 'BRIEF_TLDR_TOO_LONG'), `expected TL;DR-too-long warning; warnings=${JSON.stringify(r.warnings)}`, ); }); test('validateBrief — v5.5 backward-compat: 2.1 brief without framing/TL;DR stays valid', () => { const t = GOOD_BRIEF .replace('brief_version: "2.0"', 'brief_version: "2.1"') .replace('source: interview\n', 'source: interview\nphase_signals_partial: true\n'); const r = validateBriefContent(t, { strict: true }); assert.equal(r.valid, true, JSON.stringify(r.errors)); assert.ok(!r.errors.find(e => e.code === 'BRIEF_MISSING_FRAMING')); assert.ok(!r.errors.find(e => e.code === 'BRIEF_MISSING_SECTION' && /TL;DR/.test(e.message))); }); test('validateBrief — v5.5 trekreview brief not subject to framing/TL;DR gate', () => { const r = validateBriefContent(REVIEW_AS_BRIEF, { strict: true }); assert.equal(r.valid, true, JSON.stringify(r.errors)); assert.ok(!r.errors.find(e => e.code === 'BRIEF_MISSING_FRAMING')); assert.ok(!r.errors.find(e => e.code === 'BRIEF_MISSING_SECTION' && /TL;DR/.test(e.message))); }); // S18 — opt-in --min-brief-version gate. Warns (never blocks) when a brief // declares a version below the caller's floor, sidestepping framing enforcement. const BRIEF_21 = GOOD_BRIEF .replace('brief_version: "2.0"', 'brief_version: "2.1"') .replace('source: interview\n', 'source: interview\nphase_signals_partial: true\n'); test('validateBrief — S18 min-version: older brief below floor warns but does not block', () => { const r = validateBriefContent(BRIEF_21, { strict: true, minBriefVersion: '2.2' }); assert.equal(r.valid, true, `must warn, not block: ${JSON.stringify(r.errors)}`); assert.ok( r.warnings.find(w => w.code === 'BRIEF_VERSION_BELOW_MINIMUM'), `expected BRIEF_VERSION_BELOW_MINIMUM; warnings=${JSON.stringify(r.warnings)}`, ); }); test('validateBrief — S18 min-version: absent opt means no version-floor check', () => { const r = validateBriefContent(BRIEF_21, { strict: true }); assert.ok(!r.warnings.find(w => w.code === 'BRIEF_VERSION_BELOW_MINIMUM')); }); test('validateBrief — S18 min-version: brief meeting the floor does not warn', () => { const r = validateBriefContent(GOOD_BRIEF_22, { strict: true, minBriefVersion: '2.2' }); assert.ok(!r.warnings.find(w => w.code === 'BRIEF_VERSION_BELOW_MINIMUM')); }); test('validateBrief — S18 min-version: 2.0 brief below 2.2 floor warns', () => { const r = validateBriefContent(GOOD_BRIEF, { minBriefVersion: '2.2' }); assert.ok(r.warnings.find(w => w.code === 'BRIEF_VERSION_BELOW_MINIMUM')); }); test('validateBrief — S18 min-version: trekreview brief is exempt (no framing to bypass)', () => { const r = validateBriefContent(REVIEW_AS_BRIEF, { minBriefVersion: '2.2' }); assert.ok(!r.warnings.find(w => w.code === 'BRIEF_VERSION_BELOW_MINIMUM')); }); // S56 — CLI arg-parsing regression. The no-flag invocation `brief-validator.mjs ` // used to bail to Usage/exit 2 because the --min-version skip index was 0 when the flag // was absent, dropping the file positional (which sits at argv index 0). test('CLI — no-flag invocation reaches validation, does not bail to Usage (S56 regression)', () => { const dir = mkdtempSync(join(tmpdir(), 'brief-cli-')); try { const file = join(dir, 'brief.md'); writeFileSync(file, GOOD_BRIEF); // execFileSync throws on non-zero exit; pre-fix this bailed to Usage (exit 2). const out = execFileSync(process.execPath, [ 'lib/validators/brief-validator.mjs', file, ], { encoding: 'utf-8' }); assert.match(out, /PASS/); } finally { rmSync(dir, { recursive: true, force: true }); } }); test('CLI — --min-version still locates the file positional after the value token (S56)', () => { const dir = mkdtempSync(join(tmpdir(), 'brief-cli-')); try { const file = join(dir, 'brief.md'); writeFileSync(file, GOOD_BRIEF); const out = execFileSync(process.execPath, [ 'lib/validators/brief-validator.mjs', '--min-version', '2.0', file, ], { encoding: 'utf-8' }); assert.match(out, /PASS/); } finally { rmSync(dir, { recursive: true, force: true }); } });