Passe de performance guidée par la mesure, pas par l'intuition. Porte de qualité (`npm run check`) — la commande qui tourne des dizaines de fois par jour : - Les 5 vérifications sont indépendantes : exécutées en parallèle via scripts/check.mjs au lieu d'un enchaînement `&&` qui imposait la somme des durées ET 7 démarrages npm imbriqués. 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. Tests de mutation : - Config Vitest dédiée en pool `threads` sans ré-isolation. Mesuré sur le dry-run Stryker : overhead d'amorçage 12 844 ms → 1 592 ms, soit ~90 % du cycle par mutant. 5 min 02 → 3 min 40 - Le mode incrémental ramène le profil complet à 12 s après une édition. Pistes mesurées puis REJETÉES (documentées dans README-CI.md pour éviter qu'on les retente) : - 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. - Monter `concurrency` de 6 à 16 : aucun effet (~8,5 mutants/s partout), le débit est borné par l'orchestration mono-processus de Stryker. - Séparer vitest API/frontend en deux processus : aucun gain, jsdom domine. - Pool `threads` sur la suite complète : 18,8 s contre 3,5 s. Le bon réglage dépend du jeu de fichiers (jsdom ou non) — d'où deux configs distinctes. Le périmètre muté est inchangé (1511 mutants) : un score obtenu en retirant les mutants gênants vaudrait moins que pas de score. Corrections au passage : - scripts/*.mjs n'était couvert par aucune section ESLint - Les deux suites pytest ont chacune un paquet `src` : réunies dans un seul processus, le premier importé masquait l'autre. Séparées (et parallèles). Co-Authored-By: Claude <noreply@anthropic.com>
6.6 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 ──▶ [hook pre-push : npm run check] ──▶ Forgejo ──▶ webhook ──▶ Coolify redeploy
↑ 6 s, bloquant
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 check:full |
check + audits de dépendances + mutation complète |
— | ~40 s |
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 dansreports/, 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 | Où | 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 |
| 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 :
-
Une app Fastify par fichier de test, pas par cas.
buildServer()coûte 14 ms ; à raison d'une construction parit(), ces 14 ms étaient multipliés par des milliers d'exécutions en mutation. Les tests partagent une app et reprogramment un faux pool viapool.reset()(test/helpers/testApp.js). -
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 branchescatchdont 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.
Ce qui n'est pas couvert
- Règles a11y exigeant un moteur de rendu (contraste, cibles tactiles) : jsdom ne calcule pas de styles. À compléter par un audit Lighthouse manuel.
migrate.js: s'exécute au démarrage du conteneur et appelleprocess.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.jsest testé mais exclu de la mutation : il est chargé viafs+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.