The retired --profile is one list the parser drops (with its value, never an error) instead of a valued flag in six schemas, so the removed-surfaces check exempts one parser line, not seven; seen red on the old parser. The empty OPTIONAL_STRING_KEYS machinery, a one-item loop and an unused plugin-data dir go; the removed-surfaces check reads each file once; the Phase 8 slice in executor-prose-pins asserts both anchors. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
410 lines
16 KiB
JavaScript
410 lines
16 KiB
JavaScript
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.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).
|
|
// 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
|
|
---
|
|
|
|
# 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.
|
|
`;
|
|
|
|
// An older brief may still carry a phase_signals block (removed in cleanup R-b,
|
|
// decision 191): the key is ignored, with no error and no warning.
|
|
|
|
test('validateBrief — a 2.2 brief that still carries phase_signals validates with no error or warning about it', () => {
|
|
const t = GOOD_BRIEF_22.replace('framing: new-direction\n',
|
|
'framing: new-direction\nphase_signals:\n - phase: plan\n effort: high\n - phase: brief\n effort: xhigh\n');
|
|
const r = validateBriefContent(t, { strict: true });
|
|
assert.equal(r.valid, true, JSON.stringify(r.errors));
|
|
assert.deepEqual(r.errors, []);
|
|
assert.deepEqual(r.warnings.filter(w => /signal/i.test(w.code + ' ' + w.message)), []);
|
|
});
|
|
|
|
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"');
|
|
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"');
|
|
|
|
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 <brief.md>`
|
|
// 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 });
|
|
}
|
|
});
|
|
|
|
const codes = (list) => list.map((e) => e.code);
|
|
|
|
function briefWith(sections) {
|
|
return [
|
|
'---', 'type: trekbrief', 'brief_version: "2.2"', 'task: t', 'slug: s',
|
|
'research_topics: 0', 'research_status: skipped', 'framing: new-direction', '---', '',
|
|
'## TL;DR', '', 'A toy.', '', '## Intent', '', 'Why.', '', '## Goal', '', 'What.', '',
|
|
...sections,
|
|
].join('\n');
|
|
}
|
|
|
|
// ---- BRIEF_SC_SUBPOINTS: sub-points belong to their criterion -----------------
|
|
|
|
test('BRIEF_SC_SUBPOINTS: a criterion with sub-points and no command of its own is a warning that names it', () => {
|
|
const r = validateBriefContent(briefWith([
|
|
'## Success Criteria', '',
|
|
'- `npm test` exits 0.',
|
|
'- The separator is honoured:',
|
|
' - `-` joins words',
|
|
' - `_` joins words',
|
|
' - `.` joins words',
|
|
'- `bash tests/e2e.sh` exits 0.', '',
|
|
]));
|
|
const w = r.warnings.filter((x) => x.code === 'BRIEF_SC_SUBPOINTS');
|
|
assert.equal(w.length, 1, JSON.stringify(r.warnings));
|
|
assert.match(w[0].message, /SC2/);
|
|
});
|
|
|
|
// ---- BRIEF_SC_BASH_NOT_PINNED: a bash 3.2 constraint is measured under bash 3.2 ----
|
|
|
|
const BASH32 = ['## Constraints', '', '- Scripts must run under bash 3.2 (macOS system bash).', ''];
|
|
|
|
test('BRIEF_SC_BASH_NOT_PINNED: a brief that requires bash 3.2 and runs a criterion with plain `bash` gets a warning pointing at /bin/bash', () => {
|
|
const r = validateBriefContent(briefWith([...BASH32, '## Success Criteria', '', '- `bash tests/contract.sh` exits 0.', '']));
|
|
const w = r.warnings.filter((x) => x.code === 'BRIEF_SC_BASH_NOT_PINNED');
|
|
assert.equal(w.length, 1, JSON.stringify(r.warnings));
|
|
assert.match(w[0].hint, /\/bin\/bash tests\//);
|
|
});
|
|
|
|
test('BRIEF_SC_BASH_NOT_PINNED: `/bin/bash`, or no bash 3.2 constraint, raises no such warning', () => {
|
|
const pinned = validateBriefContent(briefWith([...BASH32, '## Success Criteria', '', '- `/bin/bash tests/contract.sh` exits 0.', '']));
|
|
assert.ok(!codes(pinned.warnings).includes('BRIEF_SC_BASH_NOT_PINNED'));
|
|
const free = validateBriefContent(briefWith(['## Success Criteria', '', '- `bash tests/contract.sh` exits 0.', '']));
|
|
assert.ok(!codes(free.warnings).includes('BRIEF_SC_BASH_NOT_PINNED'));
|
|
});
|
|
|
|
test('BRIEF_SC_BASH_NOT_PINNED: a negated bash 3.2 constraint, an env prefix and `bash -c` are handled', () => {
|
|
const neg = validateBriefContent(briefWith(['## Constraints', '', '- bash 3.2 is not required here.', '',
|
|
'## Success Criteria', '', '- `bash tests/contract.sh` exits 0.', '']));
|
|
assert.ok(!codes(neg.warnings).includes('BRIEF_SC_BASH_NOT_PINNED'), 'negated constraint');
|
|
const env = validateBriefContent(briefWith([...BASH32, '## Success Criteria', '', '- `LC_ALL=C bash tests/contract.sh` exits 0.', '']));
|
|
assert.ok(codes(env.warnings).includes('BRIEF_SC_BASH_NOT_PINNED'), 'env-prefixed bash');
|
|
const dashC = validateBriefContent(briefWith([...BASH32, '## Success Criteria', '', '- `bash -c true` exits 0.', '']));
|
|
for (const w of dashC.warnings.filter((x) => x.code === 'BRIEF_SC_BASH_NOT_PINNED')) {
|
|
assert.doesNotMatch(w.hint, /\/bin\/bash -c/, 'never suggests a form the runner refuses');
|
|
}
|
|
});
|
|
|
|
test('/trekbrief proposes /bin/bash when the brief requires bash 3.2, and names both warnings', () => {
|
|
const md = readFileSync(new URL('../../commands/trekbrief.md', import.meta.url), 'utf8');
|
|
assert.match(md, /requires\s+bash\s+3\.2[^`]*`\/bin\/bash tests\/<script>\.sh`/);
|
|
assert.match(md, /BRIEF_SC_BASH_NOT_PINNED/);
|
|
assert.match(md, /BRIEF_SC_SUBPOINTS/);
|
|
});
|