From 23be1e4d045da4e50cdafdbece817e852ef90ff0 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Sat, 5 Sep 2026 19:56:45 +0200 Subject: [PATCH] fix(admin): ferme le serveur socket.io en fin de suite de test monitoring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- apps/api/test/admin-monitoring-socket.test.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/apps/api/test/admin-monitoring-socket.test.ts b/apps/api/test/admin-monitoring-socket.test.ts index 70e7b44..a322cbc 100644 --- a/apps/api/test/admin-monitoring-socket.test.ts +++ b/apps/api/test/admin-monitoring-socket.test.ts @@ -51,6 +51,15 @@ function waitForEvent(socket: ClientSocket, event: string, timeoutMs = 1500): describe("Admin monitoring — WebSocket push", () => { let baseUrl: string; let broadcast: MonitoringBroadcastService; + // Hoisted to suite scope (not a local in `before`) so `after` can shut + // it down. `broadcast.stop()` alone only clears the probe interval — the + // socket.io server keeps engine.io's ping timers and the listening + // http.Server (port 0) alive, and Mocha runs without `--exit` + // (`.mocharc.json`), so that one leaked handle stops the whole process + // from ever draining once the last test passes. That is exactly what + // stalled CI: the `test` job printed "N passing" and then hung for + // hours until the runner was killed. + let io: SocketIoServer; const clientSockets: ClientSocket[] = []; // A real listening server, not just createApp()'s bare Express instance @@ -60,7 +69,7 @@ describe("Admin monitoring — WebSocket push", () => { // free one, so this suite never collides with anything else listening. before(async () => { const httpServer = createServer().httpServer; - const io = new SocketIoServer(httpServer, { cors: { origin: "*" } }); + io = new SocketIoServer(httpServer, { cors: { origin: "*" } }); broadcast = new MonitoringBroadcastService(io); await new Promise((resolve) => httpServer.listen(0, resolve)); const { port } = httpServer.address() as AddressInfo; @@ -70,6 +79,12 @@ describe("Admin monitoring — WebSocket push", () => { after(async () => { broadcast.stop(); + // Disconnects any socket still open, stops engine.io's timers, and + // closes the http.Server socket.io was attached to — without this the + // suite never lets Node's event loop drain (see the `io` field's note). + await new Promise((resolve, reject) => { + io.close((err) => (err ? reject(err) : resolve())); + }); await prisma.$disconnect(); });