2323
2424import * as assert from 'assert' ;
2525import * as vscode from 'vscode' ;
26- import { DidChangeEnvironmentEventArgs , PythonEnvironment , PythonEnvironmentApi } from '../../api' ;
26+ import { DidChangeEnvironmentEventArgs , PythonEnvironment , PythonEnvironmentApi , SetEnvironmentScope } from '../../api' ;
2727import { ENVS_EXTENSION_ID } from '../constants' ;
2828import { TestEventHandler , waitForCondition } from '../testUtils' ;
2929
30+ /**
31+ * Calls setEnvironment and waits for the async event chain to fully settle.
32+ *
33+ * setEnvironment fires onDidChangeActiveEnvironment via setImmediate, which
34+ * can trigger refreshEnvironment (another setImmediate), which may fire
35+ * again (a third setImmediate). This helper subscribes to the event BEFORE
36+ * the call and yields enough event loop iterations to drain all pending
37+ * setImmediate callbacks — ensuring no stale events leak into subsequent
38+ * test steps.
39+ *
40+ * This is NOT a timeout — each iteration is a deterministic event loop tick
41+ * that completes in microseconds. For idempotent sets (env already matches),
42+ * no events fire and the loop exits immediately after the ticks drain.
43+ */
44+ async function setEnvironmentAndWait (
45+ api : PythonEnvironmentApi ,
46+ scope : SetEnvironmentScope ,
47+ env : PythonEnvironment | undefined ,
48+ ) : Promise < void > {
49+ const handler = new TestEventHandler < DidChangeEnvironmentEventArgs > (
50+ api . onDidChangeEnvironment ,
51+ 'setEnvironmentAndWait' ,
52+ ) ;
53+
54+ try {
55+ await api . setEnvironment ( scope , env ) ;
56+
57+ // Drain the setImmediate event chain. Each tick processes one
58+ // pending callback. 5 ticks covers the full chain:
59+ // tick 1: _onDidChangeActiveEnvironment.fire()
60+ // tick 2: refreshEnvironment() from manager callback
61+ // tick 3: _onDidChangeActiveEnvironment.fire() from refresh
62+ // ticks 4-5: safety margin for edge cases
63+ for ( let tick = 0 ; tick < 5 ; tick ++ ) {
64+ await new Promise < void > ( ( resolve ) => setImmediate ( resolve ) ) ;
65+ if ( handler . fired ) {
66+ break ;
67+ }
68+ }
69+ } finally {
70+ handler . dispose ( ) ;
71+ }
72+ }
73+
3074suite ( 'Integration: Interpreter Selection Priority' , function ( ) {
3175 this . timeout ( 60_000 ) ;
3276
@@ -51,14 +95,12 @@ suite('Integration: Interpreter Selection Priority', function () {
5195 originalEnv = await api . getEnvironment ( undefined ) ;
5296 } ) ;
5397
54- // Reset to original state after each test to prevent state pollution
98+ // Reset to original state after each test to prevent state pollution.
99+ // Uses setEnvironmentAndWait to ensure all async events drain before
100+ // the next test starts — prevents stale events from leaking.
55101 teardown ( async function ( ) {
56102 try {
57- if ( originalEnv ) {
58- await api . setEnvironment ( undefined , originalEnv ) ;
59- } else {
60- await api . setEnvironment ( undefined , undefined ) ;
61- }
103+ await setEnvironmentAndWait ( api , undefined , originalEnv ) ;
62104 } catch {
63105 // Ignore errors during reset
64106 }
@@ -96,11 +138,9 @@ suite('Integration: Interpreter Selection Priority', function () {
96138
97139 const envToSet = environments [ 0 ] ;
98140
99- // Set environment globally
100- await api . setEnvironment ( undefined , envToSet ) ;
141+ // Set environment and wait for async event chain to settle
142+ await setEnvironmentAndWait ( api , undefined , envToSet ) ;
101143
102- // getEnvironment calls manager.get() directly (no cache),
103- // so it returns the correct value immediately after set.
104144 const retrieved = await api . getEnvironment ( undefined ) ;
105145
106146 assert . ok ( retrieved , 'Should have environment after setting' ) ;
@@ -130,14 +170,11 @@ suite('Integration: Interpreter Selection Priority', function () {
130170 const projectEnv = environments [ 1 ] ;
131171 const project = projects [ 0 ] ;
132172
133- // Set global environment
134- await api . setEnvironment ( undefined , globalEnv ) ;
135-
136- // Set different environment for project
137- await api . setEnvironment ( project . uri , projectEnv ) ;
173+ // Set global environment and wait for event chain to settle
174+ await setEnvironmentAndWait ( api , undefined , globalEnv ) ;
138175
139- // getEnvironment calls manager.get() directly (no cache),
140- // so it returns the correct value immediately after set.
176+ // Set different environment for project and wait
177+ await setEnvironmentAndWait ( api , project . uri , projectEnv ) ;
141178
142179 // Verify global is unchanged
143180 const globalRetrieved = await api . getEnvironment ( undefined ) ;
@@ -175,29 +212,22 @@ suite('Integration: Interpreter Selection Priority', function () {
175212 const oldEnv = environments [ 0 ] ;
176213 const newEnv = environments [ 1 ] ;
177214
178- // Subscribe BEFORE any setEnvironment calls. The handler captures
179- // all events — from both sets and any async refreshEnvironment
180- // side-effects. We then search for the specific event we need.
181- // This eliminates drain races entirely: no matter how many
182- // setImmediate ticks the event chain takes, the handler sees
183- // everything and we pick the right event by envId match.
215+ // Set initial environment and wait for all async events to drain.
216+ // This ensures no stale events from this call leak into the handler.
217+ await setEnvironmentAndWait ( api , undefined , oldEnv ) ;
218+
219+ // Now subscribe a fresh handler for the second set
184220 const handler = new TestEventHandler < DidChangeEnvironmentEventArgs > (
185221 api . onDidChangeEnvironment ,
186222 'onDidChangeEnvironment' ,
187223 ) ;
188224
189225 try {
190- // Set initial environment (may or may not fire events)
191- await api . setEnvironment ( undefined , oldEnv ) ;
192-
193226 // Change to new environment — this MUST fire an event since
194- // oldEnv.envId.id !== newEnv.envId.id, so the _activeSelection
195- // idempotency check passes.
227+ // oldEnv.envId.id !== newEnv.envId.id
196228 await api . setEnvironment ( undefined , newEnv ) ;
197229
198- // Wait for the specific event whose new env matches newEnv.
199- // Events from the first set (new=oldEnv) are captured but
200- // won't match this condition, so they're harmlessly ignored.
230+ // Wait for the event whose new env matches what we set
201231 await waitForCondition (
202232 ( ) => handler . all . some ( ( e ) => e . new ?. envId . id === newEnv . envId . id ) ,
203233 15_000 ,
@@ -237,8 +267,8 @@ suite('Integration: Interpreter Selection Priority', function () {
237267 const project = projects [ 0 ] ;
238268 const env = environments [ 0 ] ;
239269
240- // Set project environment
241- await api . setEnvironment ( project . uri , env ) ;
270+ // Set project environment and wait for event chain to settle
271+ await setEnvironmentAndWait ( api , project . uri , env ) ;
242272
243273 // Query for a file inside the project
244274 const fileUri = vscode . Uri . joinPath ( project . uri , 'subdir' , 'script.py' ) ;
@@ -292,11 +322,11 @@ suite('Integration: Interpreter Selection Priority', function () {
292322
293323 const env = environments [ 0 ] ;
294324
295- // Set environment first time
296- await api . setEnvironment ( undefined , env ) ;
325+ // Set environment first time and wait for event chain to settle
326+ await setEnvironmentAndWait ( api , undefined , env ) ;
297327
298- // Set same environment again
299- await api . setEnvironment ( undefined , env ) ;
328+ // Set same environment again and wait for any events to drain
329+ await setEnvironmentAndWait ( api , undefined , env ) ;
300330
301331 // Verify functional idempotency: after setting the same environment
302332 // twice, getEnvironment should still return the same environment.
@@ -327,13 +357,13 @@ suite('Integration: Interpreter Selection Priority', function () {
327357 return ;
328358 }
329359
330- // Set an explicit environment
331- await api . setEnvironment ( undefined , environments [ 0 ] ) ;
360+ // Set an explicit environment and wait for event chain to settle
361+ await setEnvironmentAndWait ( api , undefined , environments [ 0 ] ) ;
332362 const beforeClear = await api . getEnvironment ( undefined ) ;
333363 assert . ok ( beforeClear , 'Should have environment before clearing' ) ;
334364
335- // Clear the selection
336- await api . setEnvironment ( undefined , undefined ) ;
365+ // Clear the selection and wait for event chain to settle
366+ await setEnvironmentAndWait ( api , undefined , undefined ) ;
337367
338368 // Get environment - should return auto-discovered environment
339369 const autoEnv = await api . getEnvironment ( undefined ) ;
0 commit comments