@@ -23,6 +23,47 @@ import { PythonEnvironment, PythonEnvironmentApi } from '../../api';
2323import { ENVS_EXTENSION_ID } from '../constants' ;
2424import { TestEventHandler , waitForCondition } from '../testUtils' ;
2525
26+ const ENV_CHANGE_TIMEOUT_MS = 15_000 ;
27+
28+ function getDifferentEnvironment (
29+ environments : PythonEnvironment [ ] ,
30+ currentEnv : PythonEnvironment | undefined ,
31+ ) : PythonEnvironment | undefined {
32+ return environments . find ( ( env ) => env . envId . id !== currentEnv ?. envId . id ) ;
33+ }
34+
35+ async function setEnvironmentAndWaitForChange (
36+ api : PythonEnvironmentApi ,
37+ projectUri : vscode . Uri ,
38+ env : PythonEnvironment ,
39+ timeoutMs = ENV_CHANGE_TIMEOUT_MS ,
40+ ) : Promise < void > {
41+ await new Promise < void > ( ( resolve , reject ) => {
42+ let subscription : vscode . Disposable | undefined ;
43+ const timeout = setTimeout ( ( ) => {
44+ subscription ?. dispose ( ) ;
45+ reject (
46+ new Error ( `onDidChangeEnvironment did not fire within ${ timeoutMs } ms. Expected envId: ${ env . envId . id } ` ) ,
47+ ) ;
48+ } , timeoutMs ) ;
49+
50+ subscription = api . onDidChangeEnvironment ( ( e ) => {
51+ if ( e . uri ?. toString ( ) === projectUri . toString ( ) && e . new ?. envId . id === env . envId . id ) {
52+ clearTimeout ( timeout ) ;
53+ subscription ?. dispose ( ) ;
54+ resolve ( ) ;
55+ }
56+ } ) ;
57+
58+ // Set environment after subscribing so we don't miss the event.
59+ api . setEnvironment ( projectUri , env ) . catch ( ( err ) => {
60+ clearTimeout ( timeout ) ;
61+ subscription ?. dispose ( ) ;
62+ reject ( err ) ;
63+ } ) ;
64+ } ) ;
65+ }
66+
2667suite ( 'Integration: Python Projects' , function ( ) {
2768 this . timeout ( 60_000 ) ;
2869
@@ -162,54 +203,29 @@ suite('Integration: Python Projects', function () {
162203
163204 const environments = await api . getEnvironments ( 'all' ) ;
164205
165- if ( environments . length === 0 ) {
206+ if ( environments . length < 2 ) {
166207 this . skip ( ) ;
167208 return ;
168209 }
169210
170211 const project = projects [ 0 ] ;
171- const env = environments [ 0 ] ;
172-
173- // Diagnostic logging for CI debugging
174- console . log ( `[TEST DEBUG] Project URI: ${ project . uri . fsPath } ` ) ;
175- console . log ( `[TEST DEBUG] Setting environment with envId: ${ env . envId . id } ` ) ;
176- console . log ( `[TEST DEBUG] Environment path: ${ env . environmentPath ?. fsPath } ` ) ;
177- console . log ( `[TEST DEBUG] Total environments available: ${ environments . length } ` ) ;
178- environments . forEach ( ( e , i ) => {
179- console . log ( `[TEST DEBUG] env[${ i } ]: ${ e . envId . id } (${ e . displayName } )` ) ;
180- } ) ;
181212
182- // Set environment for project
183- await api . setEnvironment ( project . uri , env ) ;
184-
185- // Track what getEnvironment returns during polling for diagnostics
186- let pollCount = 0 ;
187- let lastRetrievedId : string | undefined ;
188- let lastRetrievedManagerId : string | undefined ;
189-
190- // Wait for the environment to be retrievable with the correct ID
191- // This handles async persistence across platforms
192- // Use 15s timeout - CI runners (especially macos) can be slow with settings persistence
193- await waitForCondition (
194- async ( ) => {
195- const retrieved = await api . getEnvironment ( project . uri ) ;
196- pollCount ++ ;
197- const retrievedId = retrieved ?. envId ?. id ;
198- lastRetrievedManagerId = retrieved ?. envId ?. managerId ;
199- if ( retrievedId !== lastRetrievedId ) {
200- console . log (
201- `[TEST DEBUG] Poll #${ pollCount } : getEnvironment returned envId=${ retrievedId ?? 'undefined' } , managerId=${ lastRetrievedManagerId ?? 'undefined' } ` ,
202- ) ;
203- lastRetrievedId = retrievedId ;
204- }
205- return retrieved !== undefined && retrieved . envId . id === env . envId . id ;
206- } ,
207- 15_000 ,
208- ( ) =>
209- `Environment was not set correctly. Expected envId: ${ env . envId . id } (manager: ${ env . envId . managerId } ), last retrieved: ${ lastRetrievedId ?? 'undefined' } (manager: ${ lastRetrievedManagerId ?? 'undefined' } ) after ${ pollCount } polls` ,
210- ) ;
211-
212- // Final verification
213+ // Pick an environment different from the current one so setEnvironment
214+ // actually triggers a change event. If all candidates map to the same env,
215+ // skip instead of hanging on an event that will never fire.
216+ const currentEnv = await api . getEnvironment ( project . uri ) ;
217+ const env = getDifferentEnvironment ( environments , currentEnv ) ;
218+ if ( ! env ) {
219+ this . skip ( ) ;
220+ return ;
221+ }
222+
223+ // Using an event-driven approach instead of polling avoids a race condition where
224+ // setEnvironment's async settings write hasn't landed by the time getEnvironment
225+ // reads back the manager from settings.
226+ await setEnvironmentAndWaitForChange ( api , project . uri , env ) ;
227+
228+ // Verify getEnvironment returns the correct value now that setEnvironment has fully completed
213229 const retrievedEnv = await api . getEnvironment ( project . uri ) ;
214230 assert . ok ( retrievedEnv , 'Should get environment after setting' ) ;
215231 assert . strictEqual ( retrievedEnv . envId . id , env . envId . id , 'Retrieved environment should match set environment' ) ;
@@ -232,13 +248,12 @@ suite('Integration: Python Projects', function () {
232248
233249 const project = projects [ 0 ] ;
234250
235- // Get current environment to pick a different one
251+ // Pick an environment different from the current one so a change event is guaranteed.
236252 const currentEnv = await api . getEnvironment ( project . uri ) ;
237-
238- // Pick an environment different from current
239- let targetEnv = environments [ 0 ] ;
240- if ( currentEnv && currentEnv . envId . id === targetEnv . envId . id ) {
241- targetEnv = environments [ 1 ] ;
253+ const targetEnv = getDifferentEnvironment ( environments , currentEnv ) ;
254+ if ( ! targetEnv ) {
255+ this . skip ( ) ;
256+ return ;
242257 }
243258
244259 // Register handler BEFORE making the change
@@ -275,30 +290,23 @@ suite('Integration: Python Projects', function () {
275290 const projects = api . getPythonProjects ( ) ;
276291 const environments = await api . getEnvironments ( 'all' ) ;
277292
278- if ( projects . length === 0 || environments . length === 0 ) {
293+ if ( projects . length === 0 || environments . length < 2 ) {
279294 this . skip ( ) ;
280295 return ;
281296 }
282297
283298 const project = projects [ 0 ] ;
284- const env = environments [ 0 ] ;
285-
286- // Set environment first
287- await api . setEnvironment ( project . uri , env ) ;
288-
289- // Wait for it to be set
290- // Use 15s timeout - CI runners can be slow with settings persistence
291- let clearTestLastId : string | undefined ;
292- await waitForCondition (
293- async ( ) => {
294- const retrieved = await api . getEnvironment ( project . uri ) ;
295- clearTestLastId = retrieved ?. envId ?. id ;
296- return retrieved !== undefined && retrieved . envId . id === env . envId . id ;
297- } ,
298- 15_000 ,
299- ( ) =>
300- `Environment was not set before clearing. Expected: ${ env . envId . id } (manager: ${ env . envId . managerId } ), got: ${ clearTestLastId ?? 'undefined' } ` ,
301- ) ;
299+
300+ // Pick an environment different from the current one to guarantee a change event
301+ const currentEnv = await api . getEnvironment ( project . uri ) ;
302+ const env = getDifferentEnvironment ( environments , currentEnv ) ;
303+ if ( ! env ) {
304+ this . skip ( ) ;
305+ return ;
306+ }
307+
308+ // Set environment first, using event-driven wait.
309+ await setEnvironmentAndWaitForChange ( api , project . uri , env ) ;
302310
303311 // Verify it was set
304312 const beforeClear = await api . getEnvironment ( project . uri ) ;
@@ -337,32 +345,23 @@ suite('Integration: Python Projects', function () {
337345 const projects = api . getPythonProjects ( ) ;
338346 const environments = await api . getEnvironments ( 'all' ) ;
339347
340- if ( projects . length === 0 || environments . length === 0 ) {
348+ if ( projects . length === 0 || environments . length < 2 ) {
341349 this . skip ( ) ;
342350 return ;
343351 }
344352
345353 const project = projects [ 0 ] ;
346- const env = environments [ 0 ] ;
347-
348- // Set environment for project
349- await api . setEnvironment ( project . uri , env ) ;
350-
351- // Wait for it to be set
352- // Use 15s timeout - CI runners can be slow with settings persistence
353- let fileTestLastId : string | undefined ;
354- let fileTestLastManagerId : string | undefined ;
355- await waitForCondition (
356- async ( ) => {
357- const retrieved = await api . getEnvironment ( project . uri ) ;
358- fileTestLastId = retrieved ?. envId ?. id ;
359- fileTestLastManagerId = retrieved ?. envId ?. managerId ;
360- return retrieved !== undefined && retrieved . envId . id === env . envId . id ;
361- } ,
362- 15_000 ,
363- ( ) =>
364- `Environment was not set for project. Expected: ${ env . envId . id } (manager: ${ env . envId . managerId } ), got: ${ fileTestLastId ?? 'undefined' } (manager: ${ fileTestLastManagerId ?? 'undefined' } )` ,
365- ) ;
354+
355+ // Pick an environment different from the current one to guarantee a change event
356+ const currentEnv = await api . getEnvironment ( project . uri ) ;
357+ const env = getDifferentEnvironment ( environments , currentEnv ) ;
358+ if ( ! env ) {
359+ this . skip ( ) ;
360+ return ;
361+ }
362+
363+ // Set environment for project, using event-driven wait.
364+ await setEnvironmentAndWaitForChange ( api , project . uri , env ) ;
366365
367366 // Create a hypothetical file path inside the project
368367 const fileUri = vscode . Uri . joinPath ( project . uri , 'some_script.py' ) ;
0 commit comments