From b5c61998dd2670fc209ca82cda02ce4c04ccb0ec Mon Sep 17 00:00:00 2001 From: qwen-code-dev-bot Date: Tue, 25 Aug 2026 21:14:55 +0000 Subject: [PATCH 1/3] fix(ci): yield the event loop between script tests to avoid vitest RPC timeouts (#10037) The v0.22.1 release quality job exited 1 on `npm run test:scripts` with every test green. vitest's worker->main `onTaskUpdate` RPC has a fixed 60s timeout; the synchronous spawnSync-driven script suites keep a forked worker's event loop blocked for an entire file (~66s on the heaviest suite), so the queued RPC response is never processed before the timer fires, surfacing as an unhandled `[vitest-worker]: Timeout calling "onTaskUpdate"` error. Linux keeps unhandled errors fatal (the scripts vitest config only exempts non-Linux since #9728), so the release died. Add a global per-test event-loop yield to the scripts test setup. The timer is captured at setup load so `vi.useFakeTimers()` inside a test cannot intercept the yield. Any continuous stall is now bounded by a single test, so RPC responses drain long before the 60s deadline. Real test failures stay fatal on every platform; the Linux unhandled-error signal is untouched. --- scripts/tests/test-setup.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/scripts/tests/test-setup.ts b/scripts/tests/test-setup.ts index 5a769fc6cf1..f8eb1b09d0c 100644 --- a/scripts/tests/test-setup.ts +++ b/scripts/tests/test-setup.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { vi } from 'vitest'; +import { beforeEach, vi } from 'vitest'; vi.mock('fs', async () => { const actual = await vi.importActual('fs'); @@ -18,3 +18,12 @@ vi.mock('fs', async () => { }, }; }); + +// Captured before any test can install fake timers, so this yield always +// uses the real timer. Suites made of synchronous spawnSync tests can keep +// a worker's event loop blocked for the whole file (>60s), which makes +// vitest's fixed 60s worker->main `onTaskUpdate` RPC timeout fire and the +// run exit 1 with every test green. Yielding between tests releases the +// loop so the RPC response is always processed in time. +const realSetTimeout = setTimeout; +beforeEach(() => new Promise((resolve) => realSetTimeout(resolve, 0))); From 17ba8c6101ab3572147626f746bb2a3240205ec3 Mon Sep 17 00:00:00 2001 From: qwen-code-dev-bot Date: Tue, 25 Aug 2026 23:57:24 +0000 Subject: [PATCH 2/3] fix(ci): state the actual yield invariant in the script test setup comment (#10037) --- scripts/tests/test-setup.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/scripts/tests/test-setup.ts b/scripts/tests/test-setup.ts index f8eb1b09d0c..659d4c5b302 100644 --- a/scripts/tests/test-setup.ts +++ b/scripts/tests/test-setup.ts @@ -23,7 +23,10 @@ vi.mock('fs', async () => { // uses the real timer. Suites made of synchronous spawnSync tests can keep // a worker's event loop blocked for the whole file (>60s), which makes // vitest's fixed 60s worker->main `onTaskUpdate` RPC timeout fire and the -// run exit 1 with every test green. Yielding between tests releases the -// loop so the RPC response is always processed in time. +// run exit 1 with every test green. Yielding between tests bounds any +// continuous stall to a single test, so the RPC response drains well before +// the deadline. (A single test, beforeAll, or module-level block stalling +// for 60s would still trip it: testTimeout cannot interrupt synchronous +// bodies.) const realSetTimeout = setTimeout; beforeEach(() => new Promise((resolve) => realSetTimeout(resolve, 0))); From 9ad92bd90eaf543a65c126a0f0b6fad7b2848bb3 Mon Sep 17 00:00:00 2001 From: qwen-code-dev-bot Date: Wed, 26 Aug 2026 01:15:47 +0000 Subject: [PATCH 3/3] test(ci): pin the script-test event-loop yield invariant (#10037) --- scripts/tests/event-loop-yield.test.js | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 scripts/tests/event-loop-yield.test.js diff --git a/scripts/tests/event-loop-yield.test.js b/scripts/tests/event-loop-yield.test.js new file mode 100644 index 00000000000..3bf2b46a2f4 --- /dev/null +++ b/scripts/tests/event-loop-yield.test.js @@ -0,0 +1,25 @@ +/** + * @license + * Copyright 2026 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + */ + +import { expect, it } from 'vitest'; + +// Pins the invariant established by test-setup.ts's beforeEach yield: +// between two tests the worker's event loop must turn far enough to drain +// pending macrotasks. Without that yield a fully synchronous file never +// reaches the timer phase between tests, the flag armed below never flips, +// and the same unbroken stall is what lets vitest's fixed 60s worker->main +// `onTaskUpdate` RPC timeout fire (see test-setup.ts). +let macrotaskRan = false; + +it('arms a flag from a real macrotask callback', () => { + setTimeout(() => { + macrotaskRan = true; + }, 0); +}); + +it('observes the event loop turned between tests', () => { + expect(macrotaskRan).toBe(true); +});