fix(admin): ferme le serveur socket.io en fin de suite de test monitoring
All checks were successful
CI / build (push) Successful in 2m18s
CI / lint (push) Successful in 2m21s
CI / intent-service-test (push) Successful in 3m55s
CI / e2e (push) Successful in 7m45s
CI / test (push) Successful in 14m30s

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>
This commit is contained in:
Nicolas 2026-09-05 19:56:45 +02:00
parent 13d92f8fa8
commit 23be1e4d04

View file

@ -51,6 +51,15 @@ function waitForEvent<T>(socket: ClientSocket, event: string, timeoutMs = 1500):
describe("Admin monitoring — WebSocket push", () => { describe("Admin monitoring — WebSocket push", () => {
let baseUrl: string; let baseUrl: string;
let broadcast: MonitoringBroadcastService; 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[] = []; const clientSockets: ClientSocket[] = [];
// A real listening server, not just createApp()'s bare Express instance // 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. // free one, so this suite never collides with anything else listening.
before(async () => { before(async () => {
const httpServer = createServer().httpServer; const httpServer = createServer().httpServer;
const io = new SocketIoServer(httpServer, { cors: { origin: "*" } }); io = new SocketIoServer(httpServer, { cors: { origin: "*" } });
broadcast = new MonitoringBroadcastService(io); broadcast = new MonitoringBroadcastService(io);
await new Promise<void>((resolve) => httpServer.listen(0, resolve)); await new Promise<void>((resolve) => httpServer.listen(0, resolve));
const { port } = httpServer.address() as AddressInfo; const { port } = httpServer.address() as AddressInfo;
@ -70,6 +79,12 @@ describe("Admin monitoring — WebSocket push", () => {
after(async () => { after(async () => {
broadcast.stop(); 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<void>((resolve, reject) => {
io.close((err) => (err ? reject(err) : resolve()));
});
await prisma.$disconnect(); await prisma.$disconnect();
}); });