forked from yvv/decision
- engine/ : parametric (médiane basse, cristallisation, impact linear-share, bimodalité), state (canTransition 9 gardes doctrinales + windowOutcome), settings (resolveSettings + replis), triage (R-U→R6, phrases françaises), impact (concernés calculés), électionResult (blanc, quorum, égalité sans départage machine) — 296 tests vitest verts - moods.css v2 : Source/Margelle/Nappe/Minuit (champ lexical du puits), tokens routes/états, socle borderless, print A4, tampon 井 - data/persistence.ts : IndexedDB local-first, export/import Bundle, lignée - Seed Atelier du Canal (145 Ko, tous les états de l'UI) + test - backend/scripts/export_seed_bundle.py (extraction Ğ1, bundle à générer) - test anti-lexique (marqueur ld-v2) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
609 lines
18 KiB
TypeScript
609 lines
18 KiB
TypeScript
/**
|
||
* Tests du moteur de routage — « Le chemin ».
|
||
* Ordre des règles vérifié : R-U, R0a, R0b, R0c, R2, R3, R4, R5, R6.
|
||
* Fonctions pures : fixtures assemblées à la main, aucun store.
|
||
*/
|
||
import { describe, expect, it } from 'vitest'
|
||
import { detectParametricHint, triage } from '../../app/engine/triage'
|
||
import {
|
||
BINARY_COST,
|
||
PARAMETRIC_ALT,
|
||
RECORD_ALT,
|
||
URGENT_REFUSED,
|
||
} from '../../app/lexicon'
|
||
import type {
|
||
Clause,
|
||
CollectiveSettings,
|
||
Mandate,
|
||
TriageContext,
|
||
TriageInput,
|
||
} from '../../app/types/domain'
|
||
|
||
const T0 = '2026-01-01T00:00:00Z'
|
||
const T9 = '2027-01-01T00:00:00Z'
|
||
|
||
function mkSettings(over: {
|
||
triage?: Partial<CollectiveSettings['triage']>
|
||
protocolByRange?: Partial<CollectiveSettings['protocolByRange']>
|
||
} = {}): CollectiveSettings {
|
||
return {
|
||
triage: {
|
||
smallGroupMax: 5,
|
||
collectiveMin: 50,
|
||
consentMax: 7,
|
||
objectionWindowHours: 48,
|
||
adviceWindowHours: 72,
|
||
framingDays: 14,
|
||
concernEscalateRatio: 0.5,
|
||
recurrenceThreshold: 3,
|
||
reviewDelayDays: 90,
|
||
requireEffects: 'binding',
|
||
...over.triage,
|
||
},
|
||
protocolByRange: { consent: 'proto-consent', ...over.protocolByRange },
|
||
}
|
||
}
|
||
|
||
function mkInput(over: Partial<TriageInput> = {}): TriageInput {
|
||
return {
|
||
title: 'Repeindre la salle',
|
||
tags: [],
|
||
scope: { selfOnly: false, circleIds: [], personIds: [] },
|
||
reversibility: 'costly',
|
||
weight: 'light',
|
||
urgent: false,
|
||
...over,
|
||
}
|
||
}
|
||
|
||
function mkCtx(over: Partial<TriageContext> = {}): TriageContext {
|
||
return {
|
||
myActiveMandates: [],
|
||
otherActiveMandates: [],
|
||
matchingClauses: [],
|
||
similarRecentCount: 0,
|
||
similarRecordedCount: 0,
|
||
computedConcernedIds: ['p1', 'p2', 'p3'],
|
||
...over,
|
||
}
|
||
}
|
||
|
||
function mkMandate(over: Partial<Mandate> = {}): Mandate {
|
||
return {
|
||
id: 'm1',
|
||
collectiveId: 'col',
|
||
createdAt: T0,
|
||
updatedAt: T0,
|
||
title: 'Modération',
|
||
holderId: 'p-holder',
|
||
originDecisionId: 'd0',
|
||
domain: { circleIds: ['c1', 'c2'], tags: [] },
|
||
startsAt: T0,
|
||
endsAt: T9,
|
||
electorCircleId: 'c1',
|
||
nominationMethod: 'consent',
|
||
reports: [],
|
||
status: 'active',
|
||
...over,
|
||
}
|
||
}
|
||
|
||
function mkClause(over: Partial<Clause> = {}): Clause {
|
||
return {
|
||
id: 'cl1',
|
||
collectiveId: 'col',
|
||
createdAt: T0,
|
||
updatedAt: T0,
|
||
docId: 'doc1',
|
||
section: 'E',
|
||
position: 9,
|
||
code: 'E9',
|
||
title: 'Acte Forgeron',
|
||
inertia: 'high',
|
||
...over,
|
||
}
|
||
}
|
||
|
||
const scoped = (circleIds: string[]): TriageInput['scope'] =>
|
||
({ selfOnly: false, circleIds, personIds: [] })
|
||
|
||
describe('detectParametricHint — nombre, %, montant dans la phrase', () => {
|
||
it('détecte un montant en euros', () => {
|
||
expect(detectParametricHint('Fixer la cotisation à 25 €')).toBe(true)
|
||
})
|
||
|
||
it('ne détecte rien dans une phrase sans chiffre', () => {
|
||
expect(detectParametricHint('Repeindre la salle')).toBe(false)
|
||
})
|
||
|
||
it('détecte un pourcentage', () => {
|
||
expect(detectParametricHint('Porter la part commune à douze %')).toBe(true)
|
||
})
|
||
|
||
it('détecte l\'unité DU en majuscules', () => {
|
||
expect(detectParametricHint('Verser un DU aux nouveaux venus')).toBe(true)
|
||
})
|
||
|
||
it('ne confond pas l\'article « du » avec l\'unité DU', () => {
|
||
expect(detectParametricHint('Changer du mobilier dans la salle')).toBe(false)
|
||
})
|
||
})
|
||
|
||
describe('R-U — urgence', () => {
|
||
it('conservatoire : route sous-jacente calculée sans urgence, fenêtre doublée, chaîne posée', () => {
|
||
// Sans urgence, ce cas serait R4 (easy, 3 ≤ 5, fenêtre 72 h).
|
||
const verdict = triage(
|
||
mkInput({ urgent: true, reversibility: 'easy' }),
|
||
mkCtx(),
|
||
mkSettings(),
|
||
)
|
||
|
||
expect(verdict.rule).toBe('R-U')
|
||
expect(verdict.route).toBe('advice')
|
||
expect(verdict.conservatoryChain).toBe(true)
|
||
expect(verdict.windowHours).toBe(144)
|
||
expect(verdict.explanation).toBe('Décidé en urgence — le collectif ratifie.')
|
||
})
|
||
|
||
it('urgence × irréversible : refus — route normale, fenêtres minimales (jamais doublées)', () => {
|
||
// Sans urgence, ce cas serait R0b (mandat couvrant, fenêtre 48 h).
|
||
const verdict = triage(
|
||
mkInput({ urgent: true, reversibility: 'irreversible', scope: scoped(['c1']) }),
|
||
mkCtx({ myActiveMandates: [mkMandate()] }),
|
||
mkSettings(),
|
||
)
|
||
|
||
expect(verdict.rule).toBe('R-U')
|
||
expect(verdict.route).toBe('mandate')
|
||
expect(verdict.explanation).toBe(URGENT_REFUSED)
|
||
expect(verdict.conservatoryChain).toBeUndefined()
|
||
expect(verdict.windowHours).toBe(48)
|
||
})
|
||
})
|
||
|
||
describe('R0a — déjà décidé', () => {
|
||
it('clause en vigueur trouvée : consigner en principal, agir ou contester', () => {
|
||
const verdict = triage(mkInput(), mkCtx({ matchingClauses: [mkClause()] }), mkSettings())
|
||
|
||
expect(verdict.rule).toBe('R0a')
|
||
expect(verdict.route).toBe('record')
|
||
expect(verdict.explanation).toBe(
|
||
'C\'est déjà décidé (E9, Acte Forgeron) — agis, ou conteste la règle.',
|
||
)
|
||
expect(verdict.windowHours).toBeUndefined()
|
||
})
|
||
|
||
it('limite : viser explicitement la clause (amendsClauseId) passe à R3, pas R0a', () => {
|
||
// Choix documenté : contester la règle EST R3 — R0a ne bloque pas l'amendement.
|
||
const verdict = triage(
|
||
mkInput({ amendsClauseId: 'cl1' }),
|
||
mkCtx({ matchingClauses: [mkClause()] }),
|
||
mkSettings(),
|
||
)
|
||
|
||
expect(verdict.rule).toBe('R3')
|
||
expect(verdict.route).toBe('collective')
|
||
})
|
||
})
|
||
|
||
describe('R0b — mon mandat couvre', () => {
|
||
it('périmètre inclus dans le domaine du mandat : décide, c\'est tracé', () => {
|
||
const verdict = triage(
|
||
mkInput({ scope: scoped(['c1']) }),
|
||
mkCtx({ myActiveMandates: [mkMandate()] }),
|
||
mkSettings(),
|
||
)
|
||
|
||
expect(verdict.rule).toBe('R0b')
|
||
expect(verdict.route).toBe('mandate')
|
||
expect(verdict.windowHours).toBe(48)
|
||
expect(verdict.explanation).toBe('Ton mandat Modération couvre — décide, c\'est tracé.')
|
||
})
|
||
|
||
it('limite : couverture partielle du périmètre ne suffit pas', () => {
|
||
const verdict = triage(
|
||
mkInput({ scope: scoped(['c1', 'c3']) }),
|
||
mkCtx({ myActiveMandates: [mkMandate({ domain: { circleIds: ['c1', 'c2'], tags: [] } })] }),
|
||
mkSettings(),
|
||
)
|
||
|
||
expect(verdict.rule).not.toBe('R0b')
|
||
})
|
||
|
||
it('ordre : selfOnly avec mandat couvrant ⇒ R0b gagne, car R0b précède R2', () => {
|
||
// Choix documenté : le devoir de trace du mandat prime sur le solo.
|
||
const verdict = triage(
|
||
mkInput({ scope: { selfOnly: true, circleIds: ['c1'], personIds: [] } }),
|
||
mkCtx({ myActiveMandates: [mkMandate()] }),
|
||
mkSettings(),
|
||
)
|
||
|
||
expect(verdict.rule).toBe('R0b')
|
||
expect(verdict.route).toBe('mandate')
|
||
})
|
||
|
||
it('limite : périmètre sans cercle ⇒ jamais de couverture triviale', () => {
|
||
const verdict = triage(
|
||
mkInput({ scope: scoped([]) }),
|
||
mkCtx({ myActiveMandates: [mkMandate()] }),
|
||
mkSettings(),
|
||
)
|
||
|
||
expect(verdict.rule).not.toBe('R0b')
|
||
})
|
||
})
|
||
|
||
describe('R0c — le mandat d\'un autre couvre', () => {
|
||
it('transmets à sa ou son titulaire — le titre du mandat nomme le pouvoir', () => {
|
||
const verdict = triage(
|
||
mkInput({ scope: scoped(['c1']) }),
|
||
mkCtx({ otherActiveMandates: [mkMandate({ holderId: 'p-other' })] }),
|
||
mkSettings(),
|
||
)
|
||
|
||
expect(verdict.rule).toBe('R0c')
|
||
expect(verdict.route).toBe('transmit')
|
||
expect(verdict.explanation).toBe(
|
||
'Le mandat Modération couvre — transmets à sa ou son titulaire.',
|
||
)
|
||
})
|
||
|
||
it('ordre : mon mandat gagne sur celui d\'un autre (R0b avant R0c)', () => {
|
||
const verdict = triage(
|
||
mkInput({ scope: scoped(['c1']) }),
|
||
mkCtx({
|
||
myActiveMandates: [mkMandate({ id: 'm-me', title: 'Trésorerie' })],
|
||
otherActiveMandates: [mkMandate({ id: 'm-other', holderId: 'p-other' })],
|
||
}),
|
||
mkSettings(),
|
||
)
|
||
|
||
expect(verdict.rule).toBe('R0b')
|
||
})
|
||
|
||
it('limite : un mandat expiré ne couvre pas', () => {
|
||
const verdict = triage(
|
||
mkInput({ scope: scoped(['c1']) }),
|
||
mkCtx({ otherActiveMandates: [mkMandate({ status: 'expired' })] }),
|
||
mkSettings(),
|
||
)
|
||
|
||
expect(verdict.rule).not.toBe('R0c')
|
||
})
|
||
})
|
||
|
||
describe('R2 — moi seul', () => {
|
||
it('selfOnly : décide, zéro fenêtre', () => {
|
||
const verdict = triage(
|
||
mkInput({ scope: { selfOnly: true, circleIds: [], personIds: [] } }),
|
||
mkCtx({ computedConcernedIds: [] }),
|
||
mkSettings(),
|
||
)
|
||
|
||
expect(verdict.rule).toBe('R2')
|
||
expect(verdict.route).toBe('solo')
|
||
expect(verdict.explanation).toBe('Personne d\'autre n\'est concerné — décide.')
|
||
expect(verdict.windowHours).toBeUndefined()
|
||
expect(verdict.reviewRequired).toBe(false)
|
||
})
|
||
|
||
it('limite : irréversible sur soi ⇒ revoyure suggérée (pré-coche retirable côté UI)', () => {
|
||
// Le moteur suggère (reviewRequired) ; la pré-coche retirable est un fait
|
||
// d'UI — sur soi, l'outil ne s'interpose jamais.
|
||
const verdict = triage(
|
||
mkInput({
|
||
scope: { selfOnly: true, circleIds: [], personIds: [] },
|
||
reversibility: 'irreversible',
|
||
}),
|
||
mkCtx({ computedConcernedIds: [] }),
|
||
mkSettings(),
|
||
)
|
||
|
||
expect(verdict.rule).toBe('R2')
|
||
expect(verdict.reviewRequired).toBe(true)
|
||
})
|
||
})
|
||
|
||
describe('R3 — clause visée', () => {
|
||
const clauseSettings = mkSettings({
|
||
protocolByRange: {
|
||
clauseByInertia: {
|
||
low: 'proto-low',
|
||
standard: 'proto-standard',
|
||
high: 'proto-high',
|
||
max: 'proto-max',
|
||
},
|
||
},
|
||
})
|
||
|
||
it('l\'inertie de la clause choisit le protocole', () => {
|
||
const verdict = triage(
|
||
mkInput({ amendsClauseId: 'cl1' }),
|
||
mkCtx({ matchingClauses: [mkClause({ inertia: 'high' })] }),
|
||
clauseSettings,
|
||
)
|
||
|
||
expect(verdict.rule).toBe('R3')
|
||
expect(verdict.route).toBe('collective')
|
||
expect(verdict.protocolId).toBe('proto-high')
|
||
expect(verdict.explanation).toBe(
|
||
'Tu proposes une version de E9 — son inertie s\'applique : vote de ceux qu\'elle gouverne.',
|
||
)
|
||
})
|
||
|
||
it('repli : sans clauseByInertia, le consentement s\'applique', () => {
|
||
const verdict = triage(
|
||
mkInput({ amendsClauseId: 'cl1' }),
|
||
mkCtx({ matchingClauses: [mkClause()] }),
|
||
mkSettings(),
|
||
)
|
||
|
||
expect(verdict.protocolId).toBe('proto-consent')
|
||
})
|
||
|
||
it('poids structurel ⇒ fenêtre de formulation posée', () => {
|
||
const verdict = triage(
|
||
mkInput({ amendsClauseId: 'cl1', weight: 'structural' }),
|
||
mkCtx({ matchingClauses: [mkClause()] }),
|
||
clauseSettings,
|
||
)
|
||
|
||
expect(verdict.framingDays).toBe(14)
|
||
})
|
||
|
||
it('limite : clause introuvable dans l\'index ⇒ consentement et phrase générique', () => {
|
||
const verdict = triage(
|
||
mkInput({ amendsClauseId: 'cl-inconnue' }),
|
||
mkCtx({ matchingClauses: [] }),
|
||
clauseSettings,
|
||
)
|
||
|
||
expect(verdict.rule).toBe('R3')
|
||
expect(verdict.protocolId).toBe('proto-consent')
|
||
expect(verdict.explanation).toBe(
|
||
'Tu proposes une version de cette règle — son inertie s\'applique : vote de ceux qu\'elle gouverne.',
|
||
)
|
||
})
|
||
})
|
||
|
||
describe('R4 — petit cercle réversible', () => {
|
||
it('réversible et 3 personnes : demande leur avis puis décide (72 h)', () => {
|
||
const verdict = triage(mkInput({ reversibility: 'easy' }), mkCtx(), mkSettings())
|
||
|
||
expect(verdict.rule).toBe('R4')
|
||
expect(verdict.route).toBe('advice')
|
||
expect(verdict.windowHours).toBe(72)
|
||
expect(verdict.explanation).toBe(
|
||
'Réversible et 3 personnes concernées — demande leur avis puis décide.',
|
||
)
|
||
})
|
||
|
||
it('limite : accord du singulier pour 1 personne', () => {
|
||
const verdict = triage(
|
||
mkInput({ reversibility: 'easy' }),
|
||
mkCtx({ computedConcernedIds: ['p1'] }),
|
||
mkSettings(),
|
||
)
|
||
|
||
expect(verdict.explanation).toBe(
|
||
'Réversible et 1 personne concernée — demande leur avis puis décide.',
|
||
)
|
||
})
|
||
|
||
it('limite : au-delà de smallGroupMax, l\'escalade collective s\'applique', () => {
|
||
const verdict = triage(
|
||
mkInput({ reversibility: 'easy' }),
|
||
mkCtx({ computedConcernedIds: ['p1', 'p2', 'p3', 'p4', 'p5', 'p6'] }),
|
||
mkSettings(),
|
||
)
|
||
|
||
expect(verdict.rule).toBe('R5')
|
||
})
|
||
|
||
it('limite : coûteux à défaire ⇒ jamais R4, même à 3 personnes', () => {
|
||
const verdict = triage(mkInput({ reversibility: 'costly' }), mkCtx(), mkSettings())
|
||
|
||
expect(verdict.rule).toBe('R5')
|
||
})
|
||
})
|
||
|
||
describe('R5 — collectif, modalité par taille du périmètre', () => {
|
||
const ids = (n: number): string[] => Array.from({ length: n }, (_, i) => `p${i}`)
|
||
const fullSettings = mkSettings({
|
||
protocolByRange: { nuanced: 'proto-nuanced', large: 'proto-large' },
|
||
})
|
||
|
||
it('n ≤ consentMax : un tour d\'accord suffit', () => {
|
||
const verdict = triage(
|
||
mkInput(),
|
||
mkCtx({ computedConcernedIds: ids(6) }),
|
||
fullSettings,
|
||
)
|
||
|
||
expect(verdict.rule).toBe('R5')
|
||
expect(verdict.route).toBe('collective')
|
||
expect(verdict.protocolId).toBe('proto-consent')
|
||
expect(verdict.explanation).toBe(
|
||
'Vous êtes 6 — un tour d\'accord suffit : sans objection, c\'est adopté.',
|
||
)
|
||
})
|
||
|
||
it('n ≤ collectiveMin : vote nuancé', () => {
|
||
const verdict = triage(
|
||
mkInput(),
|
||
mkCtx({ computedConcernedIds: ids(23) }),
|
||
fullSettings,
|
||
)
|
||
|
||
expect(verdict.protocolId).toBe('proto-nuanced')
|
||
expect(verdict.explanation).toBe(
|
||
'Vous êtes 23 — vote nuancé : chacun se prononce en nuances, pas en camps.',
|
||
)
|
||
})
|
||
|
||
it('repli : sans protocole nuancé, le consentement s\'applique', () => {
|
||
const verdict = triage(mkInput(), mkCtx({ computedConcernedIds: ids(23) }), mkSettings())
|
||
|
||
expect(verdict.protocolId).toBe('proto-consent')
|
||
})
|
||
|
||
it('n > collectiveMin : la modalité choisie par le Pacte', () => {
|
||
const verdict = triage(
|
||
mkInput(),
|
||
mkCtx({ computedConcernedIds: ids(60) }),
|
||
fullSettings,
|
||
)
|
||
|
||
expect(verdict.protocolId).toBe('proto-large')
|
||
expect(verdict.explanation).toBe(
|
||
'Vous êtes 60 — la modalité que votre Pacte a choisie s\'applique.',
|
||
)
|
||
})
|
||
|
||
it('repli : sans protocole large, le consentement s\'applique', () => {
|
||
const verdict = triage(mkInput(), mkCtx({ computedConcernedIds: ids(60) }), mkSettings())
|
||
|
||
expect(verdict.protocolId).toBe('proto-consent')
|
||
})
|
||
|
||
it('poids structurel ⇒ formulation + revoyure + gravure suggérée', () => {
|
||
const verdict = triage(
|
||
mkInput({ weight: 'structural' }),
|
||
mkCtx({ computedConcernedIds: ids(6) }),
|
||
fullSettings,
|
||
)
|
||
|
||
expect(verdict.framingDays).toBe(14)
|
||
expect(verdict.reviewRequired).toBe(true)
|
||
expect(verdict.engravingSuggested).toBe(true)
|
||
})
|
||
|
||
it('irréversible ⇒ revoyure exigée, sans gravure automatique', () => {
|
||
const verdict = triage(
|
||
mkInput({ reversibility: 'irreversible' }),
|
||
mkCtx({ computedConcernedIds: ids(6) }),
|
||
fullSettings,
|
||
)
|
||
|
||
expect(verdict.reviewRequired).toBe(true)
|
||
expect(verdict.engravingSuggested).toBe(false)
|
||
})
|
||
})
|
||
|
||
describe('R6 — récurrence et maturation (surcouche, jamais bloquante)', () => {
|
||
it('3 décisions similaires récentes ⇒ suggère de réclamer un mandat, sans changer la route', () => {
|
||
const verdict = triage(
|
||
mkInput({ tags: ['cuisine', 'achats'] }),
|
||
mkCtx({ similarRecentCount: 3 }),
|
||
mkSettings(),
|
||
)
|
||
|
||
expect(verdict.rule).toBe('R5')
|
||
expect(verdict.suggestion?.kind).toBe('claim-mandate')
|
||
})
|
||
|
||
it('3 consignations similaires ⇒ suggère de protocoliser la pratique', () => {
|
||
const verdict = triage(mkInput(), mkCtx({ similarRecordedCount: 3 }), mkSettings())
|
||
|
||
expect(verdict.suggestion?.kind).toBe('protocolize')
|
||
})
|
||
|
||
it('les deux seuils atteints ⇒ réclamer un mandat passe en premier', () => {
|
||
const verdict = triage(
|
||
mkInput(),
|
||
mkCtx({ similarRecentCount: 3, similarRecordedCount: 3 }),
|
||
mkSettings(),
|
||
)
|
||
|
||
expect(verdict.suggestion?.kind).toBe('claim-mandate')
|
||
})
|
||
|
||
it('sous le seuil ⇒ aucune suggestion', () => {
|
||
const verdict = triage(
|
||
mkInput(),
|
||
mkCtx({ similarRecentCount: 2, similarRecordedCount: 2 }),
|
||
mkSettings(),
|
||
)
|
||
|
||
expect(verdict.suggestion).toBeUndefined()
|
||
})
|
||
})
|
||
|
||
describe('Repli sans protocole de consentement (bundle corrompu)', () => {
|
||
const broken = mkSettings({ protocolByRange: { consent: '' } })
|
||
|
||
it('route sur avis avec la bannière — prioritaire même sur un mandat couvrant', () => {
|
||
const verdict = triage(
|
||
mkInput({ scope: scoped(['c1']) }),
|
||
mkCtx({ myActiveMandates: [mkMandate()] }),
|
||
broken,
|
||
)
|
||
|
||
expect(verdict.route).toBe('advice')
|
||
expect(verdict.explanation).toBe('Aucun protocole — crée-le ou décide sur avis.')
|
||
expect(verdict.windowHours).toBe(72)
|
||
})
|
||
|
||
it('sauf R2 : moi seul décide sans protocole', () => {
|
||
const verdict = triage(
|
||
mkInput({ scope: { selfOnly: true, circleIds: [], personIds: [] } }),
|
||
mkCtx(),
|
||
broken,
|
||
)
|
||
|
||
expect(verdict.rule).toBe('R2')
|
||
expect(verdict.route).toBe('solo')
|
||
})
|
||
|
||
it('sauf consigner : une clause en vigueur reste consignable sans protocole', () => {
|
||
const verdict = triage(mkInput(), mkCtx({ matchingClauses: [mkClause()] }), broken)
|
||
|
||
expect(verdict.rule).toBe('R0a')
|
||
expect(verdict.route).toBe('record')
|
||
})
|
||
})
|
||
|
||
describe('Alternatives permanentes — « Je choisis autrement »', () => {
|
||
it('toujours présentes : réglage, consigner, binaire avec son coût', () => {
|
||
const verdict = triage(mkInput(), mkCtx(), mkSettings())
|
||
|
||
expect(verdict.alternatives).toHaveLength(3)
|
||
const labels = verdict.alternatives.map(a => a.label)
|
||
expect(labels).toContain(PARAMETRIC_ALT)
|
||
expect(labels).toContain(RECORD_ALT)
|
||
const binary = verdict.alternatives.find(a => a.cost === BINARY_COST)
|
||
expect(binary).toBeDefined()
|
||
expect(binary?.route).toBe('collective')
|
||
})
|
||
|
||
it('présentes sur toutes les routes, solo et consigner comprises', () => {
|
||
const solo = triage(
|
||
mkInput({ scope: { selfOnly: true, circleIds: [], personIds: [] } }),
|
||
mkCtx(),
|
||
mkSettings(),
|
||
)
|
||
const record = triage(mkInput(), mkCtx({ matchingClauses: [mkClause()] }), mkSettings())
|
||
|
||
expect(solo.alternatives).toHaveLength(3)
|
||
expect(record.alternatives).toHaveLength(3)
|
||
})
|
||
|
||
it('montant détecté ⇒ le réglage passe en premier et parametricHint est posé', () => {
|
||
const verdict = triage(
|
||
mkInput({ title: 'Fixer la cotisation à 25 €' }),
|
||
mkCtx(),
|
||
mkSettings(),
|
||
)
|
||
|
||
expect(verdict.parametricHint).toBe(true)
|
||
expect(verdict.alternatives[0]?.label).toBe(PARAMETRIC_ALT)
|
||
})
|
||
|
||
it('sans montant ⇒ pas de mise en avant du réglage', () => {
|
||
const verdict = triage(mkInput(), mkCtx(), mkSettings())
|
||
|
||
expect(verdict.parametricHint).toBeUndefined()
|
||
expect(verdict.alternatives[0]?.label).not.toBe(PARAMETRIC_ALT)
|
||
})
|
||
})
|