@@ -268,6 +268,173 @@ suite('Interpreter Selection - Priority Chain', () => {
268268 assert . ok ( mockNativeFinder . resolve . calledOnceWithExactly ( expandedInterpreterPath ) ) ;
269269 } ) ;
270270
271+ test ( 'should resolve a relative defaultInterpreterPath against the workspace folder' , async ( ) => {
272+ // A relative path must be made absolute against the workspace folder so the native
273+ // finder does not resolve it against an unrelated working directory (which can yield
274+ // a malformed, duplicated path such as <workspace>/<workspace-name>/.venv/...).
275+ const workspaceUri = Uri . file ( path . resolve ( '/test/workspace' ) ) ;
276+ const absoluteInterpreterPath = path . resolve ( workspaceUri . fsPath , '.venv/bin/python' ) ;
277+ const workspaceFolder = { name : 'workspace' , uri : workspaceUri } as WorkspaceFolder ;
278+
279+ sandbox . stub ( workspaceApis , 'getConfiguration' ) . returns ( createMockConfig ( [ ] ) as WorkspaceConfiguration ) ;
280+ sandbox . stub ( workspaceApis , 'getWorkspaceFolder' ) . returns ( workspaceFolder ) ;
281+ sandbox . stub ( workspaceApis , 'getWorkspaceFolders' ) . returns ( [ workspaceFolder ] ) ;
282+ sandbox . stub ( helpers , 'getUserConfiguredSetting' ) . callsFake ( ( section : string , key : string ) => {
283+ if ( section === 'python' && key === 'defaultInterpreterPath' ) {
284+ return './.venv/bin/python' ;
285+ }
286+ return undefined ;
287+ } ) ;
288+ mockNativeFinder . resolve . resolves ( {
289+ executable : absoluteInterpreterPath ,
290+ version : '3.11.0' ,
291+ prefix : path . dirname ( path . dirname ( absoluteInterpreterPath ) ) ,
292+ } ) ;
293+ mockApi . resolveEnvironment . resolves ( {
294+ ...mockVenvEnv ,
295+ displayPath : absoluteInterpreterPath ,
296+ environmentPath : Uri . file ( absoluteInterpreterPath ) ,
297+ execInfo : { run : { executable : absoluteInterpreterPath } } ,
298+ } ) ;
299+
300+ const result = await resolveEnvironmentByPriority (
301+ workspaceUri ,
302+ mockEnvManagers as unknown as EnvironmentManagers ,
303+ mockProjectManager as unknown as PythonProjectManager ,
304+ mockNativeFinder as unknown as NativePythonFinder ,
305+ mockApi as unknown as PythonEnvironmentApi ,
306+ ) ;
307+
308+ assert . strictEqual ( result . source , 'defaultInterpreterPath' ) ;
309+ assert . ok ( mockNativeFinder . resolve . calledOnceWithExactly ( absoluteInterpreterPath ) ) ;
310+ // The path passed to the native finder must not duplicate the workspace folder name.
311+ const passedPath = mockNativeFinder . resolve . firstCall . args [ 0 ] as string ;
312+ assert . ok (
313+ ! passedPath . includes ( `workspace${ path . sep } workspace` ) ,
314+ `path should not duplicate the workspace folder: ${ passedPath } ` ,
315+ ) ;
316+ } ) ;
317+
318+ test ( 'should resolve a relative defaultInterpreterPath from its scope when folder lookup fails' , async ( ) => {
319+ // The scope already identifies the workspace folder, so drive-letter casing must not
320+ // prevent relative path resolution when getWorkspaceFolder returns undefined.
321+ const workspaceUri = Uri . file ( path . resolve ( '/test/workspace' ) ) ;
322+ const absoluteInterpreterPath = path . resolve ( workspaceUri . fsPath , '.venv/bin/python' ) ;
323+ const workspaceFolder = { name : 'workspace' , uri : workspaceUri } as WorkspaceFolder ;
324+
325+ sandbox . stub ( workspaceApis , 'getConfiguration' ) . returns ( createMockConfig ( [ ] ) as WorkspaceConfiguration ) ;
326+ sandbox . stub ( workspaceApis , 'getWorkspaceFolder' ) . returns ( undefined ) ;
327+ sandbox . stub ( workspaceApis , 'getWorkspaceFolders' ) . returns ( [ workspaceFolder ] ) ;
328+ sandbox . stub ( helpers , 'getUserConfiguredSetting' ) . callsFake ( ( section : string , key : string ) => {
329+ if ( section === 'python' && key === 'defaultInterpreterPath' ) {
330+ return './.venv/bin/python' ;
331+ }
332+ return undefined ;
333+ } ) ;
334+ mockNativeFinder . resolve . resolves ( {
335+ executable : absoluteInterpreterPath ,
336+ version : '3.11.0' ,
337+ prefix : path . dirname ( path . dirname ( absoluteInterpreterPath ) ) ,
338+ } ) ;
339+ mockApi . resolveEnvironment . resolves ( {
340+ ...mockVenvEnv ,
341+ displayPath : absoluteInterpreterPath ,
342+ environmentPath : Uri . file ( absoluteInterpreterPath ) ,
343+ execInfo : { run : { executable : absoluteInterpreterPath } } ,
344+ } ) ;
345+
346+ const result = await resolveEnvironmentByPriority (
347+ workspaceUri ,
348+ mockEnvManagers as unknown as EnvironmentManagers ,
349+ mockProjectManager as unknown as PythonProjectManager ,
350+ mockNativeFinder as unknown as NativePythonFinder ,
351+ mockApi as unknown as PythonEnvironmentApi ,
352+ ) ;
353+
354+ assert . strictEqual ( result . source , 'defaultInterpreterPath' ) ;
355+ assert . ok ( mockNativeFinder . resolve . calledOnceWithExactly ( absoluteInterpreterPath ) ) ;
356+ } ) ;
357+
358+ test ( 'should resolve a relative defaultInterpreterPath from its scope with multiple folders open' , async ( ) => {
359+ const workspaceUri = Uri . file ( path . resolve ( '/test/workspace' ) ) ;
360+ const otherUri = Uri . file ( path . resolve ( '/test/other' ) ) ;
361+ const relativeInterpreterPath = './.venv/bin/python' ;
362+ const absoluteInterpreterPath = path . resolve ( workspaceUri . fsPath , relativeInterpreterPath ) ;
363+
364+ sandbox . stub ( workspaceApis , 'getConfiguration' ) . returns ( createMockConfig ( [ ] ) as WorkspaceConfiguration ) ;
365+ sandbox . stub ( workspaceApis , 'getWorkspaceFolder' ) . returns ( undefined ) ;
366+ sandbox . stub ( workspaceApis , 'getWorkspaceFolders' ) . returns ( [
367+ { name : 'workspace' , uri : workspaceUri } as WorkspaceFolder ,
368+ { name : 'other' , uri : otherUri } as WorkspaceFolder ,
369+ ] ) ;
370+ sandbox . stub ( helpers , 'getUserConfiguredSetting' ) . callsFake ( ( section : string , key : string ) => {
371+ if ( section === 'python' && key === 'defaultInterpreterPath' ) {
372+ return relativeInterpreterPath ;
373+ }
374+ return undefined ;
375+ } ) ;
376+ mockNativeFinder . resolve . resolves ( {
377+ executable : absoluteInterpreterPath ,
378+ version : '3.11.0' ,
379+ prefix : path . dirname ( path . dirname ( absoluteInterpreterPath ) ) ,
380+ } ) ;
381+ mockApi . resolveEnvironment . resolves ( {
382+ ...mockVenvEnv ,
383+ displayPath : absoluteInterpreterPath ,
384+ environmentPath : Uri . file ( absoluteInterpreterPath ) ,
385+ execInfo : { run : { executable : absoluteInterpreterPath } } ,
386+ } ) ;
387+
388+ await resolveEnvironmentByPriority (
389+ workspaceUri ,
390+ mockEnvManagers as unknown as EnvironmentManagers ,
391+ mockProjectManager as unknown as PythonProjectManager ,
392+ mockNativeFinder as unknown as NativePythonFinder ,
393+ mockApi as unknown as PythonEnvironmentApi ,
394+ ) ;
395+
396+ assert . ok ( mockNativeFinder . resolve . calledOnceWithExactly ( absoluteInterpreterPath ) ) ;
397+ } ) ;
398+
399+ test ( 'should pass an absolute defaultInterpreterPath to the native finder unchanged' , async ( ) => {
400+ // Absolute paths must not be re-resolved against the workspace folder.
401+ const workspaceUri = Uri . file ( path . resolve ( '/test/workspace' ) ) ;
402+ const absoluteInterpreterPath = Uri . file ( path . resolve ( '/opt/python/bin/python' ) ) . fsPath ;
403+ const workspaceFolder = { name : 'workspace' , uri : workspaceUri } as WorkspaceFolder ;
404+
405+ sandbox . stub ( workspaceApis , 'getConfiguration' ) . returns ( createMockConfig ( [ ] ) as WorkspaceConfiguration ) ;
406+ sandbox . stub ( workspaceApis , 'getWorkspaceFolder' ) . returns ( workspaceFolder ) ;
407+ sandbox . stub ( workspaceApis , 'getWorkspaceFolders' ) . returns ( [ workspaceFolder ] ) ;
408+ sandbox . stub ( helpers , 'getUserConfiguredSetting' ) . callsFake ( ( section : string , key : string ) => {
409+ if ( section === 'python' && key === 'defaultInterpreterPath' ) {
410+ return absoluteInterpreterPath ;
411+ }
412+ return undefined ;
413+ } ) ;
414+ mockNativeFinder . resolve . resolves ( {
415+ executable : absoluteInterpreterPath ,
416+ version : '3.11.0' ,
417+ prefix : path . dirname ( path . dirname ( absoluteInterpreterPath ) ) ,
418+ } ) ;
419+ mockApi . resolveEnvironment . resolves ( {
420+ ...mockSystemEnv ,
421+ displayPath : absoluteInterpreterPath ,
422+ environmentPath : Uri . file ( absoluteInterpreterPath ) ,
423+ execInfo : { run : { executable : absoluteInterpreterPath } } ,
424+ } ) ;
425+
426+ const result = await resolveEnvironmentByPriority (
427+ workspaceUri ,
428+ mockEnvManagers as unknown as EnvironmentManagers ,
429+ mockProjectManager as unknown as PythonProjectManager ,
430+ mockNativeFinder as unknown as NativePythonFinder ,
431+ mockApi as unknown as PythonEnvironmentApi ,
432+ ) ;
433+
434+ assert . strictEqual ( result . source , 'defaultInterpreterPath' ) ;
435+ assert . ok ( mockNativeFinder . resolve . calledOnceWithExactly ( absoluteInterpreterPath ) ) ;
436+ } ) ;
437+
271438 test ( 'should skip native resolution when defaultInterpreterPath has unresolved variables' , async ( ) => {
272439 // When resolveVariables can't resolve ${workspaceFolder} (e.g., global scope with no workspace),
273440 // the path still contains '${' and should be skipped without calling nativeFinder.resolve
@@ -1183,6 +1350,36 @@ suite('Interpreter Selection - resolveGlobalEnvironmentByPriority', () => {
11831350 assert . strictEqual ( result . environment . execInfo ?. run ?. executable , userPyenvPath ) ;
11841351 } ) ;
11851352
1353+ test ( 'should not resolve a global relative defaultInterpreterPath against the open workspace' , async ( ) => {
1354+ const relativeInterpreterPath = path . join ( '.venv' , process . platform === 'win32' ? 'Scripts' : 'bin' , 'python' ) ;
1355+ const workspaceFolder = {
1356+ name : 'workspace' ,
1357+ uri : Uri . file ( path . resolve ( '/test/workspace' ) ) ,
1358+ } as WorkspaceFolder ;
1359+
1360+ sandbox . stub ( workspaceApis , 'getWorkspaceFolders' ) . returns ( [ workspaceFolder ] ) ;
1361+ sandbox . stub ( helpers , 'getUserConfiguredSetting' ) . callsFake ( ( section : string , key : string ) => {
1362+ if ( section === 'python' && key === 'defaultInterpreterPath' ) {
1363+ return relativeInterpreterPath ;
1364+ }
1365+ return undefined ;
1366+ } ) ;
1367+ mockNativeFinder . resolve . resolves ( {
1368+ executable : relativeInterpreterPath ,
1369+ version : '3.11.0' ,
1370+ prefix : '.venv' ,
1371+ } ) ;
1372+ mockApi . resolveEnvironment . resolves ( mockSystemEnv ) ;
1373+
1374+ await resolveGlobalEnvironmentByPriority (
1375+ mockEnvManagers as unknown as EnvironmentManagers ,
1376+ mockNativeFinder as unknown as NativePythonFinder ,
1377+ mockApi as unknown as PythonEnvironmentApi ,
1378+ ) ;
1379+
1380+ assert . ok ( mockNativeFinder . resolve . calledOnceWithExactly ( relativeInterpreterPath ) ) ;
1381+ } ) ;
1382+
11861383 test ( 'should use original user path for global scope even when nativeFinder resolves to different executable' , async ( ) => {
11871384 // This is the key bug fix test - user configures pyenv path, native finder returns homebrew
11881385 const userPyenvPath = '/Users/test/.pyenv/versions/3.13.7/bin/python' ;
0 commit comments