diff --git a/src/features/settings/settingHelpers.ts b/src/features/settings/settingHelpers.ts index 5acef3d4..4ef9f9ab 100644 --- a/src/features/settings/settingHelpers.ts +++ b/src/features/settings/settingHelpers.ts @@ -1,12 +1,5 @@ import * as path from 'path'; -import { - ConfigurationScope, - ConfigurationTarget, - Uri, - workspace, - WorkspaceConfiguration, - WorkspaceFolder, -} from 'vscode'; +import { ConfigurationScope, ConfigurationTarget, Uri, WorkspaceConfiguration, WorkspaceFolder } from 'vscode'; import { PythonProject } from '../../api'; import { DEFAULT_ENV_MANAGER_ID, DEFAULT_PACKAGE_MANAGER_ID } from '../../common/constants'; import { traceError, traceInfo, traceWarn } from '../../common/logging'; @@ -22,7 +15,7 @@ function getSettings( if (overrides.length > 0 && scope instanceof Uri) { const pw = wm.get(scope); - const w = workspace.getWorkspaceFolder(scope); + const w = workspaceApis.getWorkspaceFolder(scope); if (pw && w) { const pwPath = path.normalize(pw.uri.fsPath); return overrides.find((s) => path.resolve(w.uri.fsPath, s.path) === pwPath); @@ -106,7 +99,7 @@ export async function setAllManagerSettings(edits: EditAllManagerSettings[]): Pr .filter((e) => !!e.project) .map((e) => e as EditAllManagerSettingsInternal) .forEach((e) => { - const w = workspace.getWorkspaceFolder(e.project.uri); + const w = workspaceApis.getWorkspaceFolder(e.project.uri); if (w) { workspaces.set(w, [ ...(workspaces.get(w) || []), @@ -136,13 +129,36 @@ export async function setAllManagerSettings(edits: EditAllManagerSettings[]): Pr es.forEach((e) => { const pwPath = path.normalize(e.project.uri.fsPath); + const isRoot = path.normalize(w.uri.fsPath) === pwPath; const index = overrides.findIndex((s) => path.resolve(w.uri.fsPath, s.path) === pwPath); - if (index >= 0) { + + // For workspace root in single-folder workspaces (no workspaceFile), + // use default settings instead of pythonProjects entries + if (isRoot && !workspaceFile) { + // Remove existing entry if present (migration from buggy empty path) + if (index >= 0) { + overrides.splice(index, 1); + } + if (config.get('defaultEnvManager') !== e.envManager) { + promises.push(config.update('defaultEnvManager', e.envManager, ConfigurationTarget.Workspace)); + } + if (config.get('defaultPackageManager') !== e.packageManager) { + promises.push( + config.update('defaultPackageManager', e.packageManager, ConfigurationTarget.Workspace), + ); + } + } else if (index >= 0) { overrides[index].envManager = e.envManager; overrides[index].packageManager = e.packageManager; + // Fix empty path to "." for workspace root (migration from buggy entries) + if (overrides[index].path === '') { + overrides[index].path = '.'; + } } else if (workspaceFile) { + // Use "." for workspace root instead of empty string + const relativePath = path.relative(w.uri.fsPath, pwPath).replace(/\\/g, '/'); overrides.push({ - path: path.relative(w.uri.fsPath, pwPath).replace(/\\/g, '/'), + path: relativePath || '.', envManager: e.envManager, packageManager: e.packageManager, }); @@ -160,13 +176,19 @@ export async function setAllManagerSettings(edits: EditAllManagerSettings[]): Pr // Only write pythonProjects if: // 1. There was already an explicit setting OR - // 2. adding new project entries - const shouldWriteProjects = existingProjectsSetting !== undefined || overrides.length > originalOverridesLength; + // 2. adding new project entries OR + // 3. removing entries (migration from buggy empty path) + const shouldWriteProjects = + existingProjectsSetting !== undefined || + overrides.length > originalOverridesLength || + overrides.length < originalOverridesLength; if (shouldWriteProjects) { + // If all entries are removed, set to undefined to clean up settings + const valueToWrite = overrides.length > 0 ? overrides : undefined; promises.push( config.update( 'pythonProjects', - overrides, + valueToWrite, workspaceFile ? ConfigurationTarget.WorkspaceFolder : ConfigurationTarget.Workspace, ), ); @@ -204,7 +226,7 @@ export async function setEnvironmentManager(edits: EditEnvManagerSettings[]): Pr .filter((e) => !!e.project) .map((e) => e as EditEnvManagerSettingsInternal) .forEach((e) => { - const w = workspace.getWorkspaceFolder(e.project.uri); + const w = workspaceApis.getWorkspaceFolder(e.project.uri); if (w) { workspaces.set(w, [...(workspaces.get(w) || []), { project: e.project, envManager: e.envManager }]); } else { @@ -275,7 +297,7 @@ export async function setPackageManager(edits: EditPackageManagerSettings[]): Pr .filter((e) => !!e.project) .map((e) => e as EditPackageManagerSettingsInternal) .forEach((e) => { - const w = workspace.getWorkspaceFolder(e.project.uri); + const w = workspaceApis.getWorkspaceFolder(e.project.uri); if (w) { workspaces.set(w, [ ...(workspaces.get(w) || []), @@ -348,7 +370,7 @@ export async function addPythonProjectSetting(edits: EditProjectSettings[]): Pro const pkgManager = globalConfig.get('defaultPackageManager', DEFAULT_PACKAGE_MANAGER_ID); edits.forEach((e) => { - const w = workspace.getWorkspaceFolder(e.project.uri); + const w = workspaceApis.getWorkspaceFolder(e.project.uri); if (w) { workspaces.set(w, [...(workspaces.get(w) || []), e]); } else { @@ -366,10 +388,36 @@ export async function addPythonProjectSetting(edits: EditProjectSettings[]): Pro workspaces.forEach((es, w) => { const config = workspaceApis.getConfiguration('python-envs', w.uri); const overrides = config.get('pythonProjects', []); + let overridesModified = false; es.forEach((e) => { - if (isMultiroot) { - } const pwPath = path.normalize(e.project.uri.fsPath); + const isRoot = path.normalize(w.uri.fsPath) === pwPath; + + // For workspace root projects in single-folder workspaces, use default settings + // instead of adding to pythonProjects with empty path + if (isRoot && !isMultiroot) { + // Remove existing entry if present (migration from buggy empty path) + const existingIndex = overrides.findIndex((s) => path.resolve(w.uri.fsPath, s.path) === pwPath); + if (existingIndex >= 0) { + overrides.splice(existingIndex, 1); + overridesModified = true; + } + + const effectiveEnvManager = e.envManager ?? envManager; + const effectivePkgManager = e.packageManager ?? pkgManager; + if (config.get('defaultEnvManager') !== effectiveEnvManager) { + promises.push( + config.update('defaultEnvManager', effectiveEnvManager, ConfigurationTarget.Workspace), + ); + } + if (config.get('defaultPackageManager') !== effectivePkgManager) { + promises.push( + config.update('defaultPackageManager', effectivePkgManager, ConfigurationTarget.Workspace), + ); + } + return; + } + const index = overrides.findIndex((s) => { if (s.workspace) { // If the workspace is set, check workspace and path in existing overrides @@ -381,16 +429,29 @@ export async function addPythonProjectSetting(edits: EditProjectSettings[]): Pro // Preserve existing manager settings if not explicitly provided overrides[index].envManager = e.envManager ?? overrides[index].envManager; overrides[index].packageManager = e.packageManager ?? overrides[index].packageManager; + // Fix empty path to "." for workspace root in multi-root (migration from buggy entries) + if (overrides[index].path === '') { + overrides[index].path = '.'; + } + overridesModified = true; } else { + // Use "." for workspace root in multi-root workspaces instead of empty string + const relativePath = path.relative(w.uri.fsPath, pwPath).replace(/\\/g, '/'); overrides.push({ - path: path.relative(w.uri.fsPath, pwPath).replace(/\\/g, '/'), + path: relativePath || '.', envManager, packageManager: pkgManager, workspace: isMultiroot ? w.name : undefined, }); + overridesModified = true; } }); - promises.push(config.update('pythonProjects', overrides, ConfigurationTarget.Workspace)); + // Write pythonProjects if modified (entries added, removed, or updated) + if (overridesModified) { + // If all entries are removed, set to undefined to clean up settings + const valueToWrite = overrides.length > 0 ? overrides : undefined; + promises.push(config.update('pythonProjects', valueToWrite, ConfigurationTarget.Workspace)); + } }); await Promise.all(promises); } @@ -399,7 +460,7 @@ export async function removePythonProjectSetting(edits: EditProjectSettings[]): const noWorkspace: EditProjectSettings[] = []; const workspaces = new Map(); edits.forEach((e) => { - const w = workspace.getWorkspaceFolder(e.project.uri); + const w = workspaceApis.getWorkspaceFolder(e.project.uri); if (w) { workspaces.set(w, [...(workspaces.get(w) || []), e]); } else { diff --git a/src/test/features/settings/settingHelpers.unit.test.ts b/src/test/features/settings/settingHelpers.unit.test.ts index fae18ddd..1cb3a0c1 100644 --- a/src/test/features/settings/settingHelpers.unit.test.ts +++ b/src/test/features/settings/settingHelpers.unit.test.ts @@ -1,15 +1,26 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import * as assert from 'assert'; +import * as path from 'path'; import * as sinon from 'sinon'; -import { ConfigurationTarget } from 'vscode'; +import { ConfigurationTarget, Uri, WorkspaceFolder } from 'vscode'; import * as workspaceApis from '../../../common/workspace.apis'; import { + addPythonProjectSetting, setAllManagerSettings, setEnvironmentManager, setPackageManager, } from '../../../features/settings/settingHelpers'; +import { PythonProjectsImpl } from '../../../internal.api'; import { MockWorkspaceConfiguration } from '../../mocks/mockWorkspaceConfig'; +/** + * Returns a platform-appropriate workspace path for testing. + * On Windows, paths must include a drive letter to work correctly with path.resolve(). + */ +function getTestWorkspacePath(): string { + return process.platform === 'win32' ? 'C:\\workspace' : '/workspace'; +} + /** * These tests verify that settings ARE written when the value changes, * regardless of whether it's the default/system manager or not. @@ -249,3 +260,415 @@ suite('Setting Helpers - Settings Write Behavior', () => { }); }); }); + +/** + * Tests for the empty path bug fix (Issue #1219, #1115) + * When a project is the workspace root folder, we should NOT write "path": "" to pythonProjects. + * Instead, we should use defaultEnvManager/defaultPackageManager settings. + */ +suite('Setting Helpers - Empty Path Bug Fix', () => { + const VENV_MANAGER_ID = 'ms-python.python:venv'; + const PIP_MANAGER_ID = 'ms-python.python:pip'; + + const workspacePath = getTestWorkspacePath(); + const workspaceUri = Uri.file(workspacePath); + const workspaceFolder: WorkspaceFolder = { + uri: workspaceUri, + name: 'workspace', + index: 0, + }; + + let updateCalls: Array<{ key: string; value: unknown; target: ConfigurationTarget }>; + + setup(() => { + updateCalls = []; + }); + + teardown(() => { + sinon.restore(); + }); + + function createMockConfigForWorkspace(options?: { + pythonProjects?: any[]; + defaultEnvManager?: string; + defaultPackageManager?: string; + }): MockWorkspaceConfiguration { + const mockConfig = new MockWorkspaceConfiguration(); + + (mockConfig as any).get = (key: string, defaultValue?: T): T | undefined => { + if (key === 'pythonProjects') { + return (options?.pythonProjects ?? []) as T; + } + if (key === 'defaultEnvManager') { + return (options?.defaultEnvManager ?? VENV_MANAGER_ID) as T; + } + if (key === 'defaultPackageManager') { + return (options?.defaultPackageManager ?? PIP_MANAGER_ID) as T; + } + return defaultValue; + }; + + mockConfig.update = ( + section: string, + value: unknown, + configurationTarget?: boolean | ConfigurationTarget, + ): Promise => { + updateCalls.push({ + key: section, + value, + target: configurationTarget as ConfigurationTarget, + }); + return Promise.resolve(); + }; + + return mockConfig; + } + + suite('addPythonProjectSetting - Single Folder Workspace', () => { + test('should use defaultEnvManager/defaultPackageManager for workspace root instead of empty path', async () => { + // Setup: single folder workspace + sinon.stub(workspaceApis, 'getWorkspaceFolders').returns([workspaceFolder]); + sinon.stub(workspaceApis, 'getConfiguration').returns(createMockConfigForWorkspace()); + sinon.stub(workspaceApis, 'getWorkspaceFolder').returns(workspaceFolder); + + // Create a project at the workspace root + const rootProject = new PythonProjectsImpl('workspace', workspaceUri); + + await addPythonProjectSetting([ + { + project: rootProject, + envManager: VENV_MANAGER_ID, + packageManager: PIP_MANAGER_ID, + }, + ]); + + // Should NOT write to pythonProjects at all for root projects in single folder workspace + const pythonProjectsUpdates = updateCalls.filter((c) => c.key === 'pythonProjects'); + assert.strictEqual( + pythonProjectsUpdates.length, + 0, + 'Should NOT write to pythonProjects for workspace root in single folder workspace', + ); + + // Instead should write to defaultEnvManager/defaultPackageManager + // (only if values differ, which they don't in this test) + }); + + test('should write to pythonProjects for subfolders (not workspace root)', async () => { + // Setup: single folder workspace + sinon.stub(workspaceApis, 'getWorkspaceFolders').returns([workspaceFolder]); + sinon.stub(workspaceApis, 'getConfiguration').returns(createMockConfigForWorkspace()); + sinon.stub(workspaceApis, 'getWorkspaceFolder').returns(workspaceFolder); + + // Create a project at a subfolder (not workspace root) + const subfolderPath = path.join(workspacePath, 'subfolder'); + const subfolderUri = Uri.file(subfolderPath); + const subfolderProject = new PythonProjectsImpl('subfolder', subfolderUri); + + await addPythonProjectSetting([ + { + project: subfolderProject, + envManager: VENV_MANAGER_ID, + packageManager: PIP_MANAGER_ID, + }, + ]); + + // Should write to pythonProjects for subfolders + const pythonProjectsUpdates = updateCalls.filter((c) => c.key === 'pythonProjects'); + assert.strictEqual(pythonProjectsUpdates.length, 1, 'Should write to pythonProjects for subfolders'); + + // The path should NOT be empty + const projects = pythonProjectsUpdates[0].value as any[]; + assert.ok(projects.length > 0, 'Should have at least one project entry'); + assert.strictEqual(projects[0].path, 'subfolder', 'Path should be "subfolder", not empty'); + }); + }); + + suite('addPythonProjectSetting - Multi-root Workspace', () => { + test('should use "." for workspace root path instead of empty string', async () => { + // Setup: multi-root workspace + const secondWorkspaceUri = Uri.file(process.platform === 'win32' ? 'C:\\workspace2' : '/workspace2'); + const secondWorkspaceFolder: WorkspaceFolder = { + uri: secondWorkspaceUri, + name: 'workspace2', + index: 1, + }; + sinon.stub(workspaceApis, 'getWorkspaceFolders').returns([workspaceFolder, secondWorkspaceFolder]); + sinon.stub(workspaceApis, 'getConfiguration').returns(createMockConfigForWorkspace()); + sinon.stub(workspaceApis, 'getWorkspaceFolder').returns(workspaceFolder); + + // Create a project at the workspace root + const rootProject = new PythonProjectsImpl('workspace', workspaceUri); + + await addPythonProjectSetting([ + { + project: rootProject, + envManager: VENV_MANAGER_ID, + packageManager: PIP_MANAGER_ID, + }, + ]); + + // Should write to pythonProjects + const pythonProjectsUpdates = updateCalls.filter((c) => c.key === 'pythonProjects'); + assert.strictEqual(pythonProjectsUpdates.length, 1, 'Should write to pythonProjects in multi-root'); + + // The path should be "." not empty string + const projects = pythonProjectsUpdates[0].value as any[]; + assert.ok(projects.length > 0, 'Should have at least one project entry'); + assert.strictEqual(projects[0].path, '.', 'Path should be "." not empty string for workspace root'); + }); + }); + + suite('setAllManagerSettings - Multi-root Workspace', () => { + test('should use "." for workspace root path instead of empty string when workspaceFile exists', async () => { + // Setup: multi-root workspace with workspace file + sinon.stub(workspaceApis, 'getWorkspaceFolders').returns([workspaceFolder]); + sinon.stub(workspaceApis, 'getWorkspaceFile').returns(Uri.file('/test.code-workspace')); + const mockConfig = createMockConfigForWorkspace(); + (mockConfig as any).inspect = () => ({ + workspaceFolderValue: undefined, + workspaceValue: undefined, + }); + sinon.stub(workspaceApis, 'getConfiguration').returns(mockConfig); + sinon.stub(workspaceApis, 'getWorkspaceFolder').returns(workspaceFolder); + + // Create a project at the workspace root + const rootProject = new PythonProjectsImpl('workspace', workspaceUri); + + await setAllManagerSettings([ + { + project: rootProject, + envManager: VENV_MANAGER_ID, + packageManager: PIP_MANAGER_ID, + }, + ]); + + // Should write to pythonProjects + const pythonProjectsUpdates = updateCalls.filter((c) => c.key === 'pythonProjects'); + assert.strictEqual(pythonProjectsUpdates.length, 1, 'Should write to pythonProjects'); + + // The path should be "." not empty string + const projects = pythonProjectsUpdates[0].value as any[]; + assert.ok(projects.length > 0, 'Should have at least one project entry'); + assert.strictEqual(projects[0].path, '.', 'Path should be "." not empty string for workspace root'); + }); + }); +}); + +/** + * Tests for migrating existing entries with empty path (Issue #1219, #1115) + * When there's an existing entry with "path": "", it should be fixed or removed. + */ +suite('Setting Helpers - Empty Path Migration', () => { + const VENV_MANAGER_ID = 'ms-python.python:venv'; + const PIP_MANAGER_ID = 'ms-python.python:pip'; + const CONDA_MANAGER_ID = 'ms-python.python:conda'; + + const workspacePath = getTestWorkspacePath(); + const workspaceUri = Uri.file(workspacePath); + const workspaceFolder: WorkspaceFolder = { + uri: workspaceUri, + name: 'workspace', + index: 0, + }; + + let updateCalls: Array<{ key: string; value: unknown; target: ConfigurationTarget }>; + + setup(() => { + updateCalls = []; + }); + + teardown(() => { + sinon.restore(); + }); + + function createMockConfigWithExistingEmptyPath(options?: { + defaultEnvManager?: string; + defaultPackageManager?: string; + }): MockWorkspaceConfiguration { + const mockConfig = new MockWorkspaceConfiguration(); + + // Existing pythonProjects with buggy empty path entry + const existingProjects = [ + { + path: '', // Buggy empty path + envManager: VENV_MANAGER_ID, + packageManager: PIP_MANAGER_ID, + }, + ]; + + (mockConfig as any).get = (key: string, defaultValue?: T): T | undefined => { + if (key === 'pythonProjects') { + return existingProjects as T; + } + if (key === 'defaultEnvManager') { + return (options?.defaultEnvManager ?? VENV_MANAGER_ID) as T; + } + if (key === 'defaultPackageManager') { + return (options?.defaultPackageManager ?? PIP_MANAGER_ID) as T; + } + return defaultValue; + }; + + mockConfig.update = ( + section: string, + value: unknown, + configurationTarget?: boolean | ConfigurationTarget, + ): Promise => { + updateCalls.push({ + key: section, + value, + target: configurationTarget as ConfigurationTarget, + }); + return Promise.resolve(); + }; + + return mockConfig; + } + + suite('addPythonProjectSetting - Migration of existing empty path', () => { + test('should remove existing empty path entry and use defaults in single folder workspace', async () => { + // Setup: single folder workspace with existing buggy empty path entry + sinon.stub(workspaceApis, 'getWorkspaceFolders').returns([workspaceFolder]); + sinon.stub(workspaceApis, 'getConfiguration').returns(createMockConfigWithExistingEmptyPath()); + sinon.stub(workspaceApis, 'getWorkspaceFolder').returns(workspaceFolder); + + // Create a project at the workspace root + const rootProject = new PythonProjectsImpl('workspace', workspaceUri); + + await addPythonProjectSetting([ + { + project: rootProject, + envManager: CONDA_MANAGER_ID, + packageManager: PIP_MANAGER_ID, + }, + ]); + + // Should write to pythonProjects to remove the empty path entry + const pythonProjectsUpdates = updateCalls.filter((c) => c.key === 'pythonProjects'); + assert.strictEqual(pythonProjectsUpdates.length, 1, 'Should write to pythonProjects'); + + // The value should be undefined (empty array removed) or empty array + const projects = pythonProjectsUpdates[0].value; + assert.ok( + projects === undefined || (Array.isArray(projects) && projects.length === 0), + 'Should remove the buggy entry or set to undefined', + ); + + // Should also write to defaultEnvManager since value changed + const envManagerUpdates = updateCalls.filter((c) => c.key === 'defaultEnvManager'); + assert.strictEqual(envManagerUpdates.length, 1, 'Should write to defaultEnvManager when value differs'); + assert.strictEqual(envManagerUpdates[0].value, CONDA_MANAGER_ID); + }); + + test('should fix empty path to "." when updating in multi-root workspace', async () => { + // Setup: multi-root workspace with existing buggy empty path entry + const secondWorkspaceUri = Uri.file(process.platform === 'win32' ? 'C:\\workspace2' : '/workspace2'); + const secondWorkspaceFolder: WorkspaceFolder = { + uri: secondWorkspaceUri, + name: 'workspace2', + index: 1, + }; + sinon.stub(workspaceApis, 'getWorkspaceFolders').returns([workspaceFolder, secondWorkspaceFolder]); + sinon.stub(workspaceApis, 'getConfiguration').returns(createMockConfigWithExistingEmptyPath()); + sinon.stub(workspaceApis, 'getWorkspaceFolder').returns(workspaceFolder); + + // Create a project at the workspace root + const rootProject = new PythonProjectsImpl('workspace', workspaceUri); + + await addPythonProjectSetting([ + { + project: rootProject, + envManager: CONDA_MANAGER_ID, + packageManager: PIP_MANAGER_ID, + }, + ]); + + // Should write to pythonProjects + const pythonProjectsUpdates = updateCalls.filter((c) => c.key === 'pythonProjects'); + assert.strictEqual(pythonProjectsUpdates.length, 1, 'Should write to pythonProjects'); + + // The path should be fixed to "." not empty string + const projects = pythonProjectsUpdates[0].value as any[]; + assert.ok(projects.length > 0, 'Should have at least one project entry'); + assert.strictEqual(projects[0].path, '.', 'Path should be fixed to "." not empty string'); + assert.strictEqual(projects[0].envManager, CONDA_MANAGER_ID, 'envManager should be updated'); + }); + }); + + suite('setAllManagerSettings - Migration of existing empty path', () => { + test('should remove existing empty path entry and use defaults in single folder workspace', async () => { + // Setup: single folder workspace with existing buggy empty path entry + sinon.stub(workspaceApis, 'getWorkspaceFolders').returns([workspaceFolder]); + sinon.stub(workspaceApis, 'getWorkspaceFile').returns(undefined); // No workspace file + const mockConfig = createMockConfigWithExistingEmptyPath(); + (mockConfig as any).inspect = () => ({ + workspaceFolderValue: undefined, + workspaceValue: [{ path: '', envManager: VENV_MANAGER_ID, packageManager: PIP_MANAGER_ID }], + }); + sinon.stub(workspaceApis, 'getConfiguration').returns(mockConfig); + sinon.stub(workspaceApis, 'getWorkspaceFolder').returns(workspaceFolder); + + // Create a project at the workspace root + const rootProject = new PythonProjectsImpl('workspace', workspaceUri); + + await setAllManagerSettings([ + { + project: rootProject, + envManager: CONDA_MANAGER_ID, + packageManager: PIP_MANAGER_ID, + }, + ]); + + // Should write to pythonProjects to remove the empty path entry + const pythonProjectsUpdates = updateCalls.filter((c) => c.key === 'pythonProjects'); + assert.strictEqual(pythonProjectsUpdates.length, 1, 'Should write to pythonProjects'); + + // The value should be undefined or empty array (entry removed) + const projects = pythonProjectsUpdates[0].value; + assert.ok( + projects === undefined || (Array.isArray(projects) && projects.length === 0), + 'Should remove the buggy entry', + ); + + // Should also write to defaultEnvManager since value changed + const envManagerUpdates = updateCalls.filter((c) => c.key === 'defaultEnvManager'); + assert.strictEqual(envManagerUpdates.length, 1, 'Should write to defaultEnvManager when value differs'); + assert.strictEqual(envManagerUpdates[0].value, CONDA_MANAGER_ID); + }); + + test('should fix empty path to "." when updating in multi-root workspace', async () => { + // Setup: multi-root workspace (with workspace file) and existing buggy empty path entry + sinon.stub(workspaceApis, 'getWorkspaceFolders').returns([workspaceFolder]); + sinon.stub(workspaceApis, 'getWorkspaceFile').returns(Uri.file('/test.code-workspace')); + const mockConfig = createMockConfigWithExistingEmptyPath(); + (mockConfig as any).inspect = () => ({ + workspaceFolderValue: [{ path: '', envManager: VENV_MANAGER_ID, packageManager: PIP_MANAGER_ID }], + workspaceValue: undefined, + }); + sinon.stub(workspaceApis, 'getConfiguration').returns(mockConfig); + sinon.stub(workspaceApis, 'getWorkspaceFolder').returns(workspaceFolder); + + // Create a project at the workspace root + const rootProject = new PythonProjectsImpl('workspace', workspaceUri); + + await setAllManagerSettings([ + { + project: rootProject, + envManager: CONDA_MANAGER_ID, + packageManager: PIP_MANAGER_ID, + }, + ]); + + // Should write to pythonProjects + const pythonProjectsUpdates = updateCalls.filter((c) => c.key === 'pythonProjects'); + assert.strictEqual(pythonProjectsUpdates.length, 1, 'Should write to pythonProjects'); + + // The path should be fixed to "." not empty string + const projects = pythonProjectsUpdates[0].value as any[]; + assert.ok(projects.length > 0, 'Should have at least one project entry'); + assert.strictEqual(projects[0].path, '.', 'Path should be fixed to "." not empty string'); + assert.strictEqual(projects[0].envManager, CONDA_MANAGER_ID, 'envManager should be updated'); + }); + }); +});