From 8eeab7d066f916f245e80c0e7a3fd32962dae1b8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Jun 2026 16:56:03 +0000 Subject: [PATCH 1/4] Initial plan From 2a5b55cb723f78c743c6808c421112630ebfae27 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Jun 2026 17:02:11 +0000 Subject: [PATCH 2/4] Plan: fix ${workspaceFolder} resolution and path duplication for defaultInterpreterPath --- package-lock.json | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1ac3bcbf..d3e775a1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1296,7 +1296,6 @@ "integrity": "sha512-D7DbgGFtsqIPIFMPJwCad9Gfi/hC0PWErRRHFnaCWoEDYi5tQUDiJCTmGUbBiLzjqAck4KcXt9Ayj0CNlIrF+w==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.16.0", "@typescript-eslint/types": "8.16.0", @@ -2150,7 +2149,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2476,7 +2474,6 @@ "url": "https://github.com/sponsors/ai" } ], - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -3357,7 +3354,6 @@ "integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -6135,7 +6131,6 @@ "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -7003,7 +6998,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", "dev": true, - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -7163,7 +7157,6 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.0.tgz", "integrity": "sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw==", "dev": true, - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -7212,7 +7205,6 @@ "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", "dev": true, - "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^2.1.1", @@ -8436,7 +8428,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.16.0.tgz", "integrity": "sha512-D7DbgGFtsqIPIFMPJwCad9Gfi/hC0PWErRRHFnaCWoEDYi5tQUDiJCTmGUbBiLzjqAck4KcXt9Ayj0CNlIrF+w==", "dev": true, - "peer": true, "requires": { "@typescript-eslint/scope-manager": "8.16.0", "@typescript-eslint/types": "8.16.0", @@ -9025,8 +9016,7 @@ "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true, - "peer": true + "dev": true }, "acorn-import-phases": { "version": "1.0.4", @@ -9249,7 +9239,6 @@ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", "dev": true, - "peer": true, "requires": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -9854,7 +9843,6 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.37.0.tgz", "integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==", "dev": true, - "peer": true, "requires": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -11828,7 +11816,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, - "peer": true, "requires": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -12431,8 +12418,7 @@ "version": "5.3.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", - "dev": true, - "peer": true + "dev": true }, "uc.micro": { "version": "2.1.0", @@ -12540,7 +12526,6 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.0.tgz", "integrity": "sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw==", "dev": true, - "peer": true, "requires": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -12574,7 +12559,6 @@ "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", "dev": true, - "peer": true, "requires": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^2.1.1", From f95b4c5b8d97d194f9a53c1f28869d5389aac549 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Jun 2026 17:06:47 +0000 Subject: [PATCH 3/4] Fix ${workspaceFolder} resolution and relative path handling for defaultInterpreterPath --- src/common/utils/internalVariables.ts | 13 +++- src/features/interpreterSelection.ts | 78 +++++++++++-------- .../common/internalVariables.unit.test.ts | 33 ++++++++ .../interpreterSelection.unit.test.ts | 47 +++++++++++ 4 files changed, 139 insertions(+), 32 deletions(-) diff --git a/src/common/utils/internalVariables.ts b/src/common/utils/internalVariables.ts index 22d5888a..de589e57 100644 --- a/src/common/utils/internalVariables.ts +++ b/src/common/utils/internalVariables.ts @@ -12,7 +12,18 @@ export function resolveVariables(value: string, project?: Uri, env?: { [key: str substitutions.set('${pythonProject}', project.fsPath); } - const workspace = project ? getWorkspaceFolder(project) : undefined; + // Resolve ${workspaceFolder} for a project (workspace) scope. Prefer the workspace folder + // that owns the project URI, but fall back to the single open workspace folder when the + // owning folder can't be determined (e.g. drive-letter casing differences on Windows can + // cause workspace.getWorkspaceFolder() to return undefined). Without this fallback the + // ${workspaceFolder} token would be left unexpanded and downstream path resolution fails. + let workspace = project ? getWorkspaceFolder(project) : undefined; + if (!workspace && project) { + const folders = getWorkspaceFolders(); + if (folders && folders.length === 1) { + workspace = folders[0]; + } + } if (workspace) { substitutions.set('${workspaceFolder}', workspace.uri.fsPath); } diff --git a/src/features/interpreterSelection.ts b/src/features/interpreterSelection.ts index 1e96ea7b..b64b25a6 100644 --- a/src/features/interpreterSelection.ts +++ b/src/features/interpreterSelection.ts @@ -131,38 +131,24 @@ async function resolvePriorityChainCore( ); } } else { - const expandedInterpreterPath = resolveVariables(userInterpreterPath, scope); - if (expandedInterpreterPath.includes('${')) { - traceWarn( - `${logPrefix} defaultInterpreterPath '${userInterpreterPath}' contains unresolved variables, falling back to auto-discovery`, - ); - const error: SettingResolutionError = { - setting: 'defaultInterpreterPath', - kind: 'pathUnresolvedVariables', - configuredValue: userInterpreterPath, - }; - errors.push(error); - } else { - const resolved = await tryResolveInterpreterPath( - nativeFinder, - api, - expandedInterpreterPath, - envManagers, - ); - if (resolved) { - traceVerbose(`${logPrefix} Priority 3: Using defaultInterpreterPath: ${userInterpreterPath}`); - return { result: resolved, errors }; - } - const error: SettingResolutionError = { - setting: 'defaultInterpreterPath', - kind: 'pathCannotResolve', - configuredValue: userInterpreterPath, - }; - errors.push(error); - traceWarn( - `${logPrefix} defaultInterpreterPath '${userInterpreterPath}' unresolvable, falling back to auto-discovery`, - ); + // Resolve relative paths against the workspace folder so the native finder doesn't + // resolve them against an unrelated current working directory (which can produce a + // malformed, duplicated path such as //.venv/...). + const absoluteInterpreterPath = toAbsoluteInterpreterPath(expandedInterpreterPath, scope); + const resolved = await tryResolveInterpreterPath(nativeFinder, api, absoluteInterpreterPath, envManagers); + if (resolved) { + traceVerbose(`${logPrefix} Priority 3: Using defaultInterpreterPath: ${userInterpreterPath}`); + return { result: resolved, errors }; } + const error: SettingResolutionError = { + setting: 'defaultInterpreterPath', + kind: 'pathCannotResolve', + configuredValue: userInterpreterPath, + }; + errors.push(error); + traceWarn( + `${logPrefix} defaultInterpreterPath '${userInterpreterPath}' unresolvable, falling back to auto-discovery`, + ); } } @@ -544,6 +530,36 @@ function getProjectSpecificEnvManager(projectManager: PythonProjectManager, scop return undefined; } +/** + * Resolve a (variable-expanded) interpreter path to an absolute path. + * + * `python.defaultInterpreterPath` may be configured as a relative path (e.g. `.venv/bin/python`). + * The native finder resolves relative paths against its own working directory, which is unrelated + * to the workspace and can produce malformed paths (including a duplicated workspace segment). + * To avoid this, relative paths are resolved against the workspace folder. The workspace folder is + * looked up from the scope, falling back to the single open workspace folder when needed. + * + * @param interpreterPath - The interpreter path after variable substitution. + * @param scope - The workspace folder URI, or undefined for global scope. + * @returns An absolute interpreter path when possible, otherwise the input unchanged. + */ +function toAbsoluteInterpreterPath(interpreterPath: string, scope: Uri | undefined): string { + if (path.isAbsolute(interpreterPath)) { + return interpreterPath; + } + let workspaceFolder = scope ? getWorkspaceFolder(scope) : undefined; + if (!workspaceFolder) { + const folders = getWorkspaceFolders(); + if (folders && folders.length === 1) { + workspaceFolder = folders[0]; + } + } + if (workspaceFolder) { + return path.resolve(workspaceFolder.uri.fsPath, interpreterPath); + } + return interpreterPath; +} + /** * Try to resolve an interpreter path via nativeFinder and return a PriorityChainResult. * Returns undefined if resolution fails. diff --git a/src/test/common/internalVariables.unit.test.ts b/src/test/common/internalVariables.unit.test.ts index f9ed6b66..ba82ffb5 100644 --- a/src/test/common/internalVariables.unit.test.ts +++ b/src/test/common/internalVariables.unit.test.ts @@ -42,4 +42,37 @@ suite('Internal Variable substitution', () => { assert.equal(result, `Some ${item.substitution} text ${item.substitution}`); }); }); + + test('Resolve ${workspaceFolder} via single-folder fallback when owning folder is undefined', () => { + // Simulates the Windows scenario where workspace.getWorkspaceFolder() fails to find the + // owning folder (e.g. drive-letter casing). With a single open folder, the token should + // still resolve instead of being left as a literal ${workspaceFolder}. + getWorkspaceFolderStub.returns(undefined); + getWorkspaceFoldersStub.returns([workspaceFolder]); + + const result = resolveVariables('${workspaceFolder}/.venv/Scripts/python.exe', project.uri as unknown as Uri); + + assert.equal(result, `${workspaceFolder.uri.fsPath}/.venv/Scripts/python.exe`); + assert.ok(!result.includes('${workspaceFolder}'), 'token should be expanded'); + }); + + test('Leaves ${workspaceFolder} unresolved when no project scope is provided', () => { + // Global scope (no project) must not resolve workspace-specific variables. + getWorkspaceFolderStub.returns(undefined); + getWorkspaceFoldersStub.returns([workspaceFolder]); + + const result = resolveVariables('${workspaceFolder}/.venv/Scripts/python.exe'); + + assert.equal(result, '${workspaceFolder}/.venv/Scripts/python.exe'); + }); + + test('Does not use single-folder fallback for ${workspaceFolder} with multiple folders', () => { + const otherFolder = { name: 'workspace2', uri: { fsPath: path.join(home, 'workspace2') } }; + getWorkspaceFolderStub.returns(undefined); + getWorkspaceFoldersStub.returns([workspaceFolder, otherFolder]); + + const result = resolveVariables('${workspaceFolder}/.venv/Scripts/python.exe', project.uri as unknown as Uri); + + assert.equal(result, '${workspaceFolder}/.venv/Scripts/python.exe'); + }); }); diff --git a/src/test/features/interpreterSelection.unit.test.ts b/src/test/features/interpreterSelection.unit.test.ts index b84be19e..06e5ea31 100644 --- a/src/test/features/interpreterSelection.unit.test.ts +++ b/src/test/features/interpreterSelection.unit.test.ts @@ -266,6 +266,53 @@ suite('Interpreter Selection - Priority Chain', () => { assert.ok(mockNativeFinder.resolve.calledOnceWithExactly(expandedInterpreterPath)); }); + test('should resolve a relative defaultInterpreterPath against the workspace folder', async () => { + // A relative path must be made absolute against the workspace folder so the native + // finder does not resolve it against an unrelated working directory (which can yield + // a malformed, duplicated path such as //.venv/...). + const workspaceUri = Uri.file(path.resolve('/test/workspace')); + const absoluteInterpreterPath = path.resolve(workspaceUri.fsPath, '.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 './.venv/bin/python'; + } + return undefined; + }); + mockNativeFinder.resolve.resolves({ + executable: absoluteInterpreterPath, + version: '3.11.0', + prefix: path.dirname(path.dirname(absoluteInterpreterPath)), + }); + mockApi.resolveEnvironment.resolves({ + ...mockVenvEnv, + displayPath: absoluteInterpreterPath, + environmentPath: Uri.file(absoluteInterpreterPath), + execInfo: { run: { executable: absoluteInterpreterPath } }, + }); + + 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(mockNativeFinder.resolve.calledOnceWithExactly(absoluteInterpreterPath)); + // The path passed to the native finder must not duplicate the workspace folder name. + const passedPath = mockNativeFinder.resolve.firstCall.args[0] as string; + assert.ok( + !passedPath.includes(`workspace${path.sep}workspace`), + `path should not duplicate the workspace folder: ${passedPath}`, + ); + }); + test('should skip native resolution when defaultInterpreterPath has unresolved variables', async () => { // When resolveVariables can't resolve ${workspaceFolder} (e.g., global scope with no workspace), // the path still contains '${' and should be skipped without calling nativeFinder.resolve From a611d722188fd1a9266433a111dea8a2b0f97759 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Jun 2026 19:01:25 +0000 Subject: [PATCH 4/4] Add unit tests for interpreter path resolution edge cases --- .../common/internalVariables.unit.test.ts | 11 ++ .../interpreterSelection.unit.test.ts | 117 ++++++++++++++++++ 2 files changed, 128 insertions(+) diff --git a/src/test/common/internalVariables.unit.test.ts b/src/test/common/internalVariables.unit.test.ts index ba82ffb5..98b4e4ac 100644 --- a/src/test/common/internalVariables.unit.test.ts +++ b/src/test/common/internalVariables.unit.test.ts @@ -75,4 +75,15 @@ suite('Internal Variable substitution', () => { assert.equal(result, '${workspaceFolder}/.venv/Scripts/python.exe'); }); + + test('Leaves ${workspaceFolder} unresolved when no folders are open', () => { + // No owning folder and no open folders at all (getWorkspaceFolders returns undefined): + // the single-folder fallback must not crash and the token stays literal. + getWorkspaceFolderStub.returns(undefined); + getWorkspaceFoldersStub.returns(undefined); + + const result = resolveVariables('${workspaceFolder}/.venv/Scripts/python.exe', project.uri as unknown as Uri); + + assert.equal(result, '${workspaceFolder}/.venv/Scripts/python.exe'); + }); }); diff --git a/src/test/features/interpreterSelection.unit.test.ts b/src/test/features/interpreterSelection.unit.test.ts index 06e5ea31..22163452 100644 --- a/src/test/features/interpreterSelection.unit.test.ts +++ b/src/test/features/interpreterSelection.unit.test.ts @@ -313,6 +313,123 @@ suite('Interpreter Selection - Priority Chain', () => { ); }); + test('should resolve a relative defaultInterpreterPath via single-folder fallback', async () => { + // When the owning workspace folder can't be determined (getWorkspaceFolder returns + // undefined, e.g. drive-letter casing on Windows) but a single folder is open, the + // relative path should still be resolved against that folder. + const workspaceUri = Uri.file(path.resolve('/test/workspace')); + const absoluteInterpreterPath = path.resolve(workspaceUri.fsPath, '.venv/bin/python'); + const workspaceFolder = { name: 'workspace', uri: workspaceUri } as WorkspaceFolder; + + sandbox.stub(workspaceApis, 'getConfiguration').returns(createMockConfig([]) as WorkspaceConfiguration); + sandbox.stub(workspaceApis, 'getWorkspaceFolder').returns(undefined); + sandbox.stub(workspaceApis, 'getWorkspaceFolders').returns([workspaceFolder]); + sandbox.stub(helpers, 'getUserConfiguredSetting').callsFake((section: string, key: string) => { + if (section === 'python' && key === 'defaultInterpreterPath') { + return './.venv/bin/python'; + } + return undefined; + }); + mockNativeFinder.resolve.resolves({ + executable: absoluteInterpreterPath, + version: '3.11.0', + prefix: path.dirname(path.dirname(absoluteInterpreterPath)), + }); + mockApi.resolveEnvironment.resolves({ + ...mockVenvEnv, + displayPath: absoluteInterpreterPath, + environmentPath: Uri.file(absoluteInterpreterPath), + execInfo: { run: { executable: absoluteInterpreterPath } }, + }); + + 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(mockNativeFinder.resolve.calledOnceWithExactly(absoluteInterpreterPath)); + }); + + test('should not make a relative defaultInterpreterPath absolute with multiple folders open', async () => { + // With multiple folders open and no determinable owning folder, the single-folder + // fallback must not apply, so the relative path is passed through unchanged. + const workspaceUri = Uri.file(path.resolve('/test/workspace')); + const otherUri = Uri.file(path.resolve('/test/other')); + const relativeInterpreterPath = './.venv/bin/python'; + + sandbox.stub(workspaceApis, 'getConfiguration').returns(createMockConfig([]) as WorkspaceConfiguration); + sandbox.stub(workspaceApis, 'getWorkspaceFolder').returns(undefined); + sandbox.stub(workspaceApis, 'getWorkspaceFolders').returns([ + { name: 'workspace', uri: workspaceUri } as WorkspaceFolder, + { name: 'other', uri: otherUri } as WorkspaceFolder, + ]); + sandbox.stub(helpers, 'getUserConfiguredSetting').callsFake((section: string, key: string) => { + if (section === 'python' && key === 'defaultInterpreterPath') { + return relativeInterpreterPath; + } + return undefined; + }); + mockNativeFinder.resolve.resolves({ + executable: relativeInterpreterPath, + version: '3.11.0', + prefix: '.venv', + }); + mockApi.resolveEnvironment.resolves(mockVenvEnv); + + await resolveEnvironmentByPriority( + workspaceUri, + mockEnvManagers as unknown as EnvironmentManagers, + mockProjectManager as unknown as PythonProjectManager, + mockNativeFinder as unknown as NativePythonFinder, + mockApi as unknown as PythonEnvironmentApi, + ); + + assert.ok(mockNativeFinder.resolve.calledOnceWithExactly(relativeInterpreterPath)); + }); + + test('should pass an absolute defaultInterpreterPath to the native finder unchanged', async () => { + // Absolute paths must not be re-resolved against the workspace folder. + const workspaceUri = Uri.file(path.resolve('/test/workspace')); + const absoluteInterpreterPath = Uri.file(path.resolve('/opt/python/bin/python')).fsPath; + 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 absoluteInterpreterPath; + } + return undefined; + }); + mockNativeFinder.resolve.resolves({ + executable: absoluteInterpreterPath, + version: '3.11.0', + prefix: path.dirname(path.dirname(absoluteInterpreterPath)), + }); + mockApi.resolveEnvironment.resolves({ + ...mockSystemEnv, + displayPath: absoluteInterpreterPath, + environmentPath: Uri.file(absoluteInterpreterPath), + execInfo: { run: { executable: absoluteInterpreterPath } }, + }); + + 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(mockNativeFinder.resolve.calledOnceWithExactly(absoluteInterpreterPath)); + }); + test('should skip native resolution when defaultInterpreterPath has unresolved variables', async () => { // When resolveVariables can't resolve ${workspaceFolder} (e.g., global scope with no workspace), // the path still contains '${' and should be skipped without calling nativeFinder.resolve