---
title: "Jah : « 26 tests verts » ne dit rien du FULL — leçon 4.0.20"
subtitle: "Deux batteries CI complètes brûlées pour une preuve trop étroite, puis un flake d’appel qui n’était pas un timeout : la règle ciblé ≠ FULL est maintenant écrite, et le live est en 4.0.20."
date: 2026-10-06
time: "01:51"
author: VFG
publishedBy: code
status: pub
tags: ["vfjah", "jah", "ci-cd", "agents", "post-mortem"]
---

## TL;DR

La PR-B des **Retours** (STAB-07A/B/C) est en prod **4.0.20**. Avant d’y arriver, on a brûlé
deux tours de CI complète (FULL) pour des raisons évitables : annoncer « lint 0 + 26/26 ciblés »
comme porte de merge, puis traiter un flake E2E d’appel comme un simple timeout. La leçon est
dans `.agents/` ; l’habitant, lui, a un onglet **Retours** et des appels qui ne sonnent plus
chez un fantôme.

Liens proches :

- [Suivre tes retours](https://blog.vfg.lol/devblog/posts/vfjah/2026-10-05-suivre-tes-retours.md)
  (produit — à publier si encore en 404)
- [18 minutes pour vérifier 3 lignes](https://blog.vfg.lol/devblog/posts/vfjah/2026-09-30-jah-ci-dix-huit-minutes.md)
- [Journal du 30 septembre](https://blog.vfg.lol/devblog/posts/vfjah/2026-09-30-jah-journal-du-jour.md)

## 1. Le problème rencontré

On livrait le circuit **Retours** : capture avant ACK, sync GitHub en dry-run, APIs, onglet dans
**Nouveautés**. Les tests du lot étaient verts — vingt-six sur vingt-six, ciblés. Lint propre.
La tentation : coller `ci:full`, attendre le vert, merger.

Le FULL est rouge.

Pas à cause du « cœur » du lot. À cause de deux trous que le périmètre ciblé n’avait jamais
regardés :

1. **TDZ** dans `anonymize.mjs` — une variable `has` utilisée avant d’être déclarée. Invisible
   tant qu’on ne lance pas la suite d’anonymisation hors du lot.
2. **Suites absentes de `AREAS`** — les nouveaux tests `feedback-*` n’étaient pas dans le plan
   CI. Le ciblé ne les voit pas ; le FULL non plus… jusqu’à ce que quelqu’un les ajoute, ou
   jusqu’à ce que le reste du graphe les charge autrement. Bref : le plan de preuve mentait
   par omission.

Puis, deuxième FULL brûlé : flake E2E **phase7** « appel refusé » sous charge shard. On a
d’abord allongé un timeout à 45 s. Ça n’a pas suffi — parce que ce n’était pas un délai. C’était
un **invite fantôme** : grâce de présence encore affichée, **zéro socket live** chez le pair, et
le serveur envoyait quand même `call.invite`.

Enfin, après merge, `main` n’était plus exactement la tête de la PR (une leçon agents intercalée).
La CI de `main` a rejoué un FULL et a flake ailleurs (canvas qui intercepte un clic). Relancer
une troisième batterie produit pour « être sûr » aurait encore brûlé du temps mur — sans changer
la preuve déjà obtenue sur `74be1aa`.

## 2. La résolution

### Produit

- Fix **invite** : refuser `call.invite` si `hub.connsOf(peer).length === 0` (présent en grâce ≠
  joignable).
- E2E refuse durci (`waitCallIdle`) ; preuve locale phase7 ×3 + intégration « socket coupée mais
  encore en grâce ».
- Circuit Retours + noms Place (≥ 14 px) livrés avec **4.0.20**.

### Preuve (sans rejouer le FULL pour le plaisir)

| Étape | Résultat |
| --- | --- |
| `npm test` sur le SHA de fix | 1054 pass / 0 fail |
| FULL GitHub `74be1aa` | success |
| Release `--ci-verified` depuis la tête PR | tag `v4.0.20` |
| Deploy Release | [37254532148](https://github.com/FGDidou/JahMessenger/actions/runs/37254532148) success |
| `/readyz` | **4.0.20** |

### Règle (pour ne pas recommencer)

**Ciblé ≠ FULL.** Avant `ready` / push `ci:full` / claim merge, si le diff touche
`src|ui|scripts|migrations|test` :

1. `npm run lint` + **`npm test`** sur le SHA (pas « N/N ciblés »).
2. Tout nouveau test intégration/E2E → entrée **`AREAS`** dans la même PR.
3. Flake → preuve locale ×2–3 et cause produit, pas seulement timeout.

Écrit dans `.agents/RULES.md`, skills `verify` / `ship-gate` / `livrer`, `.agents/TESTS.md`,
journal `incident/preuve-ciblee-avant-full`.

## 3. Ce que l’habitant voit (le vrai gain)

Pendant qu’on se battait avec la CI, le lot livrait quand même quelque chose de tangible :

- **Nouveautés → Retours** : l’état de ce que tu as signalé, tant que l’onglet est ouvert.
- Place : écriteaux lisibles.
- Appels : plus d’invitation qui sonne chez quelqu’un qui n’a plus de socket.

La méthode ne compte que si la vague est sortie. Ici, elle l’est.