# Doctrine de maintenance des tests — régression ou obsolescence ?

Codifiée le 07/08/2026 (décision Enguerran). Complète `14_REGLES_GIT.md` :
« les tests accompagnent le correctif dans la même PR » vaut aussi pour leur
**mise à jour** et leur **suppression**.

## Le principe

**Un test n'appartient pas au code : il appartient au comportement décidé.**
C'est la traduction exécutable d'un critère d'acceptation ou d'une décision
produit. Tant que la décision tient, le test doit être vert. Quand la décision
change, le test change — dans la même PR que la feature, jamais séparément.

## La question qui tranche, quand un test passe au rouge

> Le comportement attendu a-t-il été VOLONTAIREMENT changé par une décision
> produit tracée ?

| Réponse | Diagnostic | Geste |
|---|---|---|
| **Non** | **Régression** | On corrige le CODE, pas le test. |
| **Oui** (issue, CA, retour de recette) | **Test obsolète** | On le met à jour ou on le supprime **dans la PR qui porte le changement**, décision référencée dans le commit. |
| **Personne ne sait** | Inconnu | On n'y touche pas avant d'avoir investigué : un test dont on a oublié la raison est souvent la seule documentation d'un bug passé. |

## Les trois mécanismes qui rendent la distinction possible

1. **Chaque test porte sa provenance** : un commentaire en tête relie le test à
   sa décision (« Retour Enguerran 07/08 : l'onglet Historique disparaît… »,
   « Régression : LessonResource filtrait support_type… »). Celui qui le verra
   rouge dans deux ans saura quelle décision il encode.
2. **La revue vérifie l'appariement** : une PR qui change un comportement sans
   toucher aux tests est suspecte ; une PR qui modifie des tests sans référencer
   de décision produit l'est autant (risque de maquiller une régression en
   « mise à jour »).
3. **La suite reste verte** : si du rouge s'accumule, « rouge » ne signifie plus
   rien et la distinction devient impossible (cf. dette héritée #97/#127). Une
   suite verte fait de chaque rouge une question à trancher immédiatement.

## Unitaires et non-régression ne vieillissent pas pareil

| | Test unitaire | Test de non-régression |
|---|---|---|
| Encode | la spec ACTUELLE d'un composant | « ce bug réel ne doit jamais revenir » (référence d'incident) |
| Évolue | souvent, au rythme des CA — réécriture banale | rarement — survit aux refontes tant que la fonctionnalité existe |
| Obsolète | dès que le CA change | seulement quand la FONCTIONNALITÉ disparaît |
| Suppression | anodine si le CA remplaçant est couvert | **décision explicite validée** : c'est accepter que le bug puisse revenir — jamais dans un « nettoyage » |

## Règles pratiques

- **On ne supprime pas de la couverture, on la fait suivre la décision** :
  un test rendu obsolète par une refonte est REMPLACÉ par le test du nouveau
  comportement dans la même PR (exemple vécu 07/08 : le test du repli
  « À propos » supprimé par la refonte v2 de la page cours a été remplacé par
  « la visionneuse reste montée au changement d'onglet »).
- **Un bug corrigé = un test qui l'empêche de revenir**, avec la référence du
  bug en commentaire.
- **La baseline d'échecs pré-existants est tracée en issues** (#97 back, #127
  front) et la règle de chaque PR est « zéro échec NOUVEAU » ; l'objectif de
  fond reste une suite 100 % verte, condition du signal.

## Les angles morts que « la suite est verte » ne couvre pas

Ils ne relèvent pas d'un trou de couverture mais d'un écart entre l'environnement de test
et celui de production. Les deux premiers ont été rencontrés le 09/08 (lot L4) ; la
procédure de déploiement (`12_PROCEDURE_DEPLOIEMENT.md`) les rappelle au moment où ils
comptent. Le premier a été RÉSORBÉ le 21/08 — il reste écrit, barré, parce qu'un angle mort
supprimé s'oublie et se recrée.

- ~~**La suite tourne sur SQLite, la production sur MySQL.**~~ **RÉSORBÉ le 21/08 (#578) :
  la suite tourne sur MySQL 8, et sur lui seul** — `phpunit.xml` désigne le pilote `mysql`,
  la CI lève un service MySQL 8.0, aucun job sqlite ne subsiste. Le rejeu manuel sur
  `efektiv_local` avant PR n'est donc plus la seule preuve : la CI le fait à chaque run.
  Ce que le basculement a coûté et rapporté, mesuré : **38 tests sont devenus rouges** le
  premier jour, tous pour une dépendance à sqlite (chaîne écrite dans une colonne booléenne
  ou dans un `enum`, clé étrangère jamais posée sous sqlite, auto-incrément supposé remis à
  zéro, `DECIMAL` rendu en flottant), et la suite passe d'environ 30 s à environ 70 s.
  **Le corollaire, lui, reste entier et devient plus fréquent** : certains états de données
  hérités de MyISAM ne sont **pas reproductibles** sur la base de test — quand c'est le cas,
  on l'écrit dans le fichier de test plutôt que de désactiver une contrainte pour fabriquer
  un état que la contrainte interdit (le test ne prouverait plus rien). Appliqué le 21/08 au
  cours sans sous-catégorie de `FrontCourseTest` (garantie suivie en propre : #582).
- **Le piège de l'`enum` en écriture** (frère de celui du 21/08 en lecture). Sur une colonne
  `enum('1','0')`, MySQL lit un **entier** comme un **indice**, pas comme une valeur :
  écrire `1` désigne l'indice 1 et tombe juste **par hasard** sur la valeur `'1'` ; écrire
  `0` désigne l'indice 0, c'est-à-dire la valeur invalide, et la ligne est refusée en mode
  strict (erreur 1265). **Sur une colonne `enum`, on écrit toujours la chaîne.**
- **`Mail::fake()` ne compile pas les gabarits.** Une erreur Blade reste invisible tant
  qu'aucun test ne rend réellement la vue (vécu : #55, création de compte en 500). Règle :
  **tout gabarit d'e-mail a au moins un test qui appelle `->render()`**, en plus des
  `Mail::assertSent()`.

## La vérification des tests est une ÉTAPE SÉPARÉE (règle du 12/08)

Ne JAMAIS enchaîner un commit, un push ou un merge derrière une commande de
test dans une chaîne shell. On lance les tests **seuls**, on **lit** le compte
de succès et d'échecs, puis on décide.

**Deux incidents le même jour, tous deux dus à une chaîne « maligne »** :

1. `php artisan test | grep -E "Tests:|FAILED" && git commit && … && merge`
   Le `grep` **réussit quand il trouve « FAILED »**. Trois tests rouges ont été
   commités, poussés et **mergés dans `development`** ; la liste unifiée rendait
   500 en ligne. Le code de sortie lu était celui du filtre, pas celui des tests.

2. `git push origin development || (git checkout -b … && gh pr create …)`
   Le push a **réussi** : le repli n'a jamais joué, et un commit est parti
   directement sur `development`, sans branche ni PR.

Dans les deux cas la commande protectrice n'a pas été exécutée, parce que la
précédente avait réussi *au sens du shell* et non au sens métier.

**En pratique**
- Une commande de test, une lecture, une décision. Jamais de `&&` entre un test
  et une action irréversible.
- Idem pour le lint et l'attente de CI.
- Se méfier de tout `grep` posé derrière une commande dont on veut le code de
  retour : filtrer l'affichage change le code de retour.
- Ne pas écrire de repli en `||` autour d'une action interdite dans son cas
  nominal (pousser sur `development`) : si c'est interdit, ça ne se tente pas.
