La liste des mutants survivants est la carte de ce qui n'est pas vérifié. Une
passe dessus a montré que TOUS les filtres de /admin/api/bands étaient non
testés (sept mutants survivants par drapeau booléen) — précisément ceux dont
dépendent les filtres rapides du dashboard. Ils pouvaient être inopérants sans
que rien ne le signale.
Bugs trouvés et corrigés
1. crawler_pending détruit silencieusement (backend)
Le PATCH reconstruisait crawler_pending avec jsonb_build_object() sur les
seuls champs édités. Il gardait donc le conflit qu'on venait de trancher ET
supprimait ceux des champs non touchés : éditer le nom d'un groupe effaçait
ses conflits de genre, de pays et de localisation, sans trace.
Corrigé en suivant la convention de /resolve-conflict : retrait des clés
arbitrées (`crawler_pending - ARRAY[...]`).
2. Filtre genre non borné (backend)
q, location_q et themes_q tronquaient à 100 caractères ; genre non. Un motif
ILIKE de taille arbitraire partait vers Postgres, qui ne peut pas l'indexer.
country et status sont bornés au passage, par cohérence.
3. Chips inutilisables après une alerte (frontend)
Le paramètre de statut de l'URL était relu à CHAQUE rendu. Arrivé depuis une
alerte du Pilotage, cliquer un autre chip n'avait aucun effet : le filtre
revenait aussitôt à celui de l'URL. Le paramètre est désormais consommé
(replaceState, qui ne déclenche pas hashchange).
4. Minuteur d'auto-refresh orphelin (frontend)
Le minuteur était armé APRÈS le chargement des données. Quitter la vue
pendant celui-ci le laissait s'armer après le clearTimer() du routeur : le
Pilotage continuait d'interroger quatre endpoints toutes les 15 s depuis un
autre onglet, indéfiniment. Résolu par un jeton de rendu.
Commentaire dangereux corrigé
La saisie manuelle de coordonnées écrit geocode_status='done'. Le commentaire
annonçait 'manual', ce qui aurait conduit à une « correction » aux conséquences
invisibles : /api/clusters ne retient que ('done','country_only') — le point
n'apparaîtrait pas sur la carte — et /locations/reset-llm remet en file
('llm_needed','manual') — le bouton effacerait la saisie. Invariant verrouillé
par un test.
Test instable supprimé
Les interceptions Playwright lisaient la réponse après une possible navigation
(« Response has been disposed ») : un échec sur trois exécutions, sans rapport
avec ce qui était vérifié. Un test instable finit par être ignoré, ce qui est
pire qu'un test absent. Helper patchJson() ; stable sur 4 exécutions.
Tests ajoutés : 529 JS (+78), 43 Python, 82 Playwright (+27)
- bandFilters.test.js : les 3 drapeaux × 2 polarités × présence/absence,
bornes de q, tri, pagination, alignement des paramètres liés
- conflicts.test.js : effet du PATCH sur crawler_pending, casts par type,
allowlist, cohérence avec resolve-conflict
- tools.spec.js : tri, pagination, arbitrage de conflit, annulations,
filtres LLM, cycle de vie des vues
Les trois tests de régression ont été vérifiés NON VACUOUS : chaque bug
réintroduit les fait échouer.
Mutation : 64,00 % -> 68,29 % (seuil 55), obtenu en écrivant des tests et non
en réduisant le périmètre muté.
Co-Authored-By: Claude <noreply@anthropic.com>
Le panneau était découpé par table SQL, pas par question que se pose l'admin.
Conséquence : « je constate ici, j'agis dans un autre onglet », et aucun moyen
de voir ce qui coince réellement.
Redécoupage — un onglet = une question
- Pilotage : est-ce que ça tourne ? (fusionne l'ancien « Actions »)
- Groupes : trouver et corriger
- Localisations : qu'est-ce qui coince dans le géocodage ? ← NOUVEAU
- Journal : que s'est-il passé ?
- LLM & coûts : combien ça coûte ?
Pilotage : chaque chiffre problématique porte son action
- Bandeau de santé (groupes, géocodage, file, bloqués, coût) coloré par état
- Alertes actionnables : « 12 localisations en erreur » + [Examiner] +
[Tout remettre en file], au lieu d'un mur de boutons dans un autre onglet
- Déclencheurs de traitements inline, opérations destructives repliées
- L'ancien bloc « Éléments de genre » (des centaines de mots-clés jamais
consultés) est retiré
Localisations : la vue qui manquait totalement
L'admin ne disposait que d'actions EN MASSE sur band_locations et d'aucun moyen
de voir CE qui échouait. Débloquer un seul lieu supposait de relancer des
milliers d'appels Geoapify/Groq facturés.
- GET /admin/api/locations liste filtrable (statut, lieu, groupe, pays)
- POST /admin/api/locations/:id/requeue relance UNE ligne
- PATCH /admin/api/locations/:id saisie manuelle des coordonnées
- Diagnostic lisible sans clic : lieu brut, statut, essais, erreur réelle
Groupes : recherche d'abord
Une barre de recherche et des filtres rapides en chips remplacent les 8 champs
texte ; les filtres avancés sont repliés.
Accessibilité
- Lignes de tableau activables au clavier (role=button, tabindex, Entrée)
- Modales : Échap ferme, focus piégé, focus rendu à l'élément d'origine
- Contraste : --err (#c61a1a) échouait WCAG AA en texte sur fond sombre (3,4:1).
Les messages d'erreur étaient difficiles à lire. Ajout de --err-text /
--ok-text (~6,5:1) pour les usages en couleur de texte.
Détecté par les nouveaux tests axe sur les vues rendues.
- Statuts affichés en français au lieu des valeurs brutes de la base
Tests
- 36 tests API sur les trois nouveaux endpoints (allowlist de statuts,
bornes des coordonnées, audit, 404)
- 53 parcours Playwright sur la VRAIE app Fastify + faux pool, dans une
topologie identique à la production (statique servi + /admin/* proxifié)
- Tests axe sur les vues RENDUES : le test jsdom existant ne voyait que la
coquille vide du dashboard, tout étant construit en JavaScript
- e2e branché sur le hook pre-push, pas sur `check` : la boucle de
développement reste à 6 s, le push coûte 28 s
Correction trouvée par les tests
Le routeur ne séparait pas la query string du nom de vue :
« #/locations?status=error » ne correspondait à aucun alias et retombait sur
Pilotage — les liens des alertes ne fonctionnaient pas.
Co-Authored-By: Claude <noreply@anthropic.com>