@@ -476,6 +476,285 @@ suite('runAsTask Tests', () => {
476476 } ) ;
477477 } ) ;
478478
479+ suite ( 'UV Mode Scenarios' , ( ) => {
480+ test ( 'should pass project URI as scope to shouldUseUv' , async ( ) => {
481+ // Mock - Verify per-folder setting precedence by passing project.uri as the scope
482+ const projectUri = Uri . file ( '/workspace/project' ) ;
483+ const environment : PythonEnvironment = {
484+ envId : { id : 'test-env' , managerId : 'test-manager' } ,
485+ name : 'Test Environment' ,
486+ displayName : 'Test Environment' ,
487+ displayPath : '/path/to/env' ,
488+ version : '3.9.0' ,
489+ environmentPath : Uri . file ( '/path/to/env' ) ,
490+ execInfo : {
491+ run : { executable : '/path/to/python' , args : [ ] } ,
492+ } ,
493+ sysPrefix : '/path/to/env' ,
494+ } ;
495+
496+ const options : PythonTaskExecutionOptions = {
497+ name : 'Scoped Task' ,
498+ args : [ 'script.py' ] ,
499+ project : { name : 'Test Project' , uri : projectUri } ,
500+ } ;
501+
502+ mockGetWorkspaceFolder . withArgs ( projectUri ) . returns ( undefined ) ;
503+ mockShouldUseUv . resolves ( false ) ;
504+ mockQuoteStringIfNecessary . withArgs ( '/path/to/python' ) . returns ( '/path/to/python' ) ;
505+ mockExecuteTask . resolves ( { } as TaskExecution ) ;
506+
507+ // Run
508+ await runAsTask ( environment , options ) ;
509+
510+ // Assert - shouldUseUv was called with the project URI as the third (scope) argument
511+ assert . ok (
512+ mockShouldUseUv . calledWith ( undefined , environment . environmentPath . fsPath , projectUri ) ,
513+ 'Should pass project URI as the scope argument to shouldUseUv' ,
514+ ) ;
515+ } ) ;
516+
517+ test ( 'should pass undefined scope to shouldUseUv when project is not provided' , async ( ) => {
518+ // Mock - No project means no scope, so shouldUseUv resolves the user/global setting
519+ const environment : PythonEnvironment = {
520+ envId : { id : 'test-env' , managerId : 'test-manager' } ,
521+ name : 'Test Environment' ,
522+ displayName : 'Test Environment' ,
523+ displayPath : '/path/to/env' ,
524+ version : '3.9.0' ,
525+ environmentPath : Uri . file ( '/path/to/env' ) ,
526+ execInfo : {
527+ run : { executable : '/path/to/python' , args : [ ] } ,
528+ } ,
529+ sysPrefix : '/path/to/env' ,
530+ } ;
531+
532+ const options : PythonTaskExecutionOptions = {
533+ name : 'No-Scope Task' ,
534+ args : [ 'script.py' ] ,
535+ } ;
536+
537+ mockGetWorkspaceFolder . returns ( undefined ) ;
538+ mockShouldUseUv . resolves ( false ) ;
539+ mockQuoteStringIfNecessary . withArgs ( '/path/to/python' ) . returns ( '/path/to/python' ) ;
540+ mockExecuteTask . resolves ( { } as TaskExecution ) ;
541+
542+ // Run
543+ await runAsTask ( environment , options ) ;
544+
545+ // Assert - third argument is explicitly undefined when project is missing
546+ assert . ok (
547+ mockShouldUseUv . calledWith ( undefined , environment . environmentPath . fsPath , undefined ) ,
548+ 'Should pass undefined scope when no project is provided' ,
549+ ) ;
550+ } ) ;
551+
552+ test ( 'should fall back to run.executable in --python when activatedRun is missing under uv' , async ( ) => {
553+ // Mock - Env has only run, no activatedRun; uv mode is on
554+ const environment : PythonEnvironment = {
555+ envId : { id : 'test-env' , managerId : 'test-manager' } ,
556+ name : 'Test Environment' ,
557+ displayName : 'Test Environment' ,
558+ displayPath : '/path/to/env' ,
559+ version : '3.9.0' ,
560+ environmentPath : Uri . file ( '/path/to/env' ) ,
561+ execInfo : {
562+ run : { executable : '/path/to/python' , args : [ '-X' , 'utf8' ] } ,
563+ } ,
564+ sysPrefix : '/path/to/env' ,
565+ } ;
566+
567+ const options : PythonTaskExecutionOptions = {
568+ name : 'Fallback Run UV Task' ,
569+ args : [ 'script.py' ] ,
570+ } ;
571+
572+ mockGetWorkspaceFolder . returns ( undefined ) ;
573+ mockShouldUseUv . resolves ( true ) ;
574+ mockQuoteStringIfNecessary . withArgs ( 'uv' ) . returns ( 'uv' ) ;
575+ mockExecuteTask . resolves ( { } as TaskExecution ) ;
576+
577+ // Run
578+ await runAsTask ( environment , options ) ;
579+
580+ // Assert
581+ const taskArg = mockExecuteTask . firstCall . args [ 0 ] as Task ;
582+ const execution = taskArg . execution as ShellExecution ;
583+ assert . strictEqual ( execution . command , 'uv' , 'Should execute uv when uv mode is enabled' ) ;
584+ assert . deepStrictEqual (
585+ execution . args ,
586+ [ 'run' , '--python' , '/path/to/python' , '-X' , 'utf8' , 'script.py' ] ,
587+ 'Should use run.executable as --python value and preserve run.args' ,
588+ ) ;
589+ } ) ;
590+
591+ test ( 'should use python literal under uv when execInfo is missing' , async ( ) => {
592+ // Mock - No execInfo at all; we fall back to the literal "python" and still run via uv
593+ const environment : PythonEnvironment = {
594+ envId : { id : 'test-env' , managerId : 'test-manager' } ,
595+ name : 'Test Environment' ,
596+ displayName : 'Test Environment' ,
597+ displayPath : '/path/to/env' ,
598+ version : '3.9.0' ,
599+ environmentPath : Uri . file ( '/path/to/env' ) ,
600+ sysPrefix : '/path/to/env' ,
601+ } as PythonEnvironment ;
602+
603+ const options : PythonTaskExecutionOptions = {
604+ name : 'No ExecInfo UV Task' ,
605+ args : [ 'script.py' ] ,
606+ } ;
607+
608+ mockGetWorkspaceFolder . returns ( undefined ) ;
609+ mockShouldUseUv . resolves ( true ) ;
610+ mockQuoteStringIfNecessary . withArgs ( 'uv' ) . returns ( 'uv' ) ;
611+ mockExecuteTask . resolves ( { } as TaskExecution ) ;
612+
613+ // Run
614+ await runAsTask ( environment , options ) ;
615+
616+ // Assert - warns about missing executable AND wraps the literal "python" under uv
617+ assert . ok (
618+ mockTraceWarn . calledWith ( 'No Python executable found in environment; falling back to "python".' ) ,
619+ 'Should warn about missing executable' ,
620+ ) ;
621+ const taskArg = mockExecuteTask . firstCall . args [ 0 ] as Task ;
622+ const execution = taskArg . execution as ShellExecution ;
623+ assert . strictEqual ( execution . command , 'uv' , 'Should execute uv even when execInfo is missing' ) ;
624+ assert . deepStrictEqual (
625+ execution . args ,
626+ [ 'run' , '--python' , 'python' , 'script.py' ] ,
627+ 'Should pass the literal "python" fallback as the --python argument' ,
628+ ) ;
629+ } ) ;
630+
631+ test ( 'should preserve a Windows-style python path verbatim as --python argument under uv' , async ( ) => {
632+ // Mock - Windows backslash path; the python path now flows as a uv argument, not the executable
633+ const winPython = 'C:\\Users\\me\\.venv\\Scripts\\python.exe' ;
634+ const environment : PythonEnvironment = {
635+ envId : { id : 'test-env' , managerId : 'test-manager' } ,
636+ name : 'Test Environment' ,
637+ displayName : 'Test Environment' ,
638+ displayPath : 'C:\\Users\\me\\.venv' ,
639+ version : '3.11.0' ,
640+ environmentPath : Uri . file ( winPython ) ,
641+ execInfo : {
642+ run : { executable : winPython , args : [ ] } ,
643+ } ,
644+ sysPrefix : 'C:\\Users\\me\\.venv' ,
645+ } ;
646+
647+ const options : PythonTaskExecutionOptions = {
648+ name : 'Windows UV Task' ,
649+ args : [ 'script.py' ] ,
650+ } ;
651+
652+ mockGetWorkspaceFolder . returns ( undefined ) ;
653+ mockShouldUseUv . resolves ( true ) ;
654+ mockQuoteStringIfNecessary . withArgs ( 'uv' ) . returns ( 'uv' ) ;
655+ mockExecuteTask . resolves ( { } as TaskExecution ) ;
656+
657+ // Run
658+ await runAsTask ( environment , options ) ;
659+
660+ // Assert - the --python value matches the input path string (not quoted via quoteStringIfNecessary)
661+ const taskArg = mockExecuteTask . firstCall . args [ 0 ] as Task ;
662+ const execution = taskArg . execution as ShellExecution ;
663+ assert . strictEqual ( execution . command , 'uv' , 'Should execute uv when uv mode is enabled' ) ;
664+ assert . deepStrictEqual (
665+ execution . args ,
666+ [ 'run' , '--python' , winPython , 'script.py' ] ,
667+ 'Should preserve the Windows-style path verbatim as the --python value' ,
668+ ) ;
669+ // quoteStringIfNecessary should not be called for the python path under uv (only for the executable)
670+ assert . ok (
671+ ! mockQuoteStringIfNecessary . calledWith ( winPython ) ,
672+ 'Should not quote the python path when it is a uv argument' ,
673+ ) ;
674+ } ) ;
675+
676+ test ( 'should append user args after env activated args under uv' , async ( ) => {
677+ // Mock - Env supplies activatedRun.args; ensure ordering: run --python <py> <env-args> <user-args>
678+ const environment : PythonEnvironment = {
679+ envId : { id : 'test-env' , managerId : 'test-manager' } ,
680+ name : 'Test Environment' ,
681+ displayName : 'Test Environment' ,
682+ displayPath : '/path/to/env' ,
683+ version : '3.9.0' ,
684+ environmentPath : Uri . file ( '/path/to/env' ) ,
685+ execInfo : {
686+ run : { executable : '/path/to/python' , args : [ '--default' ] } ,
687+ activatedRun : {
688+ executable : '/activated/python' ,
689+ args : [ '-X' , 'utf8' ] ,
690+ } ,
691+ } ,
692+ sysPrefix : '/path/to/env' ,
693+ } ;
694+
695+ const options : PythonTaskExecutionOptions = {
696+ name : 'Args Order UV Task' ,
697+ args : [ 'script.py' , '--user-arg' ] ,
698+ } ;
699+
700+ mockGetWorkspaceFolder . returns ( undefined ) ;
701+ mockShouldUseUv . resolves ( true ) ;
702+ mockQuoteStringIfNecessary . withArgs ( 'uv' ) . returns ( 'uv' ) ;
703+ mockExecuteTask . resolves ( { } as TaskExecution ) ;
704+
705+ // Run
706+ await runAsTask ( environment , options ) ;
707+
708+ // Assert
709+ const taskArg = mockExecuteTask . firstCall . args [ 0 ] as Task ;
710+ const execution = taskArg . execution as ShellExecution ;
711+ assert . deepStrictEqual (
712+ execution . args ,
713+ [ 'run' , '--python' , '/activated/python' , '-X' , 'utf8' , 'script.py' , '--user-arg' ] ,
714+ 'Env activated args should sit between --python and the user args' ,
715+ ) ;
716+ } ) ;
717+
718+ test ( 'should pass user args containing flags through to python under uv (regression guard)' , async ( ) => {
719+ // Mock - The run button only ever appends a file path, but API callers can pass arbitrary args.
720+ // This guards the contract that user args land after the script positional and are NOT consumed by uv.
721+ const environment : PythonEnvironment = {
722+ envId : { id : 'test-env' , managerId : 'test-manager' } ,
723+ name : 'Test Environment' ,
724+ displayName : 'Test Environment' ,
725+ displayPath : '/path/to/env' ,
726+ version : '3.9.0' ,
727+ environmentPath : Uri . file ( '/path/to/env' ) ,
728+ execInfo : {
729+ run : { executable : '/path/to/python' , args : [ ] } ,
730+ } ,
731+ sysPrefix : '/path/to/env' ,
732+ } ;
733+
734+ const options : PythonTaskExecutionOptions = {
735+ name : 'Flag Args UV Task' ,
736+ args : [ 'script.py' , '--user-flag' , 'value' ] ,
737+ } ;
738+
739+ mockGetWorkspaceFolder . returns ( undefined ) ;
740+ mockShouldUseUv . resolves ( true ) ;
741+ mockQuoteStringIfNecessary . withArgs ( 'uv' ) . returns ( 'uv' ) ;
742+ mockExecuteTask . resolves ( { } as TaskExecution ) ;
743+
744+ // Run
745+ await runAsTask ( environment , options ) ;
746+
747+ // Assert - the user flag appears after the script path (i.e. it goes to python, not uv).
748+ const taskArg = mockExecuteTask . firstCall . args [ 0 ] as Task ;
749+ const execution = taskArg . execution as ShellExecution ;
750+ assert . deepStrictEqual (
751+ execution . args ,
752+ [ 'run' , '--python' , '/path/to/python' , 'script.py' , '--user-flag' , 'value' ] ,
753+ 'User args should be appended after --python <path> in the order provided' ,
754+ ) ;
755+ } ) ;
756+ } ) ;
757+
479758 suite ( 'Workspace Resolution' , ( ) => {
480759 test ( 'should use workspace folder when project URI is provided' , async ( ) => {
481760 // Mock - Test workspace resolution
0 commit comments