From f4e9aa721baa23f04e7e6cf7694a2098ee18b0d9 Mon Sep 17 00:00:00 2001 From: Peter Zhao Date: Sat, 14 Feb 2026 22:50:19 +0800 Subject: [PATCH] Fix defaultInterpreterPath variable expansion and stabilize interpreter selection tests --- src/features/interpreterSelection.ts | 4 +- .../interpreterSelection.unit.test.ts | 46 +++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/src/features/interpreterSelection.ts b/src/features/interpreterSelection.ts index e2aee39e..a9bc179e 100644 --- a/src/features/interpreterSelection.ts +++ b/src/features/interpreterSelection.ts @@ -6,6 +6,7 @@ import { commands, ConfigurationChangeEvent, Disposable, l10n, Uri } from 'vscod import { PythonEnvironment, PythonEnvironmentApi } from '../api'; import { SYSTEM_MANAGER_ID, VENV_MANAGER_ID } from '../common/constants'; import { traceError, traceInfo, traceVerbose, traceWarn } from '../common/logging'; +import { resolveVariables } from '../common/utils/internalVariables'; import { showWarningMessage } from '../common/window.apis'; import { getConfiguration, @@ -105,7 +106,8 @@ async function resolvePriorityChainCore( // PRIORITY 3: User-configured python.defaultInterpreterPath const userInterpreterPath = getUserConfiguredSetting('python', 'defaultInterpreterPath', scope); if (userInterpreterPath) { - const resolved = await tryResolveInterpreterPath(nativeFinder, api, userInterpreterPath, envManagers); + const expandedInterpreterPath = resolveVariables(userInterpreterPath, scope); + const resolved = await tryResolveInterpreterPath(nativeFinder, api, expandedInterpreterPath, envManagers); if (resolved) { traceVerbose(`${logPrefix} Priority 3: Using defaultInterpreterPath: ${userInterpreterPath}`); return { result: resolved, errors }; diff --git a/src/test/features/interpreterSelection.unit.test.ts b/src/test/features/interpreterSelection.unit.test.ts index 615bd167..fcb97507 100644 --- a/src/test/features/interpreterSelection.unit.test.ts +++ b/src/test/features/interpreterSelection.unit.test.ts @@ -174,6 +174,7 @@ suite('Interpreter Selection - Priority Chain', () => { // Setup: No pythonProjects[], no user-configured defaultEnvManager (returns undefined) // But there IS a user-configured defaultInterpreterPath sandbox.stub(workspaceApis, 'getConfiguration').returns(createMockConfig([]) as WorkspaceConfiguration); + sandbox.stub(workspaceApis, 'getWorkspaceFolders').returns([]); sandbox.stub(helpers, 'getUserConfiguredSetting').callsFake((section: string, key: string) => { if (section === 'python' && key === 'defaultInterpreterPath') { return '/usr/bin/python3.11'; @@ -198,6 +199,7 @@ suite('Interpreter Selection - Priority Chain', () => { suite('Priority 3: python.defaultInterpreterPath', () => { test('should use defaultInterpreterPath when set and resolvable', async () => { sandbox.stub(workspaceApis, 'getConfiguration').returns(createMockConfig([]) as WorkspaceConfiguration); + sandbox.stub(workspaceApis, 'getWorkspaceFolders').returns([]); sandbox.stub(helpers, 'getUserConfiguredSetting').callsFake((section: string, key: string) => { if (section === 'python' && key === 'defaultInterpreterPath') { return '/usr/bin/python3.11'; @@ -220,8 +222,49 @@ suite('Interpreter Selection - Priority Chain', () => { assert.strictEqual(result.environment.displayPath, '/usr/bin/python3.11'); }); + test('should resolve ${workspaceFolder} in defaultInterpreterPath before native resolution', async () => { + const workspaceUri = Uri.file('/test/workspace'); + const expandedInterpreterPath = '/test/workspace/backend/.venv/bin/python'; + const workspaceFolder = { name: 'workspace', uri: workspaceUri } as WorkspaceFolder; + + sandbox.stub(workspaceApis, 'getConfiguration').returns(createMockConfig([]) as WorkspaceConfiguration); + sandbox.stub(workspaceApis, 'getWorkspaceFolder').returns(workspaceFolder); + sandbox.stub(workspaceApis, 'getWorkspaceFolders').returns([workspaceFolder]); + sandbox.stub(helpers, 'getUserConfiguredSetting').callsFake((section: string, key: string) => { + if (section === 'python' && key === 'defaultInterpreterPath') { + return '${workspaceFolder}/backend/.venv/bin/python'; + } + return undefined; + }); + mockNativeFinder.resolve.resolves({ + executable: expandedInterpreterPath, + version: '3.11.0', + prefix: '/test/workspace/backend/.venv', + }); + mockApi.resolveEnvironment.resolves({ + ...mockVenvEnv, + displayPath: expandedInterpreterPath, + environmentPath: Uri.file(expandedInterpreterPath), + execInfo: { run: { executable: expandedInterpreterPath } }, + }); + + const result = await resolveEnvironmentByPriority( + workspaceUri, + mockEnvManagers as unknown as EnvironmentManagers, + mockProjectManager as unknown as PythonProjectManager, + mockNativeFinder as unknown as NativePythonFinder, + mockApi as unknown as PythonEnvironmentApi, + ); + + assert.strictEqual(result.source, 'defaultInterpreterPath'); + assert.ok(result.environment); + assert.strictEqual(result.environment.displayPath, expandedInterpreterPath); + assert.ok(mockNativeFinder.resolve.calledOnceWithExactly(expandedInterpreterPath)); + }); + test('should fall through to Priority 4 when defaultInterpreterPath cannot be resolved', async () => { sandbox.stub(workspaceApis, 'getConfiguration').returns(createMockConfig([]) as WorkspaceConfiguration); + sandbox.stub(workspaceApis, 'getWorkspaceFolders').returns([]); sandbox.stub(helpers, 'getUserConfiguredSetting').callsFake((section: string, key: string) => { if (section === 'python' && key === 'defaultInterpreterPath') { return '/nonexistent/python'; @@ -249,6 +292,7 @@ suite('Interpreter Selection - Priority Chain', () => { const resolvedHomebrewPath = '/opt/homebrew/bin/python3'; sandbox.stub(workspaceApis, 'getConfiguration').returns(createMockConfig([]) as WorkspaceConfiguration); + sandbox.stub(workspaceApis, 'getWorkspaceFolders').returns([]); sandbox.stub(helpers, 'getUserConfiguredSetting').callsFake((section: string, key: string) => { if (section === 'python' && key === 'defaultInterpreterPath') { return userPyenvPath; @@ -371,6 +415,7 @@ suite('Interpreter Selection - Priority Chain', () => { suite('Edge Cases', () => { test('should fall through when nativeFinder resolves but returns no executable', async () => { sandbox.stub(workspaceApis, 'getConfiguration').returns(createMockConfig([]) as WorkspaceConfiguration); + sandbox.stub(workspaceApis, 'getWorkspaceFolders').returns([]); sandbox.stub(helpers, 'getUserConfiguredSetting').callsFake((section: string, key: string) => { if (section === 'python' && key === 'defaultInterpreterPath') { return '/some/path/python'; @@ -395,6 +440,7 @@ suite('Interpreter Selection - Priority Chain', () => { test('should fall through when api.resolveEnvironment returns undefined', async () => { sandbox.stub(workspaceApis, 'getConfiguration').returns(createMockConfig([]) as WorkspaceConfiguration); + sandbox.stub(workspaceApis, 'getWorkspaceFolders').returns([]); sandbox.stub(helpers, 'getUserConfiguredSetting').callsFake((section: string, key: string) => { if (section === 'python' && key === 'defaultInterpreterPath') { return '/usr/bin/python3.11';