From ab65d8f664a42ade8ad7435de5e0f13cae39c2d7 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Tue, 21 Jul 2026 20:10:09 -0400 Subject: [PATCH] fix(@angular/build): add bounded timeout to vitest executor disposal When disposing the VitestExecutor in non-watch runs, `vitest.close()` can hang if a Vitest worker pool process or thread fails to terminate cleanly. Calling `process.exit()` in a builder runner is undesirable as it forcibly kills the host Node.js process and aborts any downstream Architect tasks or reporters. This change wraps `this.vitest.close()` in a `Promise.race` with an unref'd `setTimeout`. If `close()` stalls beyond 10 seconds, a warning is logged and disposal finishes cleanly, allowing host process execution and downstream logic to proceed. Fixes #32832 --- .../unit-test/runners/vitest/executor.ts | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/packages/angular/build/src/builders/unit-test/runners/vitest/executor.ts b/packages/angular/build/src/builders/unit-test/runners/vitest/executor.ts index 9d3760322ecb..ac2bfe20fa38 100644 --- a/packages/angular/build/src/builders/unit-test/runners/vitest/executor.ts +++ b/packages/angular/build/src/builders/unit-test/runners/vitest/executor.ts @@ -9,6 +9,7 @@ import type { BuilderContext, BuilderOutput } from '@angular-devkit/architect'; import assert from 'node:assert'; import path from 'node:path'; +import { setTimeout } from 'node:timers/promises'; import type * as Vite from 'vite' with { 'resolution-mode': 'import', }; @@ -207,8 +208,35 @@ export class VitestExecutor implements TestExecutor { } async [Symbol.asyncDispose](): Promise { + if (!this.vitest) { + return; + } + this.debugLog(DebugLogLevel.Info, 'Disposing VitestExecutor: Closing Vitest instance.'); - await this.vitest?.close(); + + const controller = new AbortController(); + const timeoutMs = 10_000; + + try { + await Promise.race([ + this.vitest.close(), + setTimeout(timeoutMs, undefined, { signal: controller.signal, ref: false }) + .then(() => { + this.logger.warn( + `Vitest instance failed to close cleanly within ${timeoutMs}ms. Continuing teardown...`, + ); + }) + .catch(() => { + // Suppress AbortError triggered by controller.abort() when close() resolves first + }), + ]); + } catch (error: unknown) { + assertIsError(error); + this.logger.error(`An error occurred while closing Vitest instance: ${error.message}`); + } finally { + controller.abort(); + } + this.debugLog(DebugLogLevel.Info, 'Vitest instance closed.'); }