@@ -120,28 +120,96 @@ describe('PluginHealthMonitor', () => {
120120 * Ref'd `Timeout` handles — `getActiveResourcesInfo()` reports only
121121 * resources currently keeping the event loop alive, which is exactly the
122122 * property that made `os migrate` idle ~120s in #4813.
123+ *
124+ * The count is *process*-global, so it is only ever read here in
125+ * synchronously adjacent pairs (see below). Comparing two reads separated
126+ * by an `await` is what made this suite flaky in the merge queue (#6329):
127+ * the runner shares this loop and keeps a **non-unref'd 100ms** timer on it
128+ * (`throttle(sendTasksUpdate, 100)` in `@vitest/runner`), so once the
129+ * window between the reads stretched past 100ms under full concurrent load
130+ * — the failing run measured 105ms — that timer fired inside the window and
131+ * the count fell by one for a reason that had nothing to do with the
132+ * monitor. Between two adjacent synchronous statements no timer callback
133+ * can run at all, so a difference measured that way is the monitor's doing
134+ * and nobody else's.
123135 */
124136 const refdTimers = ( ) =>
125137 process . getActiveResourcesInfo ( ) . filter ( ( r ) => r === 'Timeout' ) . length ;
126138
139+ /**
140+ * Run `body` while recording the `Timeout` handles `setTimeout` hands out,
141+ * and return those armed with `delay` — the monitor's health-check guards,
142+ * told apart from every other timer on the shared loop by the very timeout
143+ * they were configured with.
144+ *
145+ * Holding the handles is what lets the assertion below name the guard
146+ * instead of counting the world. It records where the guard came from; what
147+ * it then asserts is still the observable consequence — whether that handle
148+ * is keeping the event loop alive — never that `clearTimeout` was called.
149+ */
150+ const recordingGuards = async (
151+ delay : number ,
152+ body : ( ) => Promise < void >
153+ ) : Promise < NodeJS . Timeout [ ] > => {
154+ const guards : NodeJS . Timeout [ ] = [ ] ;
155+ const real = globalThis . setTimeout ;
156+ const recording = ( ( ...args : Parameters < typeof globalThis . setTimeout > ) => {
157+ const handle = real ( ...args ) ;
158+ if ( args [ 1 ] === delay ) guards . push ( handle ) ;
159+ return handle ;
160+ } ) as typeof globalThis . setTimeout ;
161+ Object . assign ( recording , real ) ;
162+
163+ globalThis . setTimeout = recording ;
164+ try {
165+ await body ( ) ;
166+ } finally {
167+ globalThis . setTimeout = real ;
168+ }
169+ return guards ;
170+ } ;
171+
127172 it ( "leaves no ref'd timer behind when the health check wins the race" , async ( ) => {
128173 const calls = { count : 0 } ;
129- monitor . registerPlugin ( 'guarded-plugin' , guardedConfig ( ) ) ;
174+ const config = guardedConfig ( ) ;
175+ monitor . registerPlugin ( 'guarded-plugin' , config ) ;
130176
131- const before = refdTimers ( ) ;
132- monitor . startMonitoring ( 'guarded-plugin' , healthyPlugin ( calls ) ) ;
177+ const guards = await recordingGuards ( config . timeout , async ( ) => {
178+ monitor . startMonitoring ( 'guarded-plugin' , healthyPlugin ( calls ) ) ;
133179
134- // The initial check runs immediately; wait for its report to land.
135- await vi . waitFor ( ( ) => {
136- expect ( monitor . getHealthReport ( 'guarded-plugin' ) ) . toBeDefined ( ) ;
180+ // The initial check runs immediately; wait for its report to land.
181+ await vi . waitFor ( ( ) => {
182+ expect ( monitor . getHealthReport ( 'guarded-plugin' ) ) . toBeDefined ( ) ;
183+ } ) ;
137184 } ) ;
138185
139- // Drop the monitoring interval — whatever is left is the guard's doing.
140- monitor . stopMonitoring ( 'guarded-plugin' ) ;
141-
142186 expect ( calls . count ) . toBe ( 1 ) ;
143187 expect ( monitor . getHealthStatus ( 'guarded-plugin' ) ) . toBe ( 'healthy' ) ;
144- expect ( refdTimers ( ) ) . toBe ( before ) ;
188+
189+ // The round armed exactly one guard. Without this the reclaim below would
190+ // be vacuously green — a difference of zero because nothing was measured,
191+ // rather than because nothing was left behind.
192+ expect ( guards ) . toHaveLength ( 1 ) ;
193+
194+ // Everything from here to the last assertion runs in one uninterrupted
195+ // synchronous turn, so each difference is attributable.
196+ const whileMonitoring = refdTimers ( ) ;
197+
198+ // Drop the monitoring interval — whatever is left is the guard's doing.
199+ monitor . stopMonitoring ( 'guarded-plugin' ) ;
200+ const afterStop = refdTimers ( ) ;
201+
202+ // The interval was pinning the loop and is now reclaimed. This also keeps
203+ // the instrument honest: `refdTimers()` demonstrably observes *this*
204+ // monitor's timers on *this* loop, so the guard's zero below is a real
205+ // reading and not a blind one.
206+ expect ( whileMonitoring - afterStop ) . toBe ( 1 ) ;
207+
208+ // The guard is not pinning the loop: reclaiming it a second time is a
209+ // no-op. Had it outlived the race it would still be armed and ref'd, and
210+ // this reclaim would drop the count by one.
211+ for ( const guard of guards ) clearTimeout ( guard ) ;
212+ expect ( refdTimers ( ) ) . toBe ( afterStop ) ;
145213 } ) ;
146214
147215 it ( 'still reports the timeout when the check never answers' , async ( ) => {
0 commit comments