The four profiles, the resolver, the profile validator and docs/profiles.md go; /trekbrief Phase 3.5 and the brief's phase_signals checks go (an older brief's key is ignored); MANIFEST_PROFILE_DRIFT and profile_used go. Every Profile, Composition rule, Sequencing gate and High-effort section leaves the commands. Decision 209: /trekresearch runs the full swarm with contrarian-researcher as wave 2 every run; /trekreview and the coordinator run all four passes with no high-effort normalization. Voyage agents are spawned with no model (their own model: opus applies); the two built-in spawns pass model "opus". --profile stays in the parser as a retired flag that swallows its value. §Model & effort moves to docs/architecture.md; the sequencing-gate eval case goes. Files removed: 44 (43 from the R3 list + tests/commands/trekreview.test.mjs, whose every test pinned removed text). Files edited: 43. Tests: 2467 (2466 pass / 0 fail / 1 skipped) before, 2309 (2309 pass / 0 fail / 0 skipped) after. Durable check: all three groups green, none pending. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
107 lines
4.4 KiB
JavaScript
107 lines
4.4 KiB
JavaScript
// tests/validators/brief-ceremony.test.mjs
|
|
// K1 — the brief's size class: optional `ceremony: small | bounded | architectural`;
|
|
// absent = architectural (heaviest when in doubt). Plus the optional
|
|
// `artifact_store: git | local` (operator decision 2026-10-06); absent = local,
|
|
// so nothing is published without a yes. Both are additive: no brief_version bump.
|
|
|
|
import { test } from 'node:test';
|
|
import { strict as assert } from 'node:assert';
|
|
import { spawnSync } from 'node:child_process';
|
|
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { dirname, join } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
const ROOT = join(HERE, '..', '..');
|
|
const MODULE = join(ROOT, 'lib', 'validators', 'brief-validator.mjs');
|
|
const load = () => import(MODULE);
|
|
|
|
const brief = (extra = '') => `---
|
|
type: trekbrief
|
|
brief_version: "2.2"
|
|
created: 2026-10-06
|
|
task: "Add a dark-mode toggle"
|
|
slug: dark-mode-toggle
|
|
project_dir: .claude/projects/2026-10-06-dark-mode-toggle/
|
|
research_topics: 0
|
|
research_status: complete
|
|
framing: new-direction
|
|
${extra}---
|
|
|
|
# Task
|
|
|
|
## TL;DR
|
|
|
|
A toggle.
|
|
|
|
## Intent
|
|
|
|
Readers cook at night.
|
|
|
|
## Goal
|
|
|
|
A toggle.
|
|
|
|
## Success Criteria
|
|
|
|
- It persists.
|
|
`;
|
|
|
|
const codes = (r) => r.errors.map((e) => e.code);
|
|
|
|
test('ceremony: huge is rejected (BRIEF_INVALID_CEREMONY)', async () => {
|
|
const { validateBriefContent } = await load();
|
|
const r = validateBriefContent(brief('ceremony: huge\n'));
|
|
assert.equal(r.valid, false);
|
|
assert.deepEqual(codes(r), ['BRIEF_INVALID_CEREMONY']);
|
|
});
|
|
|
|
test('ceremony: small, bounded and architectural are accepted', async () => {
|
|
const { validateBriefContent } = await load();
|
|
for (const c of ['small', 'bounded', 'architectural']) {
|
|
const r = validateBriefContent(brief(`ceremony: ${c}\n`));
|
|
assert.deepEqual(codes(r), [], c);
|
|
}
|
|
});
|
|
|
|
test('ceremony absent is valid and reads as architectural (heaviest in doubt)', async () => {
|
|
const { validateBriefContent, briefCeremony, BRIEF_CEREMONY_DEFAULT } = await load();
|
|
const r = validateBriefContent(brief());
|
|
assert.deepEqual(codes(r), []);
|
|
assert.equal(BRIEF_CEREMONY_DEFAULT, 'architectural');
|
|
assert.equal(briefCeremony(r.parsed.frontmatter), 'architectural');
|
|
assert.equal(briefCeremony({ ceremony: 'small' }), 'small');
|
|
assert.equal(briefCeremony({ ceremony: 'huge' }), 'architectural', 'an invalid value never reads lighter');
|
|
});
|
|
|
|
test('artifact_store: git and local accepted, anything else rejected, absent reads as local', async () => {
|
|
const { validateBriefContent, briefArtifactStore } = await load();
|
|
assert.deepEqual(codes(validateBriefContent(brief('artifact_store: git\n'))), []);
|
|
assert.deepEqual(codes(validateBriefContent(brief('artifact_store: local\n'))), []);
|
|
assert.deepEqual(codes(validateBriefContent(brief('artifact_store: cloud\n'))), ['BRIEF_INVALID_ARTIFACT_STORE']);
|
|
assert.equal(briefArtifactStore({}), 'local');
|
|
assert.equal(briefArtifactStore({ artifact_store: 'git' }), 'git');
|
|
});
|
|
|
|
test('no brief_version bump: the template still declares 2.2 and documents both fields', () => {
|
|
const tpl = spawnSync('cat', [join(ROOT, 'templates', 'trekbrief-template.md')], { encoding: 'utf8' }).stdout;
|
|
assert.match(tpl, /^brief_version: "2\.2"$/m);
|
|
assert.match(tpl, /^ceremony: /m);
|
|
assert.match(tpl, /^artifact_store: /m);
|
|
});
|
|
|
|
test('the validator CLI runs through a symlinked path with a space (realpath main-module guard)', () => {
|
|
const d = mkdtempSync(join(tmpdir(), 'brief-ceremony-'));
|
|
try {
|
|
const link = join(d, 'with space');
|
|
spawnSync('ln', ['-s', join(ROOT, 'lib'), link]);
|
|
writeFileSync(join(d, 'b.md'), brief('ceremony: huge\n'));
|
|
const r = spawnSync(process.execPath, [join(link, 'validators', 'brief-validator.mjs'), '--json', join(d, 'b.md')], { encoding: 'utf8' });
|
|
assert.equal(r.status, 1, r.stdout + r.stderr);
|
|
assert.ok(JSON.parse(r.stdout).errors.some((e) => e.code === 'BRIEF_INVALID_CEREMONY'));
|
|
const ia = spawnSync(process.execPath, [join(link, 'validators', 'intent-approval.mjs'), '--check', '--json', join(d, 'b.md')], { encoding: 'utf8' });
|
|
assert.equal(ia.status, 1, 'intent-approval runs (and stops on the unapproved brief) through the same path');
|
|
assert.ok(JSON.parse(ia.stdout).errors.some((e) => e.code === 'BRIEF_INTENT_NOT_APPROVED'));
|
|
} finally { rmSync(d, { recursive: true, force: true }); }
|
|
});
|