A brief may carry an optional `## Threat Model` with one line per bullet, `- Inside: <text>` or `- Outside: <text>`. Present and malformed (no such bullet, another label or form, empty text, two sections) is BRIEF_THREAT_MODEL_MALFORMED; absent is today's rule. No brief_version bump (the ceremony / artifact_store precedent). Sections are found fence- and comment-aware through intent-approval's new sectionTexts, so the validator and the hash count the same sections; brief-validator exports parseThreatModel for the reader. The intent hash covers `## Threat Model` when present (two sections -> null hash, as Intent and Goal), so an edited, added or removed threat model makes the approval stale; a brief without one hashes exactly as before. The stale message and the /trekplan halt table name the section. /trekbrief offers the section in its question round when the task touches a guard, a gate, untrusted input, secrets or publishing, writes it only when given, shows it verbatim at the Phase 4h approval, and --approve reads it out. The template documents it; brief-gate-coverage pairs the new code. Part 2, step 2.1 of the E plan. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
197 lines
5.8 KiB
JavaScript
197 lines
5.8 KiB
JavaScript
// tests/validators/brief-gate-coverage.test.mjs
|
|
// SKAL-1·4a — two-sided (class-balanced) coverage for every error-severity
|
|
// BRIEF_* BLOCKER the brief-validator can emit.
|
|
//
|
|
// brief-validator.test.mjs already tests many codes; several were FAILURE-only
|
|
// (only the raising case asserted) and three were untested. This meta-test pins
|
|
// a class-balanced pair { fail, pass } for EACH in-scope BLOCKER and a parity
|
|
// guard so a code can't sit in IN_SCOPE_CODES without a paired case. It removes
|
|
// the one-sided-optimization risk programmatically, not by inspection.
|
|
//
|
|
// In scope = ERROR-severity BRIEF_* codes the validator pushes to errors[]
|
|
// (BLOCKERs). EXCLUDED: warning-severity codes (BRIEF_TLDR_TOO_LONG,
|
|
// BRIEF_VERSION_FORMAT, BRIEF_VERSION_BELOW_MINIMUM, BRIEF_PARTIAL_SKIPPED) — a
|
|
// "passing" counterpart to a warning is not a BLOCKER concern — and the
|
|
// file-level BRIEF_NOT_FOUND / BRIEF_READ_ERROR which live in validateBrief()
|
|
// (filesystem), not validateBriefContent().
|
|
//
|
|
// LIMITATION: brief-validator.mjs does not export its code set (codes are string
|
|
// literals inside issue(...)), so IN_SCOPE_CODES is HAND-MAINTAINED. A NEW
|
|
// validator BLOCKER must be added here manually; the parity guard catches
|
|
// list/case drift, not validator drift. (Operator chose the hand-maintained list
|
|
// over exporting BRIEF_ERROR_CODES from the forbidden_paths-guarded validator.)
|
|
|
|
import { test } from 'node:test';
|
|
import { strict as assert } from 'node:assert';
|
|
import { validateBriefContent } from '../../lib/validators/brief-validator.mjs';
|
|
|
|
// ---- valid bases (mirror tests/validators/brief-validator.test.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.
|
|
`;
|
|
|
|
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.
|
|
`;
|
|
|
|
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.
|
|
`;
|
|
|
|
// convenience builders
|
|
const withLine = (s, line) => s.replace('source: interview\n', `source: interview\n${line}`);
|
|
|
|
// ---- the canonical in-scope set + a class-balanced pair for each -------------
|
|
|
|
const IN_SCOPE_CODES = [
|
|
'BRIEF_MISSING_FIELD',
|
|
'BRIEF_BAD_STATUS',
|
|
'BRIEF_STATE_INCOHERENT',
|
|
'BRIEF_WRONG_TYPE',
|
|
'BRIEF_BAD_FINDINGS_TYPE',
|
|
'BRIEF_MISSING_SECTION',
|
|
'BRIEF_INVALID_FRAMING',
|
|
'BRIEF_MISSING_FRAMING',
|
|
'BRIEF_THREAT_MODEL_MALFORMED',
|
|
];
|
|
|
|
const CASES = {
|
|
BRIEF_MISSING_FIELD: {
|
|
fail: GOOD_BRIEF.replace(/^research_topics: 2\n/m, ''),
|
|
pass: GOOD_BRIEF,
|
|
},
|
|
BRIEF_BAD_STATUS: {
|
|
fail: GOOD_BRIEF.replace('research_status: pending', 'research_status: maybe'),
|
|
pass: GOOD_BRIEF,
|
|
},
|
|
BRIEF_STATE_INCOHERENT: {
|
|
fail: GOOD_BRIEF.replace('research_status: pending', 'research_status: skipped'),
|
|
pass: GOOD_BRIEF,
|
|
},
|
|
BRIEF_WRONG_TYPE: {
|
|
fail: GOOD_BRIEF.replace('type: trekbrief', 'type: notabrief'),
|
|
pass: GOOD_BRIEF,
|
|
},
|
|
BRIEF_BAD_FINDINGS_TYPE: {
|
|
fail: REVIEW_AS_BRIEF.replace(/findings:\n - 0123[\s\S]*?- fedcba[0-9a-f]+/, 'findings: not-an-array'),
|
|
pass: REVIEW_AS_BRIEF,
|
|
},
|
|
BRIEF_MISSING_SECTION: {
|
|
fail: GOOD_BRIEF.replace(/## Intent\n\nWhy this matters\.\n\n/, ''),
|
|
pass: GOOD_BRIEF,
|
|
},
|
|
BRIEF_INVALID_FRAMING: {
|
|
fail: withLine(GOOD_BRIEF, 'framing: sideways\n'),
|
|
pass: GOOD_BRIEF_22,
|
|
},
|
|
BRIEF_MISSING_FRAMING: {
|
|
fail: GOOD_BRIEF_22.replace('framing: new-direction\n', ''),
|
|
pass: GOOD_BRIEF_22,
|
|
},
|
|
BRIEF_THREAT_MODEL_MALFORMED: {
|
|
fail: GOOD_BRIEF_22.replace('## Success Criteria', '## Threat Model\n\n- Scope: the API\n\n## Success Criteria'),
|
|
pass: GOOD_BRIEF_22.replace('## Success Criteria', '## Threat Model\n\n- Inside: a forged token\n- Outside: a stolen signing key\n\n## Success Criteria'),
|
|
},
|
|
};
|
|
|
|
test('gate coverage — CASES keys exactly match IN_SCOPE_CODES (no silent gap)', () => {
|
|
assert.deepEqual(Object.keys(CASES).sort(), [...IN_SCOPE_CODES].sort());
|
|
});
|
|
|
|
test('gate coverage — every in-scope BRIEF_* BLOCKER raises on its fail case', () => {
|
|
for (const code of IN_SCOPE_CODES) {
|
|
const r = validateBriefContent(CASES[code].fail, { strict: true });
|
|
assert.ok(
|
|
r.errors.find((e) => e.code === code),
|
|
`${code}: fail case did NOT raise it; errors=${JSON.stringify(r.errors.map((e) => e.code))}`,
|
|
);
|
|
}
|
|
});
|
|
|
|
test('gate coverage — every in-scope BRIEF_* BLOCKER is absent on its (valid) pass case', () => {
|
|
for (const code of IN_SCOPE_CODES) {
|
|
const r = validateBriefContent(CASES[code].pass, { strict: true });
|
|
assert.ok(
|
|
!r.errors.find((e) => e.code === code),
|
|
`${code}: pass case wrongly raised it; errors=${JSON.stringify(r.errors.map((e) => e.code))}`,
|
|
);
|
|
assert.equal(r.valid, true, `${code}: pass case should be a fully valid brief; errors=${JSON.stringify(r.errors)}`);
|
|
}
|
|
});
|