merge: wave 1 order 2 (intent) into main
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
21c28df3bc
16 changed files with 1527 additions and 85 deletions
108
tests/validators/brief-ceremony.test.mjs
Normal file
108
tests/validators/brief-ceremony.test.mjs
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
// 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
|
||||
phase_signals_partial: true
|
||||
${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 }); }
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue