metalfrom.eu/README-CI.md
Nicolas Fryder f14060720b
Some checks are pending
CI / javascript (push) Waiting to run
CI / python (push) Waiting to run
CI / mutation (push) Waiting to run
feat(admin): refonte du panneau par intention, vue Localisations, tests e2e
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>
2026-08-18 15:05:29 +02:00

8.2 KiB

Qualité, tests et CI/CD

Le modèle en une phrase

Coolify redéploie sur webhook à chaque push (dev → dev.metalfrom.eu, main → metalfrom.eu). Rien ne s'interpose. La porte de qualité est donc locale, dans le hook pre-push.

git push ──▶ [pre-push : npm run check + test:e2e] ──▶ Forgejo ──▶ webhook ──▶ Coolify redeploy
                  ↑ 6 s + 22 s, bloquant

Deux granularités volontaires : check (6 s) tourne pendant qu'on code, les parcours Playwright (22 s) seulement au moment de pousser. Mettre l'e2e dans check ferait passer la boucle de 6 à 22 s pour un gain marginal — la plupart des régressions tombent déjà dans les tests unitaires.

Installation (une fois par clone)

npm install
npm run hooks:install                    # active .githooks/pre-push
pip install -r requirements-dev.txt

Commandes et temps mesurés

Commande Ce que ça fait Froid Incrémental
npm run check La porte : ESLint + tsc + Ruff + 444 tests 6 s
npm run check:sequential Idem, en série (pour isoler un échec) 14 s
npm run test:mutation Mutation, logique pure (352 mutants) 33 s ~5 s
npm run test:mutation:full Mutation, toute l'API (1511 mutants) 3 min 40 12 s
npm run test:e2e 53 parcours Playwright (dashboard admin) 22 s
npm run test:e2e:install Récupère Chromium (clone neuf)
npm run check:full check + e2e + audits + mutation complète ~1 min
npm run check:clean Purge les caches ESLint / tsc

Où passe le temps

La commande de la boucle de développement, c'est npm run check : 6 s, tests compris. C'est elle qui tourne cent fois par jour, et elle est passée de 17 s à 6 s par trois mesures successives :

Levier Gain
Les 5 vérifications en parallèle au lieu de && (elles sont indépendantes, et l'enchaînement npm coûtait 7 démarrages) 14 s → 6 s
Cache ESLint (--cache) et compilation incrémentale tsc 16 s → 14 s
Chemin critique restant : vitest 4,9 s, dont ~2 s de mise en place jsdom

La mutation n'est pas une commande de boucle courte :

  • test:mutation (352 mutants) : 33 s à froid, ~5 s ensuite.
  • test:mutation:full (1511 mutants) : 3 min 40 une seule fois, puis 12 s après une édition. Le fichier incrémental vit dans reports/, qui est gitignoré : un clone neuf paie donc le run complet une fois.

Ce qui a été essayé et rejeté sur mesure (les intuitions se sont trompées plus d'une fois — tout est re-mesurable) :

Piste Résultat
Découper la mutation en 5 processus Stryker parallèles 262 s contre 235 s : chaque processus repaie son bac à sable et son dry-run. Retiré.
Monter concurrency de 6 à 16 Aucun effet : ~8,5 mutants/s dans tous les cas. Le débit est limité par l'orchestration mono-processus de Stryker, pas par le CPU.
Séparer les tests API / frontend en deux processus vitest Aucun gain : 4,9 s dans les deux cas, jsdom domine.
Pool vitest threads sur la suite complète 18,8 s contre 3,5 s (jsdom pénalisé). Défaut conservé.
Pool vitest threads sur la config de mutation (sans jsdom) Overhead d'amorçage 12 844 ms → 1 592 ms. Adopté.

Ce qui n'a délibérément pas été fait : réduire le périmètre muté. Descendre sous ~1500 mutants sur l'API reviendrait à ne plus mesurer grand-chose, et un score flatteur obtenu en retirant les mutants gênants est pire que pas de score. Le plancher de ~3 min à froid est celui de Stryker sur 1511 mutants, assumé.

Ce que couvrent les tests

Batterie Approche
Unitaires apps/api/test/validate.test.js Validation pure : bbox, limit/offset, coordonnées, années, recherche
Routes / intégration publicRoutes*.test.js, adminRoutes*.test.js fastify.inject() + faux pool. Gating d'auth, allowlists de colonnes, forme du SQL, transactions
Sécurité adminAuth.test.js, rateLimit.test.js JWT forgé, alg:none, expiration, anti-énumération, verrouillage de compte, plafonds de débit par IP et par jeton
Annulation cancellation.test.js + apps/crawler/tests/test_cancellation.py Les deux moitiés du protocole coopératif
Accessibilité apps/web/test/a11y.test.js axe-core + jsdom sur le HTML statique
Frontend apps/web/test/pure.test.js Filtrage, tri, échappement HTML
Méta apps/api/test/harness.test.js Vérifie le faux pool lui-même
Python apps/geocoder/tests/, apps/crawler/tests/ Parseur de localisations, annulation
Parcours (e2e) apps/admin/test/e2e/admin.spec.js Playwright sur la VRAIE app Fastify + faux pool : connexion, recherche, édition, déblocage d'une localisation, annulation d'un traitement
a11y des vues rendues apps/admin/test/e2e/a11y.spec.js axe-core dans un vrai navigateur — contraste inclus, ce que jsdom ne sait pas calculer
Mutation stryker*.config.json 82 % (logique pure) / 65 % (API complète)

Principe des tests

Aucun test ne monte de conteneur, ne compile, ni ne touche le réseau.

Deux choix structurants côté API :

  1. Une app Fastify par fichier de test, pas par cas. buildServer() coûte 14 ms ; à raison d'une construction par it(), ces 14 ms étaient multipliés par des milliers d'exécutions en mutation. Les tests partagent une app et reprogramment un faux pool via pool.reset() (test/helpers/testApp.js).

  2. Plafonds de débit paramétrables. Conséquence du point 1 : une app partagée accumule l'état du limiteur. Les valeurs réelles de production sont donc vérifiées séparément dans rateLimit.test.js, pour que cette paramétrisation ne crée pas d'angle mort sur une protection de sécurité.

Pourquoi deux profils de mutation

La mutation prouve qu'un test échoue quand le code casse — la couverture ne dit que « cette ligne a été exécutée ».

  • test:mutation (validate.js, adminAuth.js) : 82 %. Logique pure, score interprétable, assez rapide pour être lancé souvent.
  • test:mutation:full (+ adminRoutes.js, app.js) : 65 %. Les handlers de route plafonnent structurellement plus bas — beaucoup de mutants portent sur des messages de log ou des branches catch dont l'observable exact n'a pas d'importance. Utile en audit, pas en boucle courte.

Les seuils d'échec (70 % et 55 %) sont des cliquets anti-régression, pas des objectifs.

Les parcours e2e

apps/admin/test/e2e/server.mjs reproduit la topologie de production : un serveur HTTP sert les fichiers statiques et proxifie /admin/* vers la VRAIE application Fastify (comme apps/admin/nginx.conf), avec un faux pool à la place de Postgres.

Sont donc réellement exercés : le routage Fastify, les cookies, le JWT, bcrypt, la validation des entrées et la génération SQL. Ne le sont pas : la validité du SQL pour Postgres et le comportement de PostGIS — affaire de tests d'intégration, pas de tests d'interface.

Démarrage ~1 s, aucun conteneur. Une session est enregistrée une fois (auth.setup.js) et partagée : sans ça, chacun des 53 scénarios repayait un aller-retour de connexion complet. 4 workers est l'optimum mesuré (8 et 12 sont plus lents — contention au démarrage des navigateurs).

Ce qui n'est pas couvert

  • Règles a11y exigeant un moteur de rendu sur le site public : le test jsdom ne calcule pas les styles. Le dashboard admin, lui, est couvert par axe dans un vrai navigateur.
  • migrate.js : s'exécute au démarrage du conteneur et appelle process.exit.
  • Crawler et workers de géocodage : seuls le parseur et l'annulation sont couverts, le reste est de l'I/O réseau et base.
  • apps/web/site/pure.js est testé mais exclu de la mutation : il est chargé via fs + vm (comme le fait le navigateur), donc l'instrumentation Stryker ne l'atteindrait pas et afficherait un score faussement parfait.

Forgejo Actions

.forgejo/workflows/ci.yml existe mais ne tourne pas : aucun runner act_runner n'est enregistré sur le VPS. Le fichier est prêt si tu en ajoutes un. Il est volontairement informatif — il ne bloque pas le déploiement.