@@ -480,6 +480,134 @@ suite('getAllExtraSearchPaths Integration Tests', () => {
480480 assert . ok ( mockTraceWarn . called , 'Should warn about missing workspace folders' ) ;
481481 } ) ;
482482
483+ test ( 'Multi-root workspace - each folder reads its own workspaceSearchPaths' , async ( ) => {
484+ // Mock → Two folders with different folder-level workspaceSearchPaths
485+ const workspace1 = Uri . file ( '/workspace/project1' ) ;
486+ const workspace2 = Uri . file ( '/workspace/project2' ) ;
487+
488+ // Create separate config objects for each folder
489+ const envConfig1 : MockWorkspaceConfig = {
490+ get : sinon . stub ( ) ,
491+ inspect : sinon . stub ( ) ,
492+ update : sinon . stub ( ) ,
493+ } ;
494+ const envConfig2 : MockWorkspaceConfig = {
495+ get : sinon . stub ( ) ,
496+ inspect : sinon . stub ( ) ,
497+ update : sinon . stub ( ) ,
498+ } ;
499+
500+ envConfig1 . inspect . withArgs ( 'globalSearchPaths' ) . returns ( { globalValue : [ ] } ) ;
501+ envConfig1 . inspect . withArgs ( 'workspaceSearchPaths' ) . returns ( {
502+ workspaceFolderValue : [ '/envs/project1' ] ,
503+ } ) ;
504+
505+ envConfig2 . inspect . withArgs ( 'globalSearchPaths' ) . returns ( { globalValue : [ ] } ) ;
506+ envConfig2 . inspect . withArgs ( 'workspaceSearchPaths' ) . returns ( {
507+ workspaceFolderValue : [ '/envs/project2' ] ,
508+ } ) ;
509+
510+ // Return folder-specific configs based on the scope URI passed to getConfiguration
511+ mockGetConfiguration . callsFake ( ( section : string , scope ?: unknown ) => {
512+ if ( section === 'python' ) {
513+ return pythonConfig ;
514+ }
515+ if ( section === 'python-envs' ) {
516+ if ( scope && ( scope as Uri ) . fsPath === workspace1 . fsPath ) {
517+ return envConfig1 ;
518+ }
519+ if ( scope && ( scope as Uri ) . fsPath === workspace2 . fsPath ) {
520+ return envConfig2 ;
521+ }
522+ return envConfig ; // fallback for unscoped calls
523+ }
524+ throw new Error ( `Unexpected configuration section: ${ section } ` ) ;
525+ } ) ;
526+
527+ pythonConfig . get . withArgs ( 'venvPath' ) . returns ( undefined ) ;
528+ pythonConfig . get . withArgs ( 'venvFolders' ) . returns ( undefined ) ;
529+ mockGetWorkspaceFolders . returns ( [ { uri : workspace1 } , { uri : workspace2 } ] ) ;
530+
531+ // Run
532+ const result = await getAllExtraSearchPaths ( ) ;
533+
534+ // Assert - each folder's workspaceSearchPaths is read independently
535+ assert . ok ( result . includes ( '/envs/project1' ) , 'Should include project1 env path' ) ;
536+ assert . ok ( result . includes ( '/envs/project2' ) , 'Should include project2 env path' ) ;
537+ assert . strictEqual ( result . length , 2 , 'Should have exactly 2 paths (one per folder)' ) ;
538+ } ) ;
539+
540+ test ( 'Multi-root workspace - relative paths resolved against the correct folder' , async ( ) => {
541+ // Mock → Two folders, each with a relative workspaceSearchPaths
542+ const workspace1 = Uri . file ( '/workspace/project1' ) ;
543+ const workspace2 = Uri . file ( '/workspace/project2' ) ;
544+
545+ const envConfig1 : MockWorkspaceConfig = {
546+ get : sinon . stub ( ) ,
547+ inspect : sinon . stub ( ) ,
548+ update : sinon . stub ( ) ,
549+ } ;
550+ const envConfig2 : MockWorkspaceConfig = {
551+ get : sinon . stub ( ) ,
552+ inspect : sinon . stub ( ) ,
553+ update : sinon . stub ( ) ,
554+ } ;
555+
556+ envConfig1 . inspect . withArgs ( 'globalSearchPaths' ) . returns ( { globalValue : [ ] } ) ;
557+ envConfig1 . inspect . withArgs ( 'workspaceSearchPaths' ) . returns ( {
558+ workspaceFolderValue : [ 'envs' ] ,
559+ } ) ;
560+
561+ envConfig2 . inspect . withArgs ( 'globalSearchPaths' ) . returns ( { globalValue : [ ] } ) ;
562+ envConfig2 . inspect . withArgs ( 'workspaceSearchPaths' ) . returns ( {
563+ workspaceFolderValue : [ 'venvs' ] ,
564+ } ) ;
565+
566+ mockGetConfiguration . callsFake ( ( section : string , scope ?: unknown ) => {
567+ if ( section === 'python' ) {
568+ return pythonConfig ;
569+ }
570+ if ( section === 'python-envs' ) {
571+ if ( scope && ( scope as Uri ) . fsPath === workspace1 . fsPath ) {
572+ return envConfig1 ;
573+ }
574+ if ( scope && ( scope as Uri ) . fsPath === workspace2 . fsPath ) {
575+ return envConfig2 ;
576+ }
577+ return envConfig ;
578+ }
579+ throw new Error ( `Unexpected configuration section: ${ section } ` ) ;
580+ } ) ;
581+
582+ pythonConfig . get . withArgs ( 'venvPath' ) . returns ( undefined ) ;
583+ pythonConfig . get . withArgs ( 'venvFolders' ) . returns ( undefined ) ;
584+ mockGetWorkspaceFolders . returns ( [ { uri : workspace1 } , { uri : workspace2 } ] ) ;
585+
586+ // Run
587+ const result = await getAllExtraSearchPaths ( ) ;
588+
589+ // Assert - relative paths resolved only against their own folder
590+ assert . strictEqual ( result . length , 2 , 'Should have exactly 2 paths (one per folder)' ) ;
591+ assert . ok (
592+ result . some ( ( p ) => p . includes ( 'project1' ) && p . endsWith ( '/envs' ) ) ,
593+ 'project1/envs should come from project1 config' ,
594+ ) ;
595+ assert . ok (
596+ result . some ( ( p ) => p . includes ( 'project2' ) && p . endsWith ( '/venvs' ) ) ,
597+ 'project2/venvs should come from project2 config' ,
598+ ) ;
599+ // project1 relative path must NOT be resolved against project2
600+ assert . ok (
601+ ! result . some ( ( p ) => p . includes ( 'project2' ) && p . endsWith ( '/envs' ) ) ,
602+ 'project1 relative path should not be resolved against project2' ,
603+ ) ;
604+ // project2 relative path must NOT be resolved against project1
605+ assert . ok (
606+ ! result . some ( ( p ) => p . includes ( 'project1' ) && p . endsWith ( '/venvs' ) ) ,
607+ 'project2 relative path should not be resolved against project1' ,
608+ ) ;
609+ } ) ;
610+
483611 test ( 'Empty and whitespace paths are skipped' , async ( ) => {
484612 // Mock → Mix of valid and invalid paths
485613 pythonConfig . get . withArgs ( 'venvPath' ) . returns ( undefined ) ;
0 commit comments