Le hook `after` de admin-monitoring-socket.test.ts appelait seulement
`broadcast.stop()` (qui ne fait que couper l'intervalle de sonde) et
`prisma.$disconnect()`, mais ne fermait jamais le serveur socket.io ni le
`http.Server` en écoute sur le port 0. socket.io garde alors vivants les
timers ping d'engine.io et la socket d'écoute ; comme Mocha tourne sans
`--exit` (.mocharc.json), ce seul handle en fuite empêche le process de se
terminer une fois le dernier test passé.
C'est ce qui bloquait le job `test` en CI : « N passing » affiché, puis
plusieurs heures d'attente jusqu'au kill du runner (runs #80/#81).
Hisse `io` au scope de la suite et le ferme dans `after` via `io.close()`
— ce qui déconnecte les sockets restantes, stoppe les timers d'engine.io
et ferme le `http.Server` attaché.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Le tableau de bord /admin/monitoring fonctionnait en polling côté client
(15 s) déclenchant une sonde active côté serveur à chaque appel — deux
onglets admin ouverts = deux séries de sondes redondantes.
Inverse le mécanisme : le serveur sonde une fois par minute et pousse le
résultat aux clients connectés via WebSocket (socket.io, room
"monitoring"). Décisions actées avec l'utilisateur : l'endpoint REST
disparaît entièrement (plus de repli, 100% WebSocket) ; la sonde tourne
en continu dès le démarrage, sans lien avec la présence d'un admin
connecté (le plus simple des deux, coût des sondes négligeable).
Backend :
- ExpressServer (packages/express-tools) expose désormais son
http.Server sous-jacent (créé explicitement, plus dans .listen()) —
socket.io s'attache au serveur HTTP lui-même, pas à Express/un port.
- MonitoringBroadcastService (nouveau) : sonde immédiate au démarrage
puis toutes les 60 s (réutilise getMonitoring() tel quel, aucune
duplication), diffuse à la room "monitoring", rejoue le dernier
résultat en cache à tout nouvel admin qui rejoint (pas d'attente
jusqu'à une minute pour un onglet fraîchement ouvert).
- requireAdminSocket (nouveau) : middleware de handshake socket.io,
équivalent WebSocket de requireAdmin — même vérification (cookie +
JWT + tokenVersion), factorisée dans resolveAdminFromToken
(require-admin.ts) puisque les deux ne diffèrent que par où ils lisent
le cookie brut (un handshake socket.io ne passe pas par
cookie-parser).
- GET /admin/api/monitoring supprimée (route + service de test associés).
- server.ts (pas app.ts) instancie socket.io — les tests supertest n'ont
pas besoin d'un serveur WebSocket.
Frontend :
- MonitoringSocketClient (nouveau, api/monitoring-socket.ts) : wrapper
socket.io-client, interface MonitoringSocketClientLike volontairement
petite et injectable dans MonitoringPage (prop optionnelle) pour rester
testable sans serveur réel.
- MonitoringPage : data et connectionState séparés — une coupure de
connexion après un premier chargement affiche un badge "reconnexion en
cours" sans jamais effacer le dernier board connu (repose sur la
reconnexion automatique de socket.io-client). Seul le tout premier
chargement affiche l'état plein-écran chargement/erreur.
- adminApiClient.getMonitoring() supprimée (plus utilisée).
Tests :
- Nouveau apps/api/test/admin-monitoring-socket.test.ts : vraie connexion
socket.io-client contre un serveur réel démarré dans le test (port 0),
authentification avec un admin seedé + cookie signé, vérifie la
réception de monitoring:update et le rejet sans cookie admin valide.
Les tests heartbeat de l'ancien admin-monitoring.test.ts (route
/internal/tech-steps/heartbeat, sans rapport avec ce changement) sont
déplacés vers tech-step-worker.routes.test.ts, leur router d'origine.
- Nouveau cypress/component/MonitoringPage.cy.tsx : monte la page avec un
client factice, vérifie rendu des cartes, badge de reconnexion sans
perte du board, états chargement/erreur — pas de test e2e Gherkin
contre un vrai WebSocket (aucun mécanisme pour ça dans ce repo,
fragile ; le test Mocha ci-dessus est la preuve bout-en-bout serveur).
- apps/web/cypress/e2e/admin-monitoring.cy.ts (REST) supprimé, remplacé
par le test composant ci-dessus.
Vérification : pnpm lint clean, build complet du monorepo clean, suite
Cypress e2e complète 27/27 fichiers (114/114 tests), suite composant
complète 5/5 fichiers (54/54 tests, dont les 4 nouveaux). Suite Mocha
apps/api non exécutable cette session (Postgres/Docker injoignable,
comme pour #33) — le nouveau test socket.io n'a donc pas pu être
run-vérifié, seulement relu attentivement et vérifié par le lint.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>