import { describe, expect, it } from 'vitest' import { groupVerdicts, renderVerdict } from '../render/verdict-md.js' import { mergeJudgments } from '../judgments.js' import type { Finding, JudgmentItem } from '../types.js' const OPTS = { date: '2026-08-30', rulesetVersion: '1.0.0', sourcesHash: 'abc123' } const f = (over: Partial): Finding => ({ ruleId: 'DM-001', dimension: 'data-model', severity: 'ok', scope: { app: 'CRM', module: 'PIPELINE' }, message: 'ok.', ...over, }) describe('verdict-md — le contrat de parsing existant est PRÉSERVÉ', () => { it('groupe par (dimension × scope) sur les chemins de verdict existants', () => { const targets = groupVerdicts( [ f({}), f({ dimension: 'rbac', ruleId: 'RBAC-001' }), f({ dimension: 'actors', ruleId: 'ACT-001', scope: {} }), f({ dimension: 'menu', ruleId: 'MENU-004', scope: { app: 'CRM' } }), f({ ruleId: 'CONV-001', dimension: 'use-cases', scope: {} }), // summary-owned ], [], ) expect(targets.map((t) => t.relPath).sort()).toEqual([ 'CRM/PIPELINE/_audit/entité.md', 'CRM/PIPELINE/_audit/rbac.md', 'CRM/_audit/menu.md', '_audit/actors.md', ]) }) it('rend l’ancre ligne 1 (+ fraîcheur), le header ligne 2, le Verdict ligne 3 dans l’ordre N warn · M err · K ok', () => { const target = groupVerdicts( [ f({ severity: 'err', ruleId: 'DM-013', message: 'FK implicite.', evidence: ['Budget.OwnerId'] }), f({ severity: 'warn', ruleId: 'DM-009', message: 'Isolée.' }), f({ severity: 'ok', ruleId: 'DM-001', message: '3 entités.' }), ], [], )[0]! const md = renderVerdict(target, OPTS) const lines = md.split('\n') expect(lines[0]).toBe('') expect(lines[1]).toBe('# Audit entité — CRM / PIPELINE') expect(lines[2]).toBe('_2026-08-30 · Verdict : ❌ 1 warn · 1 err · 1 ok_') // The counters stay machine-readable in the documented order. const m = /Verdict\s*:\s*\S+\s+(\d+)\s+warn\s+·\s+(\d+)\s+err\s+·\s+(\d+)\s+ok/.exec(md)! expect([m[1], m[2], m[3]]).toEqual(['1', '1', '1']) expect(md).toContain('## ✅ Conforme') expect(md).toContain('## ⚠️ Avertissements') expect(md).toContain('## ❌ Bloquants') expect(md).toContain('- **DM-013** — FK implicite.') expect(md).toContain(' - Budget.OwnerId') }) it('le jugement en attente s’AJOUTE APRÈS `K ok` (le lecteur des 3 compteurs reste intact) + section 🧠', () => { const pending: JudgmentItem[] = [ { ruleId: 'UC-011', dimension: 'use-cases', scope: { app: 'CRM', module: 'PIPELINE' }, maxSeverity: 'err', question: 'Périmètre ?', excerpts: ['UC-1 — titre'] }, ] const target = groupVerdicts([f({ dimension: 'use-cases', ruleId: 'UC-001' })], pending)[0]! const md = renderVerdict(target, OPTS) expect(md).toContain('· 0 err · 1 ok · 1 à arbitrer_') const m = /Verdict\s*:\s*\S+\s+(\d+)\s+warn\s+·\s+(\d+)\s+err\s+·\s+(\d+)\s+ok/.exec(md)! expect(m[3]).toBe('1') expect(md).toContain('') expect(md).toContain('## 🧠 Jugement requis') }) it('un verdict sans warn ni err est ✅ ; avec warn seulement ⚠️', () => { const okMd = renderVerdict(groupVerdicts([f({})], [])[0]!, OPTS) expect(okMd).toContain('Verdict : ✅ 0 warn · 0 err · 1 ok') const warnMd = renderVerdict(groupVerdicts([f({ severity: 'warn' })], [])[0]!, OPTS) expect(warnMd).toContain('Verdict : ⚠️ 1 warn · 0 err · 0 ok') }) }) describe('judgments — fusion des décisions du skill', () => { const pending: JudgmentItem[] = [ { ruleId: 'UC-011', dimension: 'use-cases', scope: { app: 'CRM', module: 'PIPELINE' }, maxSeverity: 'err', question: 'Q1', excerpts: [] }, { ruleId: 'UC-023', dimension: 'use-cases', scope: { app: 'CRM', module: 'PIPELINE' }, maxSeverity: 'warn', question: 'Q2', excerpts: [] }, ] it('consomme l’item correspondant et produit le finding arbitré', () => { const r = mergeJudgments(pending, [ { ruleId: 'UC-011', dimension: 'use-cases', scope: { app: 'CRM', module: 'PIPELINE' }, resolution: 'ok', message: 'Périmètre respecté.' }, ]) expect(r.findings).toHaveLength(1) expect(r.findings[0]).toMatchObject({ ruleId: 'UC-011', severity: 'ok' }) expect(r.remaining.map((p) => p.ruleId)).toEqual(['UC-023']) expect(r.warnings).toEqual([]) }) it('PLAFONNE la résolution au maxSeverity de la règle (une règle warn ne devient jamais err)', () => { const r = mergeJudgments(pending, [ { ruleId: 'UC-023', dimension: 'use-cases', scope: { app: 'CRM', module: 'PIPELINE' }, resolution: 'err', message: 'Doublon réel.' }, ]) expect(r.findings[0]!.severity).toBe('warn') expect(r.warnings.some((w) => w.includes('plafonnée'))).toBe(true) }) it('une décision sans item en attente est un warning BRUYANT, jamais un drop silencieux', () => { const r = mergeJudgments(pending, [ { ruleId: 'DM-999', dimension: 'data-model', scope: {}, resolution: 'ok', message: 'x' }, ]) expect(r.findings).toEqual([]) expect(r.warnings.some((w) => w.includes('périmé'))).toBe(true) }) })