From 349866d9add5b2cf4d22c931cd82f4de4500ea23 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 27 Apr 2026 22:26:42 +0000 Subject: [PATCH 01/10] feat: map run button to uv run Agent-Logs-Url: https://github.com/microsoft/vscode-python-environments/sessions/5f628821-9c7b-48a6-9f5f-9fccb9a33bf6 Co-authored-by: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> --- package.nls.json | 2 +- src/features/execution/runAsTask.ts | 13 ++++- .../features/execution/runAsTask.unit.test.ts | 58 +++++++++++++++++++ 3 files changed, 70 insertions(+), 3 deletions(-) diff --git a/package.nls.json b/package.nls.json index 3a4ddcec..7e9f84dc 100644 --- a/package.nls.json +++ b/package.nls.json @@ -45,5 +45,5 @@ "python-envs.revealProjectInExplorer.title": "Reveal Project in Explorer", "python-envs.revealEnvInManagerView.title": "Reveal in Environment Managers View", "python-envs.runPetInTerminal.title": "Run Python Environment Tool (PET) in Terminal...", - "python-envs.alwaysUseUv.description": "When set to true, uv will be used to manage all virtual environments if available. When set to false, uv will only manage virtual environments explicitly created by uv." + "python-envs.alwaysUseUv.description": "When set to true, uv will be used to manage all virtual environments if available, and the run button will execute files with uv run. When set to false, uv will only manage environments explicitly created by uv, including for the run button." } diff --git a/src/features/execution/runAsTask.ts b/src/features/execution/runAsTask.ts index 6b7e14e4..6322f7a1 100644 --- a/src/features/execution/runAsTask.ts +++ b/src/features/execution/runAsTask.ts @@ -12,6 +12,7 @@ import { PythonEnvironment, PythonTaskExecutionOptions } from '../../api'; import { traceInfo, traceWarn } from '../../common/logging'; import { executeTask } from '../../common/tasks.apis'; import { getWorkspaceFolder } from '../../common/workspace.apis'; +import { shouldUseUv } from '../../managers/builtin/helpers'; import { quoteStringIfNecessary } from './execUtils'; function getWorkspaceFolderOrDefault(uri?: Uri): WorkspaceFolder | TaskScope { @@ -31,11 +32,19 @@ export async function runAsTask( traceWarn('No Python executable found in environment; falling back to "python".'); executable = 'python'; } - // Check and quote the executable path if necessary - executable = quoteStringIfNecessary(executable); const args = environment.execInfo?.activatedRun?.args ?? environment.execInfo?.run.args ?? []; const allArgs = [...args, ...options.args]; + const useUv = await shouldUseUv(undefined, environment.environmentPath.fsPath); + + if (useUv) { + allArgs.unshift('--python', executable); + allArgs.unshift('run'); + executable = 'uv'; + } + + // Check and quote the executable path if necessary + executable = quoteStringIfNecessary(executable); traceInfo(`Running as task: ${executable} ${allArgs.join(' ')}`); const task = new Task( diff --git a/src/test/features/execution/runAsTask.unit.test.ts b/src/test/features/execution/runAsTask.unit.test.ts index dbb58164..3829abb4 100644 --- a/src/test/features/execution/runAsTask.unit.test.ts +++ b/src/test/features/execution/runAsTask.unit.test.ts @@ -7,6 +7,7 @@ import * as tasksApi from '../../../common/tasks.apis'; import * as workspaceApis from '../../../common/workspace.apis'; import * as execUtils from '../../../features/execution/execUtils'; import { runAsTask } from '../../../features/execution/runAsTask'; +import * as builtinHelpers from '../../../managers/builtin/helpers'; suite('runAsTask Tests', () => { let mockTraceInfo: sinon.SinonStub; @@ -14,6 +15,7 @@ suite('runAsTask Tests', () => { let mockExecuteTask: sinon.SinonStub; let mockGetWorkspaceFolder: sinon.SinonStub; let mockQuoteStringIfNecessary: sinon.SinonStub; + let mockShouldUseUv: sinon.SinonStub; setup(() => { mockTraceInfo = sinon.stub(logging, 'traceInfo'); @@ -21,6 +23,7 @@ suite('runAsTask Tests', () => { mockExecuteTask = sinon.stub(tasksApi, 'executeTask'); mockGetWorkspaceFolder = sinon.stub(workspaceApis, 'getWorkspaceFolder'); mockQuoteStringIfNecessary = sinon.stub(execUtils, 'quoteStringIfNecessary'); + mockShouldUseUv = sinon.stub(builtinHelpers, 'shouldUseUv').resolves(false); }); teardown(() => { @@ -113,6 +116,61 @@ suite('runAsTask Tests', () => { assert.ok(mockTraceWarn.notCalled, 'Should not log warnings for valid environment'); }); + test('should use uv run when uv mode applies', async () => { + const environment: PythonEnvironment = { + envId: { id: 'test-env', managerId: 'test-manager' }, + name: 'Test Environment', + displayName: 'Test Environment', + shortDisplayName: 'TestEnv', + displayPath: '/path/to/env', + version: '3.9.0', + environmentPath: Uri.file('/path/to/env'), + execInfo: { + run: { + executable: '/path/to/python', + args: ['--default'], + }, + activatedRun: { + executable: '/activated/python', + args: ['--activated'], + }, + }, + sysPrefix: '/path/to/env', + }; + + const options: PythonTaskExecutionOptions = { + name: 'UV Task', + args: ['script.py', '--arg1'], + }; + + const mockTaskExecution = {} as TaskExecution; + + mockGetWorkspaceFolder.returns(undefined); + mockShouldUseUv.withArgs(undefined, environment.environmentPath.fsPath).resolves(true); + mockQuoteStringIfNecessary.withArgs('uv').returns('uv'); + mockExecuteTask.resolves(mockTaskExecution); + + const result = await runAsTask(environment, options); + + assert.strictEqual(result, mockTaskExecution, 'Should return the task execution result'); + + const taskArg = mockExecuteTask.firstCall.args[0] as Task; + const execution = taskArg.execution as ShellExecution; + + assert.strictEqual(execution.command, 'uv', 'Should execute uv when uv mode is enabled'); + assert.deepStrictEqual( + execution.args, + ['run', '--python', '/activated/python', '--activated', 'script.py', '--arg1'], + 'Should prepend uv run arguments before the file arguments', + ); + assert.ok( + mockTraceInfo.calledWith( + sinon.match(/Running as task: uv run --python \/activated\/python --activated script\.py --arg1/), + ), + 'Should log the uv run command', + ); + }); + test('should create and execute task with regular run configuration when no activatedRun', async () => { // Mock - Environment without activatedRun const environment: PythonEnvironment = { From f517bd9f69b97c594670b9408d443bb2d4e4e695 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 27 Apr 2026 22:27:13 +0000 Subject: [PATCH 02/10] fix: import shell execution test type Agent-Logs-Url: https://github.com/microsoft/vscode-python-environments/sessions/5f628821-9c7b-48a6-9f5f-9fccb9a33bf6 Co-authored-by: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> --- src/test/features/execution/runAsTask.unit.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/features/execution/runAsTask.unit.test.ts b/src/test/features/execution/runAsTask.unit.test.ts index 3829abb4..10152436 100644 --- a/src/test/features/execution/runAsTask.unit.test.ts +++ b/src/test/features/execution/runAsTask.unit.test.ts @@ -1,6 +1,6 @@ import * as assert from 'assert'; import * as sinon from 'sinon'; -import { Task, TaskExecution, TaskPanelKind, TaskRevealKind, TaskScope, Uri, WorkspaceFolder } from 'vscode'; +import { ShellExecution, Task, TaskExecution, TaskPanelKind, TaskRevealKind, TaskScope, Uri, WorkspaceFolder } from 'vscode'; import { PythonEnvironment, PythonTaskExecutionOptions } from '../../../api'; import * as logging from '../../../common/logging'; import * as tasksApi from '../../../common/tasks.apis'; From b3f86673d8445cb596d40f578a0e3e3ad9031723 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 27 Apr 2026 22:29:09 +0000 Subject: [PATCH 03/10] fix: scope uv run setting lookup Agent-Logs-Url: https://github.com/microsoft/vscode-python-environments/sessions/5f628821-9c7b-48a6-9f5f-9fccb9a33bf6 Co-authored-by: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> --- src/features/execution/runAsTask.ts | 2 +- src/managers/builtin/helpers.ts | 6 +-- .../features/execution/runAsTask.unit.test.ts | 47 +++++++++++++++++++ 3 files changed, 51 insertions(+), 4 deletions(-) diff --git a/src/features/execution/runAsTask.ts b/src/features/execution/runAsTask.ts index 6322f7a1..d8b83390 100644 --- a/src/features/execution/runAsTask.ts +++ b/src/features/execution/runAsTask.ts @@ -35,7 +35,7 @@ export async function runAsTask( const args = environment.execInfo?.activatedRun?.args ?? environment.execInfo?.run.args ?? []; const allArgs = [...args, ...options.args]; - const useUv = await shouldUseUv(undefined, environment.environmentPath.fsPath); + const useUv = await shouldUseUv(undefined, environment.environmentPath.fsPath, options.project?.uri); if (useUv) { allArgs.unshift('--python', executable); diff --git a/src/managers/builtin/helpers.ts b/src/managers/builtin/helpers.ts index 911bc603..06d430c5 100644 --- a/src/managers/builtin/helpers.ts +++ b/src/managers/builtin/helpers.ts @@ -1,4 +1,4 @@ -import { CancellationError, CancellationToken, LogOutputChannel } from 'vscode'; +import { CancellationError, CancellationToken, ConfigurationScope, LogOutputChannel } from 'vscode'; import { spawnProcess } from '../../common/childProcess.apis'; import { EventNames } from '../../common/telemetry/constants'; import { sendTelemetryEvent } from '../../common/telemetry/sender'; @@ -40,7 +40,7 @@ export async function isUvInstalled(log?: LogOutputChannel): Promise { * @param envPath - Optional environment path to check against UV environments list * @returns True if uv should be used, false otherwise. For UV environments, returns true if uv is installed. For other environments, checks the 'python-envs.alwaysUseUv' setting and uv availability. */ -export async function shouldUseUv(log?: LogOutputChannel, envPath?: string): Promise { +export async function shouldUseUv(log?: LogOutputChannel, envPath?: string, scope?: ConfigurationScope): Promise { if (envPath) { // always use uv if the given environment is stored as a uv env const uvEnvs = await getUvEnvironments(); @@ -50,7 +50,7 @@ export async function shouldUseUv(log?: LogOutputChannel, envPath?: string): Pro } // For other environments, check the user setting - const config = getConfiguration('python-envs'); + const config = getConfiguration('python-envs', scope); const alwaysUseUv = config.get('alwaysUseUv', true); if (alwaysUseUv) { diff --git a/src/test/features/execution/runAsTask.unit.test.ts b/src/test/features/execution/runAsTask.unit.test.ts index 10152436..60c104a0 100644 --- a/src/test/features/execution/runAsTask.unit.test.ts +++ b/src/test/features/execution/runAsTask.unit.test.ts @@ -171,6 +171,48 @@ suite('runAsTask Tests', () => { ); }); + test('should quote uv executable when needed', async () => { + const environment: PythonEnvironment = { + envId: { id: 'test-env', managerId: 'test-manager' }, + name: 'Test Environment', + displayName: 'Test Environment', + displayPath: '/path/to/env', + version: '3.9.0', + environmentPath: Uri.file('/path/to/env'), + execInfo: { + run: { + executable: '/path/to/python', + args: [], + }, + }, + sysPrefix: '/path/to/env', + }; + + const options: PythonTaskExecutionOptions = { + name: 'Quoted UV Task', + args: ['script.py'], + }; + + const mockTaskExecution = {} as TaskExecution; + + mockGetWorkspaceFolder.returns(undefined); + mockShouldUseUv.withArgs(undefined, environment.environmentPath.fsPath, undefined).resolves(true); + mockQuoteStringIfNecessary.withArgs('uv').returns('"uv"'); + mockExecuteTask.resolves(mockTaskExecution); + + await runAsTask(environment, options); + + const taskArg = mockExecuteTask.firstCall.args[0] as Task; + const execution = taskArg.execution as ShellExecution; + + assert.strictEqual(execution.command, '"uv"', 'Should quote the uv executable when required'); + assert.deepStrictEqual( + execution.args, + ['run', '--python', '/path/to/python', 'script.py'], + 'Should preserve uv arguments when quoting the executable', + ); + }); + test('should create and execute task with regular run configuration when no activatedRun', async () => { // Mock - Environment without activatedRun const environment: PythonEnvironment = { @@ -197,6 +239,7 @@ suite('runAsTask Tests', () => { const mockTaskExecution = {} as TaskExecution; mockGetWorkspaceFolder.withArgs(undefined).returns(undefined); + mockShouldUseUv.withArgs(undefined, environment.environmentPath.fsPath, undefined).resolves(false); mockQuoteStringIfNecessary.withArgs('/path/to/python').returns('/path/to/python'); mockExecuteTask.resolves(mockTaskExecution); @@ -214,6 +257,10 @@ suite('runAsTask Tests', () => { mockTraceInfo.calledWith(sinon.match(/Running as task: \/path\/to\/python --default-arg test\.py/)), 'Should log execution with run args', ); + const taskArg = mockExecuteTask.firstCall.args[0] as Task; + const execution = taskArg.execution as ShellExecution; + assert.strictEqual(execution.command, '/path/to/python', 'Should keep the python executable when uv is off'); + assert.deepStrictEqual(execution.args, ['--default-arg', 'test.py'], 'Should keep the non-uv arguments'); }); test('should handle custom reveal option', async () => { From b51423a38d3a04dfd86a6b0c88d8408ce6e9c53c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 27 Apr 2026 22:29:33 +0000 Subject: [PATCH 04/10] fix: remove duplicate task variable Agent-Logs-Url: https://github.com/microsoft/vscode-python-environments/sessions/5f628821-9c7b-48a6-9f5f-9fccb9a33bf6 Co-authored-by: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> --- src/test/features/execution/runAsTask.unit.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/features/execution/runAsTask.unit.test.ts b/src/test/features/execution/runAsTask.unit.test.ts index 60c104a0..ebd95429 100644 --- a/src/test/features/execution/runAsTask.unit.test.ts +++ b/src/test/features/execution/runAsTask.unit.test.ts @@ -154,7 +154,6 @@ suite('runAsTask Tests', () => { assert.strictEqual(result, mockTaskExecution, 'Should return the task execution result'); - const taskArg = mockExecuteTask.firstCall.args[0] as Task; const execution = taskArg.execution as ShellExecution; assert.strictEqual(execution.command, 'uv', 'Should execute uv when uv mode is enabled'); From 8b5c11a812c6ec58ca68b7acdf06d7a9b44f013c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 27 Apr 2026 22:30:02 +0000 Subject: [PATCH 05/10] fix: correct uv task test assertions Agent-Logs-Url: https://github.com/microsoft/vscode-python-environments/sessions/5f628821-9c7b-48a6-9f5f-9fccb9a33bf6 Co-authored-by: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> --- src/test/features/execution/runAsTask.unit.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/features/execution/runAsTask.unit.test.ts b/src/test/features/execution/runAsTask.unit.test.ts index ebd95429..4eca5dc2 100644 --- a/src/test/features/execution/runAsTask.unit.test.ts +++ b/src/test/features/execution/runAsTask.unit.test.ts @@ -154,6 +154,7 @@ suite('runAsTask Tests', () => { assert.strictEqual(result, mockTaskExecution, 'Should return the task execution result'); + const taskArg = mockExecuteTask.firstCall.args[0] as Task; const execution = taskArg.execution as ShellExecution; assert.strictEqual(execution.command, 'uv', 'Should execute uv when uv mode is enabled'); @@ -201,7 +202,6 @@ suite('runAsTask Tests', () => { await runAsTask(environment, options); - const taskArg = mockExecuteTask.firstCall.args[0] as Task; const execution = taskArg.execution as ShellExecution; assert.strictEqual(execution.command, '"uv"', 'Should quote the uv executable when required'); From d8ecfda995c9681b5206fd6478a7662dd7e24cb5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 27 Apr 2026 22:30:27 +0000 Subject: [PATCH 06/10] fix: finalize uv run task tests Agent-Logs-Url: https://github.com/microsoft/vscode-python-environments/sessions/5f628821-9c7b-48a6-9f5f-9fccb9a33bf6 Co-authored-by: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> --- src/test/features/execution/runAsTask.unit.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/features/execution/runAsTask.unit.test.ts b/src/test/features/execution/runAsTask.unit.test.ts index 4eca5dc2..de34ba78 100644 --- a/src/test/features/execution/runAsTask.unit.test.ts +++ b/src/test/features/execution/runAsTask.unit.test.ts @@ -202,6 +202,7 @@ suite('runAsTask Tests', () => { await runAsTask(environment, options); + const taskArg = mockExecuteTask.firstCall.args[0] as Task; const execution = taskArg.execution as ShellExecution; assert.strictEqual(execution.command, '"uv"', 'Should quote the uv executable when required'); @@ -256,7 +257,6 @@ suite('runAsTask Tests', () => { mockTraceInfo.calledWith(sinon.match(/Running as task: \/path\/to\/python --default-arg test\.py/)), 'Should log execution with run args', ); - const taskArg = mockExecuteTask.firstCall.args[0] as Task; const execution = taskArg.execution as ShellExecution; assert.strictEqual(execution.command, '/path/to/python', 'Should keep the python executable when uv is off'); assert.deepStrictEqual(execution.args, ['--default-arg', 'test.py'], 'Should keep the non-uv arguments'); From c0172ddcb744845415c704713829d5dc8210d34e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Apr 2026 03:42:20 +0000 Subject: [PATCH 07/10] test: add run-button uv-mode test cases Agent-Logs-Url: https://github.com/microsoft/vscode-python-environments/sessions/ee61d4e1-336b-405a-93dc-f691bd210c7e Co-authored-by: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> --- .../features/execution/runAsTask.unit.test.ts | 279 ++++++++++++++++++ 1 file changed, 279 insertions(+) diff --git a/src/test/features/execution/runAsTask.unit.test.ts b/src/test/features/execution/runAsTask.unit.test.ts index de34ba78..86071409 100644 --- a/src/test/features/execution/runAsTask.unit.test.ts +++ b/src/test/features/execution/runAsTask.unit.test.ts @@ -476,6 +476,285 @@ suite('runAsTask Tests', () => { }); }); + suite('UV Mode Scenarios', () => { + test('should pass project URI as scope to shouldUseUv', async () => { + // Mock - Verify per-folder setting precedence by passing project.uri as the scope + const projectUri = Uri.file('/workspace/project'); + const environment: PythonEnvironment = { + envId: { id: 'test-env', managerId: 'test-manager' }, + name: 'Test Environment', + displayName: 'Test Environment', + displayPath: '/path/to/env', + version: '3.9.0', + environmentPath: Uri.file('/path/to/env'), + execInfo: { + run: { executable: '/path/to/python', args: [] }, + }, + sysPrefix: '/path/to/env', + }; + + const options: PythonTaskExecutionOptions = { + name: 'Scoped Task', + args: ['script.py'], + project: { name: 'Test Project', uri: projectUri }, + }; + + mockGetWorkspaceFolder.withArgs(projectUri).returns(undefined); + mockShouldUseUv.resolves(false); + mockQuoteStringIfNecessary.withArgs('/path/to/python').returns('/path/to/python'); + mockExecuteTask.resolves({} as TaskExecution); + + // Run + await runAsTask(environment, options); + + // Assert - shouldUseUv was called with the project URI as the third (scope) argument + assert.ok( + mockShouldUseUv.calledWith(undefined, environment.environmentPath.fsPath, projectUri), + 'Should pass project URI as the scope argument to shouldUseUv', + ); + }); + + test('should pass undefined scope to shouldUseUv when project is not provided', async () => { + // Mock - No project means no scope, so shouldUseUv resolves the user/global setting + const environment: PythonEnvironment = { + envId: { id: 'test-env', managerId: 'test-manager' }, + name: 'Test Environment', + displayName: 'Test Environment', + displayPath: '/path/to/env', + version: '3.9.0', + environmentPath: Uri.file('/path/to/env'), + execInfo: { + run: { executable: '/path/to/python', args: [] }, + }, + sysPrefix: '/path/to/env', + }; + + const options: PythonTaskExecutionOptions = { + name: 'No-Scope Task', + args: ['script.py'], + }; + + mockGetWorkspaceFolder.returns(undefined); + mockShouldUseUv.resolves(false); + mockQuoteStringIfNecessary.withArgs('/path/to/python').returns('/path/to/python'); + mockExecuteTask.resolves({} as TaskExecution); + + // Run + await runAsTask(environment, options); + + // Assert - third argument is explicitly undefined when project is missing + assert.ok( + mockShouldUseUv.calledWith(undefined, environment.environmentPath.fsPath, undefined), + 'Should pass undefined scope when no project is provided', + ); + }); + + test('should fall back to run.executable in --python when activatedRun is missing under uv', async () => { + // Mock - Env has only run, no activatedRun; uv mode is on + const environment: PythonEnvironment = { + envId: { id: 'test-env', managerId: 'test-manager' }, + name: 'Test Environment', + displayName: 'Test Environment', + displayPath: '/path/to/env', + version: '3.9.0', + environmentPath: Uri.file('/path/to/env'), + execInfo: { + run: { executable: '/path/to/python', args: ['-X', 'utf8'] }, + }, + sysPrefix: '/path/to/env', + }; + + const options: PythonTaskExecutionOptions = { + name: 'Fallback Run UV Task', + args: ['script.py'], + }; + + mockGetWorkspaceFolder.returns(undefined); + mockShouldUseUv.resolves(true); + mockQuoteStringIfNecessary.withArgs('uv').returns('uv'); + mockExecuteTask.resolves({} as TaskExecution); + + // Run + await runAsTask(environment, options); + + // Assert + const taskArg = mockExecuteTask.firstCall.args[0] as Task; + const execution = taskArg.execution as ShellExecution; + assert.strictEqual(execution.command, 'uv', 'Should execute uv when uv mode is enabled'); + assert.deepStrictEqual( + execution.args, + ['run', '--python', '/path/to/python', '-X', 'utf8', 'script.py'], + 'Should use run.executable as --python value and preserve run.args', + ); + }); + + test('should use python literal under uv when execInfo is missing', async () => { + // Mock - No execInfo at all; we fall back to the literal "python" and still run via uv + const environment: PythonEnvironment = { + envId: { id: 'test-env', managerId: 'test-manager' }, + name: 'Test Environment', + displayName: 'Test Environment', + displayPath: '/path/to/env', + version: '3.9.0', + environmentPath: Uri.file('/path/to/env'), + sysPrefix: '/path/to/env', + } as PythonEnvironment; + + const options: PythonTaskExecutionOptions = { + name: 'No ExecInfo UV Task', + args: ['script.py'], + }; + + mockGetWorkspaceFolder.returns(undefined); + mockShouldUseUv.resolves(true); + mockQuoteStringIfNecessary.withArgs('uv').returns('uv'); + mockExecuteTask.resolves({} as TaskExecution); + + // Run + await runAsTask(environment, options); + + // Assert - warns about missing executable AND wraps the literal "python" under uv + assert.ok( + mockTraceWarn.calledWith('No Python executable found in environment; falling back to "python".'), + 'Should warn about missing executable', + ); + const taskArg = mockExecuteTask.firstCall.args[0] as Task; + const execution = taskArg.execution as ShellExecution; + assert.strictEqual(execution.command, 'uv', 'Should execute uv even when execInfo is missing'); + assert.deepStrictEqual( + execution.args, + ['run', '--python', 'python', 'script.py'], + 'Should pass the literal "python" fallback as the --python argument', + ); + }); + + test('should preserve a Windows-style python path verbatim as --python argument under uv', async () => { + // Mock - Windows backslash path; the python path now flows as a uv argument, not the executable + const winPython = 'C:\\Users\\me\\.venv\\Scripts\\python.exe'; + const environment: PythonEnvironment = { + envId: { id: 'test-env', managerId: 'test-manager' }, + name: 'Test Environment', + displayName: 'Test Environment', + displayPath: 'C:\\Users\\me\\.venv', + version: '3.11.0', + environmentPath: Uri.file(winPython), + execInfo: { + run: { executable: winPython, args: [] }, + }, + sysPrefix: 'C:\\Users\\me\\.venv', + }; + + const options: PythonTaskExecutionOptions = { + name: 'Windows UV Task', + args: ['script.py'], + }; + + mockGetWorkspaceFolder.returns(undefined); + mockShouldUseUv.resolves(true); + mockQuoteStringIfNecessary.withArgs('uv').returns('uv'); + mockExecuteTask.resolves({} as TaskExecution); + + // Run + await runAsTask(environment, options); + + // Assert - the --python value matches the input path string (not quoted via quoteStringIfNecessary) + const taskArg = mockExecuteTask.firstCall.args[0] as Task; + const execution = taskArg.execution as ShellExecution; + assert.strictEqual(execution.command, 'uv', 'Should execute uv when uv mode is enabled'); + assert.deepStrictEqual( + execution.args, + ['run', '--python', winPython, 'script.py'], + 'Should preserve the Windows-style path verbatim as the --python value', + ); + // quoteStringIfNecessary should not be called for the python path under uv (only for the executable) + assert.ok( + !mockQuoteStringIfNecessary.calledWith(winPython), + 'Should not quote the python path when it is a uv argument', + ); + }); + + test('should append user args after env activated args under uv', async () => { + // Mock - Env supplies activatedRun.args; ensure ordering: run --python + const environment: PythonEnvironment = { + envId: { id: 'test-env', managerId: 'test-manager' }, + name: 'Test Environment', + displayName: 'Test Environment', + displayPath: '/path/to/env', + version: '3.9.0', + environmentPath: Uri.file('/path/to/env'), + execInfo: { + run: { executable: '/path/to/python', args: ['--default'] }, + activatedRun: { + executable: '/activated/python', + args: ['-X', 'utf8'], + }, + }, + sysPrefix: '/path/to/env', + }; + + const options: PythonTaskExecutionOptions = { + name: 'Args Order UV Task', + args: ['script.py', '--user-arg'], + }; + + mockGetWorkspaceFolder.returns(undefined); + mockShouldUseUv.resolves(true); + mockQuoteStringIfNecessary.withArgs('uv').returns('uv'); + mockExecuteTask.resolves({} as TaskExecution); + + // Run + await runAsTask(environment, options); + + // Assert + const taskArg = mockExecuteTask.firstCall.args[0] as Task; + const execution = taskArg.execution as ShellExecution; + assert.deepStrictEqual( + execution.args, + ['run', '--python', '/activated/python', '-X', 'utf8', 'script.py', '--user-arg'], + 'Env activated args should sit between --python and the user args', + ); + }); + + test('should pass user args containing flags through to python under uv (regression guard)', async () => { + // Mock - The run button only ever appends a file path, but API callers can pass arbitrary args. + // This guards the contract that user args land after the script positional and are NOT consumed by uv. + const environment: PythonEnvironment = { + envId: { id: 'test-env', managerId: 'test-manager' }, + name: 'Test Environment', + displayName: 'Test Environment', + displayPath: '/path/to/env', + version: '3.9.0', + environmentPath: Uri.file('/path/to/env'), + execInfo: { + run: { executable: '/path/to/python', args: [] }, + }, + sysPrefix: '/path/to/env', + }; + + const options: PythonTaskExecutionOptions = { + name: 'Flag Args UV Task', + args: ['script.py', '--user-flag', 'value'], + }; + + mockGetWorkspaceFolder.returns(undefined); + mockShouldUseUv.resolves(true); + mockQuoteStringIfNecessary.withArgs('uv').returns('uv'); + mockExecuteTask.resolves({} as TaskExecution); + + // Run + await runAsTask(environment, options); + + // Assert - the user flag appears after the script path (i.e. it goes to python, not uv). + const taskArg = mockExecuteTask.firstCall.args[0] as Task; + const execution = taskArg.execution as ShellExecution; + assert.deepStrictEqual( + execution.args, + ['run', '--python', '/path/to/python', 'script.py', '--user-flag', 'value'], + 'User args should be appended after --python in the order provided', + ); + }); + }); + suite('Workspace Resolution', () => { test('should use workspace folder when project URI is provided', async () => { // Mock - Test workspace resolution From 215118350ca52a47edc216ef883554ad72229fa7 Mon Sep 17 00:00:00 2001 From: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Tue, 28 Apr 2026 20:14:18 -0700 Subject: [PATCH 08/10] Address Copilot review feedback - Strip surrounding quotes from executable before passing as --python to uv; quoting the argument value causes uv to fail to resolve the interpreter (mirrors the same fix in runInBackground.ts) - Add regression test: quoted python path under uv mode is unquoted - Update shouldUseUv JSDoc to document the new scope parameter Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/features/execution/runAsTask.ts | 9 +++- src/managers/builtin/helpers.ts | 12 ++++-- .../features/execution/runAsTask.unit.test.ts | 43 +++++++++++++++++++ 3 files changed, 60 insertions(+), 4 deletions(-) diff --git a/src/features/execution/runAsTask.ts b/src/features/execution/runAsTask.ts index d8b83390..69b5a9f2 100644 --- a/src/features/execution/runAsTask.ts +++ b/src/features/execution/runAsTask.ts @@ -38,7 +38,14 @@ export async function runAsTask( const useUv = await shouldUseUv(undefined, environment.environmentPath.fsPath, options.project?.uri); if (useUv) { - allArgs.unshift('--python', executable); + // Strip surrounding quotes before passing as --python value; uv receives the raw path + // and shell-quoting the argument value causes it to fail to resolve the interpreter. + // (cf. runInBackground.ts which strips quotes for the same reason before spawn) + let pythonArg = executable; + if (pythonArg.startsWith('"') && pythonArg.endsWith('"')) { + pythonArg = pythonArg.substring(1, pythonArg.length - 1); + } + allArgs.unshift('--python', pythonArg); allArgs.unshift('run'); executable = 'uv'; } diff --git a/src/managers/builtin/helpers.ts b/src/managers/builtin/helpers.ts index 06d430c5..f0069f37 100644 --- a/src/managers/builtin/helpers.ts +++ b/src/managers/builtin/helpers.ts @@ -36,9 +36,15 @@ export async function isUvInstalled(log?: LogOutputChannel): Promise { /** * Determines if uv should be used for managing a virtual environment. - * @param log - Optional log output channel for logging operations - * @param envPath - Optional environment path to check against UV environments list - * @returns True if uv should be used, false otherwise. For UV environments, returns true if uv is installed. For other environments, checks the 'python-envs.alwaysUseUv' setting and uv availability. + * @param log - Optional log output channel for logging operations. + * @param envPath - Optional environment path to check against the known uv environments list. + * @param scope - Optional configuration scope used when reading the `python-envs.alwaysUseUv` setting. + * Pass the relevant project or workspace-folder `Uri` when available so VS Code resolves settings + * using normal precedence: workspace folder, then workspace, then user/global. If omitted, the + * user/global value is used unless VS Code can infer a broader scope. + * @returns True if uv should be used, false otherwise. For uv-managed environments, returns true + * if uv is installed. For other environments, checks the `python-envs.alwaysUseUv` setting for + * the provided scope and uv availability. */ export async function shouldUseUv(log?: LogOutputChannel, envPath?: string, scope?: ConfigurationScope): Promise { if (envPath) { diff --git a/src/test/features/execution/runAsTask.unit.test.ts b/src/test/features/execution/runAsTask.unit.test.ts index 86071409..91c2fbad 100644 --- a/src/test/features/execution/runAsTask.unit.test.ts +++ b/src/test/features/execution/runAsTask.unit.test.ts @@ -715,6 +715,49 @@ suite('runAsTask Tests', () => { ); }); + test('should strip surrounding quotes from executable before passing as --python to uv (regression guard)', async () => { + // Mock - executable is already quoted (e.g. from a shell-escape step or a provider that returns + // a quoted path). uv must receive the raw path without the surrounding quotes or it fails to + // locate the interpreter. + const quotedPython = '"C:\\Program Files\\Python311\\python.exe"'; + const unquotedPython = 'C:\\Program Files\\Python311\\python.exe'; + const environment: PythonEnvironment = { + envId: { id: 'test-env', managerId: 'test-manager' }, + name: 'Test Environment', + displayName: 'Test Environment', + displayPath: 'C:\\Program Files\\Python311', + version: '3.11.0', + environmentPath: Uri.file(unquotedPython), + execInfo: { + run: { executable: quotedPython, args: [] }, + }, + sysPrefix: 'C:\\Program Files\\Python311', + }; + + const options: PythonTaskExecutionOptions = { + name: 'Quoted Python UV Task', + args: ['script.py'], + }; + + mockGetWorkspaceFolder.returns(undefined); + mockShouldUseUv.resolves(true); + mockQuoteStringIfNecessary.withArgs('uv').returns('uv'); + mockExecuteTask.resolves({} as TaskExecution); + + // Run + await runAsTask(environment, options); + + // Assert - the --python value must be the unquoted path so uv can resolve it + const taskArg = mockExecuteTask.firstCall.args[0] as Task; + const execution = taskArg.execution as ShellExecution; + assert.strictEqual(execution.command, 'uv', 'Should execute uv when uv mode is enabled'); + assert.deepStrictEqual( + execution.args, + ['run', '--python', unquotedPython, 'script.py'], + 'Should strip surrounding quotes from the python path before passing to uv --python', + ); + }); + test('should pass user args containing flags through to python under uv (regression guard)', async () => { // Mock - The run button only ever appends a file path, but API callers can pass arbitrary args. // This guards the contract that user args land after the script positional and are NOT consumed by uv. From a01a406de78d62b301c837d97186bed7041c4e8d Mon Sep 17 00:00:00 2001 From: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Tue, 28 Apr 2026 20:20:28 -0700 Subject: [PATCH 09/10] Add PEP 723 inline script detection for uv run button When the run button is clicked on a PEP 723 script (one containing a `# /// script` block), uv manages the interpreter and dependencies entirely. We must not pass `--python` in that case or it would override the script's own requirements. Detection logic: - New `isPep723Script(filePath)` helper in pep723.ts reads the script and checks for the PEP 723 opening marker `# /// script` - In runAsTask, when uv mode is active, inspect options.args[0] (the script path) before building the command: `uv run