1. Suite d'intégration SQL (npm run test:integration:full) Ferme le dernier angle mort : la SÉMANTIQUE du SQL. Le parseur de grammaire ne voyait pas un nom de colonne inexistant, un type incompatible ou une fonction PostGIS mal appelée — soit exactement la classe de bugs qui n'apparaissait qu'en production. Technique : chaque requête émise par l'application passe par `PREPARE`. Postgres l'analyse et la planifie entièrement — colonnes, types, opérateurs jsonb, fonctions PostGIS — SANS l'exécuter ni nécessiter de données. Rapide, et ça couvre aussi les requêtes que le parseur JS ne sait pas lire : jsonb_build_object, `jsonb - text[]`, `raw ? 'clé'`, make_interval, l'opérateur spatial && et l'agrégation en grille des clusters. Les migrations réelles sont appliquées dans l'ordre réel : une migration invalide échoue ici, plus au redémarrage du conteneur en production. 54 tests. Exige Docker, donc HORS de `check` et du hook pre-push — la boucle de développement reste à 6 s. Base jetable en tmpfs (fsync off). Deux témoins vérifient que le détecteur n'est pas inopérant : une colonne inexistante et un type incompatible doivent être rejetés. 2. Les boutons de crawl ne mentent plus La production ne déploie pas le service crawler (docker-compose.yml ne le contient pas). Les boutons « Crawl incrémental », « Enrichir » et « Crawl complet » y créaient des demandes que personne ne consommait, avec un libellé promettant une exécution « sous ~1 min ». Ils s'appuient désormais sur le battement de cœur : pas de crawler vivant, boutons désactivés et raison affichée. « Alimenter le géocodage » reste actif, puisqu'il est traité par le geocoder. Je n'ai PAS ajouté le crawler au compose de production : ce serait déclencher du crawl depuis la prod, décision qui n'est pas la mienne. Régression évitée au passage : re-rendre le bloc de traitements effaçait le message de retour (« Demande #77 enregistrée »), puisque loadPilotage() est rappelé après un clic réussi. L'état des boutons est donc mis à jour sans reconstruire le DOM. Détecté par un test existant. Correctif : la suite d'intégration était happée par le glob de vitest.config.js et allongeait `check` de 6 à 22 s en exigeant Docker. Tests : 580 JS + 54 intégration, 63 Python, 89 Playwright Co-Authored-By: Claude <noreply@anthropic.com>
10 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 + 620 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 (2159 mutants) | ~5 min | 8 s |
npm run test:e2e |
85 parcours Playwright (dashboard admin) | 26 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 test:integration:full |
SQL validé par un vrai PostgreSQL+PostGIS (exige Docker) | ~30 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 |
| 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) / 68 % (API complète) |
| Syntaxe SQL | apps/api/test/sqlSyntax.test.js |
Chaque requête réellement émise est parsée avec la grammaire PostgreSQL — sans conteneur |
| Santé des services | apps/crawler/tests/test_health.py |
Sondes du crawler et du geocoder : base, dépendances externes, fraîcheur des données |
| Intégration SQL | apps/api/test/integration/ |
Migrations réelles + PREPARE de chaque requête sur PostgreSQL+PostGIS : colonnes, types, PostGIS. Exige Docker, hors de check |
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) : 68 %. 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 que les survivants ont révélé
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. Les tests écrits pour combler ce
trou ont mis au jour un bug réel : genre n'était pas tronqué, contrairement
aux autres champs de recherche.
Le score est passé de 64 % à 68 % en écrivant des tests, pas en retirant des
mutants. reports/mutation-full.json contient le détail exploitable ; le
script de tri utilisé est décrit dans l'historique Git.
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 appelleprocess.exit.- Crawler et workers de géocodage : le parseur, l'annulation et les contrôles de santé sont couverts ; le reste est de l'I/O réseau et base.
- La sémantique SQL est désormais couverte par
npm run test:integration:full, qui applique les migrations réelles puis faitPREPAREsur chaque requête : Postgres valide colonnes, types et fonctions PostGIS sans rien exécuter ni nécessiter de données. Elle exige Docker et reste donc hors decheck. - Ce qui n'est toujours pas couvert : le comportement à l'exécution sur des données réelles (résultats de PostGIS, performance des index).
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.