metalfrom.eu/README-CI.md
Nicolas Fryder 60074fb015
Some checks are pending
CI / javascript (push) Waiting to run
CI / python (push) Waiting to run
CI / mutation (push) Waiting to run
feat(qualité): outillage de test complet, CI locale, annulation réelle des runs
Le dépôt n'avait aucun test, aucun linter, aucune vérification de types.

Outillage
- ESLint 9 (flat config) sur api + les deux frontends, Ruff sur le Python
- tsc --checkJs sur l'API (pas de TypeScript, juste la vérification)
- Vitest : 401 tests JS ; pytest : 43 tests Python
- Tests de mutation (Stryker), deux profils : logique pure et API complète
- Hook pre-push `npm run check` (~17 s) — le déploiement Coolify est sur webhook,
  c'est donc la seule porte de qualité avant la mise en ligne
- Workflow Forgejo Actions prêt (inerte tant qu'aucun runner n'est enregistré)

Sécurité
- Injection SQL authentifiée dans resolve-conflict : `field` était interpolé
  dans le SET sans allowlist
- timingSafeEqual levait sur un jeton multi-octets (500 au lieu de 401)
- setErrorHandler écrasait tous les 4xx en 500
- .env.example : ADMIN_JWT_SECRET et ADMIN_SEED_* n'étaient documentés nulle part
  alors que leur absence casse toute connexion admin

Annulation réelle des crawl_run (migration 014)
- L'API posait status='error' sans que le crawler en sache rien : le process
  continuait, et son UPDATE final ne matchait plus (run réussi affiché en erreur)
- Protocole coopératif : drapeau cancel_requested lu à chaque lot, le crawler
  écrit lui-même status='cancelled'

Cohérence géographique (migration 014)
- Le trigger 013 supprimait les band_locations sans purger le point dénormalisé
- L'édition admin de lat/lon n'atteignait jamais band_locations : la carte
  ignorait la correction. Override step_order = -1, dans une transaction

Corrections
- limit/offset NaN → 500 au lieu de 400
- OPTIONS sans `return reply` (Fastify poursuivait le cycle de vie)
- listen() sans catch, cast ::text en dur sur les colonnes numériques
- /admin/api/logs ne renvoyait pas sa pagination
- a11y : sélecteur de langue annoncé comme liste vide (role=option manquant)

Nettoyage
- apps/web/quizz-site supprimé (sans rapport avec le projet)
- Code mort : openModal(), LANG_NAMES, double import, variables inutilisées
- .dockerignore ajoutés ; node_modules racine n'était pas gitignoré

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-18 10:05:40 +02:00

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
                  ↑ ~15 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 + Ruff + tsc + tous les tests ~15 s
npm test 401 tests JS 3,5 s
npm run test:py 43 tests Python 1 s
npm run lint / lint:py ESLint / Ruff 3 s / 1 s
npm run typecheck tsc --checkJs sur l'API 6 s
npm run test:mutation Mutation, logique pure (352 mutants) 34 s ~5 s
npm run test:mutation:full Mutation, toute l'API (1511 mutants) 5 min 13 s
npm run check:full check + audits de dépendances + mutation complète ~1 min

Où passe le temps, et pourquoi c'est acceptable

La commande de la boucle de développement, c'est npm run check : 17 s, et elle inclut déjà les 401 tests, ESLint, Ruff et tsc. C'est elle qui tourne cent fois par jour.

La mutation n'est pas une commande de boucle courte. Son coût se lit ainsi :

  • test:mutation (352 mutants) : 34 s à froid, ~5 s ensuite. Assez rapide pour être lancée avant chaque commit qui touche la validation ou l'auth.
  • test:mutation:full (1511 mutants) : 5 min une seule fois (clone neuf ou après avoir modifié les quatre fichiers mutés d'un coup). Après une édition normale d'un seul fichier : 13 s, mesuré.

Le fichier incrémental vit dans reports/, qui est gitignoré : un clone neuf paie donc les 5 minutes une fois. C'est assumé — c'est un audit, pas un test.

Ce qui n'a délibérément pas été fait pour aller plus vite : réduire encore le périmètre muté. Descendre sous ~1500 mutants sur l'API reviendrait à ne plus mesurer grand-chose, et un score de mutation flatteur obtenu en retirant les mutants gênants est pire qu'une absence de score.

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
Mutation stryker*.config.json 82 % (logique pure) / 65 % (API complète)

Performance : les décisions et leurs mesures

Le principe : aucun test ne monte de conteneur, ne compile, ni ne touche le réseau.

Quatre optimisations, toutes mesurées (les intuitions non vérifiées se sont révélées fausses au moins une fois) :

  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 ~12 000 exécutions pendant un run de mutation. Les tests partagent une app et reprogramment un pool via pool.reset() (test/helpers/testApp.js).

  2. Config Vitest dédiée à la mutation. Stryker recharge les fichiers de test à chaque mutant. En ne déclarant que ceux qui couvrent le code muté (et surtout pas le test a11y, qui initialise jsdom en ~2 s), le profil pur est passé de 1 min 23 à 34 s.

  3. Mutants StringLiteral exclus. Les handlers sont à ~80 % du SQL en template literals. Muter le contenu d'une chaîne SQL ne mesure rien. Les retirer a fait passer le profil complet de 7 min 50 à 5 min — et le score de 42 % à 65 %, parce que le bruit disparaissait du dénominateur.

  4. 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 vérifiées séparément dans rateLimit.test.js, pour que la paramétrisation ne crée pas d'angle mort.

Ce qui a été essayé et rejeté sur mesure : le pool Vitest threads avec isolate: false, réputé plus rapide, donne 18,8 s contre 3,5 s ici (la mise en place de jsdom est pénalisée). Le défaut (forks) est conservé.

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.

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 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.