ADR-0008 — Le harnais d'eval est vitest, pas un framework
5 900 tests, zero eval (ai-platform/0138). La suite couvre le deterministe — idempotence de facturation, cloisonnement tenant, heal du schema, webhooks Stripe, gardes de credits — et rien ne mesure ce qu'@Atlas et les…
Statut
Accepté · 2026-09-05
Piliers : ai-platform
Contexte
5 900 tests, zero eval (ai-platform/0138). La suite couvre le
deterministe — idempotence de facturation, cloisonnement tenant, heal du
schema, webhooks Stripe, gardes de credits — et rien ne mesure ce
qu'@Atlas et les cinq specialistes repondent.
C'est le seul sous-systeme sans filet, et c'est celui qu'on vend. La promesse produit n'est pas « la facturation est idempotente », c'est « l'IA opere le business ».
Quatre choses ne sont attrapees par rien aujourd'hui :
- une regression de prompt qui degrade le routage vers le bon specialiste ;
- un outil appele avec de mauvais arguments dans un cas limite ;
- une reponse qui affirme comme observe un chiffre qui etait estime — la faute que tout le pipeline Intelligence combat cote donnees, et que rien ne surveille cote generation ;
- une derive apres un changement de modele ou de version de passerelle.
L'item interdisait explicitement de trancher sur un article de blog.
Options
(a) DeepEval
Le plus complet sur les agents et la securite, TypeScript desormais open
source. Il apporte son propre vocabulaire de metriques (GEval,
AnswerRelevancy, Faithfulness), son propre runner, et une ecologie
qui vient du monde Python.
Ce qu'il aurait rendu : des metriques pretes, une nomenclature partagee avec le reste de l'industrie.
Ce qu'il coutait : un second runner a cote de vitest, un second endroit ou une CI peut echouer, et une couche d'abstraction entre nos quatre cas et ce qui est reellement envoye au modele. Pour quatre familles de cas, le vocabulaire coute plus qu'il ne rend.
(b) Promptfoo
Declaratif, matriciel, excellent en CI. Les cas vivent en YAML.
Ce qu'il coutait, et c'est redhibitoire ici : nos assertions ne sont pas
des comparaisons de chaines. « Le bon specialiste a ete appele » se lit
dans result.toolCalls, pas dans le texte. « L'argument storeId est
celui qu'on a donne, pas un placeholder » non plus. Exprimer ca en YAML
demande des assertions JavaScript inline — c'est-a-dire du JavaScript,
dans du YAML, sans typage.
Et un second langage de configuration est une seconde source de verite sur ce que le repo teste.
(c) Un socle sur vitest
Un eval est un test avec un juge non deterministe et un seuil. Le repo a
deja 5 900 tests, une culture de gardes derivees (docs:claims,
intel:coverage, fleet:scope) et un vi.mock par surface.
Décision
(c). evals/ a la racine, vitest.evals.config.ts, pnpm evals.
Trois raisons, dans l'ordre :
- Les assertions qui comptent sont deterministes. Trois de nos quatre
familles — routage, selection d'outil, refus — se verifient en lisant
toolCallset les arguments. Elles n'ont pas besoin d'un juge, elles ont besoin d'un modele reel et d'unexpect. C'est exactement un test vitest avec une latence. - Le juge n'est utile que sur une famille. L'honnetete des chiffres
est une propriete textuelle : elle demande un modele qui lit une regle
et rend un verdict. Ca tient en trente lignes (
support/harness.ts,judge()), et l'ecrire nous-memes nous a forces a une chose qu'aucun framework n'impose : la rubrique est adverse par construction — le juge recoit l'ordre de trouver la violation, et le defaut estfalse. Un juge a qui on demande « est-ce que ca va ? » repond oui. - La separation est la propriete de securite.
vitest.config.tsne collecte quesrc/**. Rien dansevals/n'est joignable depuispnpm test, depuis le hook pre-push, ni depuis le job CI qui garde chaque PR. Un eval appelle un modele ; un modele coute de l'argent. Un framework tiers aurait rendu cette frontiere dependante de SA configuration.
Conséquences
Ce qui est livre
| Fichier | Role |
|---|---|
evals/support/harness.ts | evalCase, ask, askWithTools, judge, le compteur de depense |
evals/routing.eval.ts | famille 1 — 7 cas, dont un piege lexical |
evals/number-honesty.eval.ts | famille 2 — 4 cas, juge adverse |
evals/tool-selection.eval.ts | famille 3 — 4 cas |
evals/refusal.eval.ts | famille 4 — 3 cas |
vitest.evals.config.ts | le runner, fileParallelism: false |
.github/workflows/evals.yml | cadence hebdomadaire + declenchement manuel |
src/test/evals-are-not-in-the-default-suite.test.ts | la frontiere, derivee |
Trois regles, tenues par du code et pas par une convention
- Jamais par accident. La garde dans
src/test/echoue si un fichier.eval.tsdevient joignable depuisvitest.config.ts, sipnpm evalsentre dans le hook pre-push, ou si le workflow CI par defaut l'appelle. - Gratuit hors ligne. Sans
AI_GATEWAY_API_KEY, chaque cas saute avec une raison lisible. Un contributeur dans un avion voit des skips, pas des echecs, et ne paie rien. - Plafonne.
EVAL_USD_BUDGET(defaut $0.50) borne la course entiere. Le compteur additionne les tokens de tous les appels, sujet et juge, et la course s'arrete quand le plafond est atteint. Le prix est une constante locale et non une lecture demodel-pricing.ts: une baisse de prix ne doit pas relever le plafond en silence.
Ce que ca ne fait pas
Ca ne mesure pas la qualite d'une reponse longue, ni la satisfaction. Ca mesure quatre proprietes verifiables. Un eval qui note « la reponse est-elle bonne ? » produit un nombre que personne ne sait interpreter et qui ne dit jamais quoi corriger.
Le sujet et le juge tournent tous deux sur le tier le moins cher. Un eval qui a besoin d'un modele frontier pour voir une regression mesure le modele, pas nous.
Alternatives écartées
- Faire tourner les evals a chaque push. Coute de l'argent a chaque commit et rend le hook pre-push dependant du reseau. La cadence hebdomadaire attrape les derives de modele et de passerelle, qui sont lentes ; les regressions de prompt sont attrapees par le declenchement manuel dans la PR qui touche au prompt.
- Un service d'observabilite commercial. Repond a une autre question (que s'est-il passe en production ?) et ne bloque rien avant le merge.
Note datee du 2026-09-25 (le texte ci-dessus n'est pas reecrit)
Trois phrases de cet ADR ne decrivent plus le harnais. La decision (vitest, pas un framework) tient ; ce sont des details d'execution qui ont bouge.
- « Le sujet et le juge tournent tous deux sur le tier le moins cher. »
Le jour meme, @Atlas Mini est passe de Haiku 4.5 a GPT 5.6 Luna
(
ATLAS_TIER_MODEL["atlas-mini"] = "mini"). Le sujet code en dur (modelId("haiku")) mesurait donc un modele qu'aucun tour client n'emploie : une regression de routage sur Luna passait tous les cas. Le sujet est desormais le modele qu'@Atlas Mini envoie, LU dans le catalogue a chaque course (evals/support/models.ts), etEVAL_SUBJECT_MODEL(inputsubject_modeldu workflow) en nomme un autre : une cle du catalogue, un tier @Atlas, ou un id Gateway brutprovider/modelpour un candidat pas encore au catalogue. Le juge reste Haiku, fixe : deux courses ne se comparent que si le correcteur est le meme. - « Le prix est une constante locale et non une lecture de
model-pricing.ts. » La constante etait le tarif de Haiku, appliquee a tout. Juste tant que le sujet etait Haiku, fausse des qu'il ne l'est plus : un plafond de $0.50 compte au tarif Haiku laisse un candidat a $10/$50 depenser dix fois plus. Chaque appel est maintenant compte au tarifwholesaledu catalogue de SON modele (sujet et juge separes). Un id inconnu du catalogue n'a pas de prix verifie : il est compte au tarif le plus eleve du catalogue, entree et sortie, pour que l'erreur termine la course trop tot plutot que de depenser sans accord. - «
EVAL_USD_BUDGETborne la course entiere. » C'etait deja faux avant ce changement, et verifie le 2026-09-25 : vitest isole chaque fichier dans son propre worker, donc le compteur de depense repart de zero a chaque fichier (trois fichiers qui incrementent un compteur de module lisent chacun 1). Le plafond vaut par FICHIER, quatre fichiers peuvent atteindre quatre fois le budget.isolate: falsedansvitest.evals.config.tsle partage sur la course (meme verification : 1, 2, 3). Non applique ici : ce fichier etait hors du perimetre du changement qui a trouve le defaut.