From 0dde66e2eb6b72758d352aa58691ddd83d6493c8 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Sun, 31 May 2026 14:38:56 +0300 Subject: [PATCH] =?UTF-8?q?fix(web-test):=20=D0=BD=D0=B5=20=D0=B4=D0=B5?= =?UTF-8?q?=D1=80=D0=B6=D0=B0=D1=82=D1=8C=20event=20loop=20=D0=B2=D0=B8?= =?UTF-8?q?=D1=81=D1=8F=D1=87=D0=B8=D0=BC=20=D1=82=D0=B0=D0=B9=D0=BC=D0=B5?= =?UTF-8?q?=D1=80=D0=BE=D0=BC=20=D1=82=D0=B0=D0=B9=D0=BC=D0=B0=D1=83=D1=82?= =?UTF-8?q?=D0=B0=20=D0=BF=D0=BE=D1=81=D0=BB=D0=B5=20=D1=82=D0=B5=D1=81?= =?UTF-8?q?=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Guard-таймаут теста собирался через Promise.race([t.fn, setTimeout(reject)]), но setTimeout после победы теста не очищался. На успешном пути раннер не зовёт process.exit(), поэтому node не мог завершиться, пока сторож не догорит — до `timeout` мс простоя после последнего теста (на стенде с timeout=60s это ~45с зависания уже после закрытия браузера). Оборачиваю гонку в try/finally с clearTimeout. Вердикт теста и таймаут-защита не меняются: clearTimeout срабатывает только после завершения гонки (тест добежал или сторож сработал), по уже сработавшему таймеру это no-op. Замер на одиночном тесте: 64.5s → 18.9s wall-clock. Co-Authored-By: Claude Opus 4.8 --- .../web-test/scripts/cli/commands/test.mjs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/.claude/skills/web-test/scripts/cli/commands/test.mjs b/.claude/skills/web-test/scripts/cli/commands/test.mjs index e44dc089..ff2e1f8d 100644 --- a/.claude/skills/web-test/scripts/cli/commands/test.mjs +++ b/.claude/skills/web-test/scripts/cli/commands/test.mjs @@ -1,4 +1,4 @@ -// web-test cli/commands/test v1.1 — regression test runner +// web-test cli/commands/test v1.2 — regression test runner // Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import { existsSync, writeFileSync, mkdirSync } from 'fs'; import { resolve, dirname, basename, relative } from 'path'; @@ -318,10 +318,18 @@ export async function cmdTest(rawArgs) { if (hooks.beforeEach) await hooks.beforeEach(ctx); if (t.setup) await t.setup(ctx); - await Promise.race([ - t.fn(ctx, t.param), - new Promise((_, reject) => setTimeout(() => reject(new Error(`Timeout (${t.timeout}ms)`)), t.timeout)), - ]); + let timeoutTimer; + try { + await Promise.race([ + t.fn(ctx, t.param), + new Promise((_, reject) => { timeoutTimer = setTimeout(() => reject(new Error(`Timeout (${t.timeout}ms)`)), t.timeout); }), + ]); + } finally { + // Clear the guard timer — otherwise it stays armed in the event loop and, + // since the success path never calls process.exit(), node can't exit until + // it fires (up to `timeout` ms after the last test finished). + clearTimeout(timeoutTimer); + } if (t.teardown) try { await t.teardown(ctx); } catch {} ctx.testResult = { status: 'passed', duration: elapsed(t0), attempts: attempt, error: null, steps };