From eadb46b54839b3c81d2f2724f5139b73d8112822 Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Wed, 1 Oct 2025 16:43:08 -0700 Subject: [PATCH 1/4] Fix where activation goes missing after user modifying rc scripts (#880) Resolves: https://github.com/microsoft/vscode-python-environments/issues/865 If user modifies already existing shell profile script needed for shell startup, they get stuck into not activating state because they still have auto activation guard, which blocks shell integration from core from activating. This also means because they have wrong env var in profile script, it won't activate there either. We should better track this, and properly clean up profile script when relying on shell integration for activation. /cc @karthiknadig --- .../terminal/shells/bash/bashStartup.ts | 10 ++++---- src/features/terminal/terminalManager.ts | 23 +++++++++++++++---- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/src/features/terminal/shells/bash/bashStartup.ts b/src/features/terminal/shells/bash/bashStartup.ts index 86838d5c..e62ad1af 100644 --- a/src/features/terminal/shells/bash/bashStartup.ts +++ b/src/features/terminal/shells/bash/bashStartup.ts @@ -61,14 +61,12 @@ function getActivationContent(key: string): string { async function isStartupSetup(profile: string, key: string): Promise { if (await fs.pathExists(profile)) { const content = await fs.readFile(profile, 'utf8'); - return hasStartupCode(content, regionStart, regionEnd, [key]) - ? ShellSetupState.Setup - : ShellSetupState.NotSetup; - } else { - return ShellSetupState.NotSetup; + if (hasStartupCode(content, regionStart, regionEnd, [key])) { + return ShellSetupState.Setup; + } } + return ShellSetupState.NotSetup; } - async function setupStartup(profile: string, key: string, name: string): Promise { if (shellIntegrationForActiveTerminal(name, profile)) { removeStartup(profile, key); diff --git a/src/features/terminal/terminalManager.ts b/src/features/terminal/terminalManager.ts index a985c079..dcba585c 100644 --- a/src/features/terminal/terminalManager.ts +++ b/src/features/terminal/terminalManager.ts @@ -147,15 +147,23 @@ export class TerminalManagerImpl implements TerminalManager { const shellsToSetup: ShellStartupScriptProvider[] = []; await Promise.all( providers.map(async (p) => { + const state = await p.isSetup(); if (this.shellSetup.has(p.shellType)) { - traceVerbose(`Shell profile for ${p.shellType} already checked.`); - return; + // This ensures modified scripts are detected even after initial setup + const cachedSetup = this.shellSetup.get(p.shellType); + if ((state === ShellSetupState.Setup) !== cachedSetup) { + traceVerbose(`Shell profile for ${p.shellType} state changed, updating cache.`); + // State changed - clear cache and re-evaluate + this.shellSetup.delete(p.shellType); + } else { + traceVerbose(`Shell profile for ${p.shellType} already checked.`); + return; + } } traceVerbose(`Checking shell profile for ${p.shellType}.`); - const state = await p.isSetup(); if (state === ShellSetupState.NotSetup) { - // Check if shell integration is available before marking for setup if (shellIntegrationForActiveTerminal(p.name)) { + await p.teardownScripts(); this.shellSetup.set(p.shellType, true); traceVerbose( `Shell integration available for ${p.shellType}, skipping prompt, and profile modification.`, @@ -169,6 +177,12 @@ export class TerminalManagerImpl implements TerminalManager { ); } } else if (state === ShellSetupState.Setup) { + if (shellIntegrationForActiveTerminal(p.name)) { + await p.teardownScripts(); + traceVerbose( + `Shell integration available for ${p.shellType}, removed profile script in favor of shell integration.`, + ); + } this.shellSetup.set(p.shellType, true); traceVerbose(`Shell profile for ${p.shellType} is setup.`); } else if (state === ShellSetupState.NotInstalled) { @@ -228,6 +242,7 @@ export class TerminalManagerImpl implements TerminalManager { let actType = getAutoActivationType(); const shellType = identifyTerminalShell(terminal); if (actType === ACT_TYPE_SHELL) { + await this.handleSetupCheck(shellType); actType = await this.getEffectiveActivationType(shellType); } From 4f4488eda24e5499073deff4fa7970a143521c76 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Thu, 2 Oct 2025 15:05:15 -0700 Subject: [PATCH 2/4] fix env var clearing (#886) --- src/features/terminal/terminalEnvVarInjector.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/features/terminal/terminalEnvVarInjector.ts b/src/features/terminal/terminalEnvVarInjector.ts index 3f08a0cd..039c4b3c 100644 --- a/src/features/terminal/terminalEnvVarInjector.ts +++ b/src/features/terminal/terminalEnvVarInjector.ts @@ -110,7 +110,6 @@ export class TerminalEnvVarInjector implements Disposable { await this.injectEnvironmentVariablesForWorkspace(workspaceFolder); } else { // No provided workspace - update all workspaces - this.envVarCollection.clear(); const workspaceFolders = workspace.workspaceFolders; if (!workspaceFolders || workspaceFolders.length === 0) { @@ -140,7 +139,6 @@ export class TerminalEnvVarInjector implements Disposable { // use scoped environment variable collection const envVarScope = this.getEnvironmentVariableCollectionScoped({ workspaceFolder }); - envVarScope.clear(); // Clear existing variables for this workspace // Check if env file injection is enabled const config = getConfiguration('python', workspaceUri); From f97991006b3b7460dea02f021dbe521eeb3578c1 Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Mon, 6 Oct 2025 00:25:40 -0700 Subject: [PATCH 3/4] Fix wsl not activating properly. (#896) Resolves: https://github.com/microsoft/vscode-python-environments/issues/865 --- .../terminal/shells/bash/bashStartup.ts | 4 +-- .../terminal/shells/common/shellUtils.ts | 11 +++++-- .../terminal/shells/fish/fishStartup.ts | 4 +-- .../terminal/shells/pwsh/pwshStartup.ts | 3 +- src/features/terminal/terminalManager.ts | 33 ++++++++++--------- 5 files changed, 33 insertions(+), 22 deletions(-) diff --git a/src/features/terminal/shells/bash/bashStartup.ts b/src/features/terminal/shells/bash/bashStartup.ts index e62ad1af..bbe98c85 100644 --- a/src/features/terminal/shells/bash/bashStartup.ts +++ b/src/features/terminal/shells/bash/bashStartup.ts @@ -5,7 +5,7 @@ import which from 'which'; import { traceError, traceInfo, traceVerbose } from '../../../../common/logging'; import { ShellConstants } from '../../../common/shellConstants'; import { hasStartupCode, insertStartupCode, removeStartupCode } from '../common/editUtils'; -import { shellIntegrationForActiveTerminal } from '../common/shellUtils'; +import { isWsl, shellIntegrationForActiveTerminal } from '../common/shellUtils'; import { ShellScriptEditState, ShellSetupState, ShellStartupScriptProvider } from '../startupProvider'; import { BASH_ENV_KEY, BASH_OLD_ENV_KEY, BASH_SCRIPT_VERSION, ZSH_ENV_KEY, ZSH_OLD_ENV_KEY } from './bashConstants'; @@ -68,7 +68,7 @@ async function isStartupSetup(profile: string, key: string): Promise { - if (shellIntegrationForActiveTerminal(name, profile)) { + if (shellIntegrationForActiveTerminal(name, profile) && !isWsl()) { removeStartup(profile, key); return true; } diff --git a/src/features/terminal/shells/common/shellUtils.ts b/src/features/terminal/shells/common/shellUtils.ts index 95e6f71a..20d310b5 100644 --- a/src/features/terminal/shells/common/shellUtils.ts +++ b/src/features/terminal/shells/common/shellUtils.ts @@ -103,10 +103,17 @@ export function shellIntegrationForActiveTerminal(name: string, profile?: string if (hasShellIntegration) { traceInfo( - `SHELL: Shell integration is available on your active terminal. Python activate scripts will be evaluated at shell integration level. - Skipping modification of ${name} profile at: ${profile}`, + `SHELL: Shell integration is available on your active terminal, with name ${name} and profile ${profile}. Python activate scripts will be evaluated at shell integration level, except in WSL.` ); return true; } return false; } + +export function isWsl(): boolean { + // WSL sets these environment variables + return !!(process.env.WSL_DISTRO_NAME || + process.env.WSL_INTEROP || + process.env.WSLENV); +} + diff --git a/src/features/terminal/shells/fish/fishStartup.ts b/src/features/terminal/shells/fish/fishStartup.ts index 37ac130b..395829e9 100644 --- a/src/features/terminal/shells/fish/fishStartup.ts +++ b/src/features/terminal/shells/fish/fishStartup.ts @@ -6,7 +6,7 @@ import which from 'which'; import { traceError, traceInfo, traceVerbose } from '../../../../common/logging'; import { ShellConstants } from '../../../common/shellConstants'; import { hasStartupCode, insertStartupCode, removeStartupCode } from '../common/editUtils'; -import { shellIntegrationForActiveTerminal } from '../common/shellUtils'; +import { isWsl, shellIntegrationForActiveTerminal } from '../common/shellUtils'; import { ShellScriptEditState, ShellSetupState, ShellStartupScriptProvider } from '../startupProvider'; import { FISH_ENV_KEY, FISH_OLD_ENV_KEY, FISH_SCRIPT_VERSION } from './fishConstants'; @@ -58,7 +58,7 @@ async function isStartupSetup(profilePath: string, key: string): Promise { try { - if (shellIntegrationForActiveTerminal('fish', profilePath)) { + if (shellIntegrationForActiveTerminal('fish', profilePath) && !isWsl()) { removeFishStartup(profilePath, key); return true; } diff --git a/src/features/terminal/shells/pwsh/pwshStartup.ts b/src/features/terminal/shells/pwsh/pwshStartup.ts index 6e0cf31e..3758dbb7 100644 --- a/src/features/terminal/shells/pwsh/pwshStartup.ts +++ b/src/features/terminal/shells/pwsh/pwshStartup.ts @@ -13,6 +13,7 @@ import { ShellConstants } from '../../../common/shellConstants'; import { hasStartupCode, insertStartupCode, removeStartupCode } from '../common/editUtils'; import { extractProfilePath, + isWsl, PROFILE_TAG_END, PROFILE_TAG_START, shellIntegrationForActiveTerminal, @@ -145,7 +146,7 @@ async function isPowerShellStartupSetup(shell: string, profile: string): Promise } async function setupPowerShellStartup(shell: string, profile: string): Promise { - if (shellIntegrationForActiveTerminal(shell, profile)) { + if (shellIntegrationForActiveTerminal(shell, profile) && !isWsl()) { removePowerShellStartup(shell, profile, POWERSHELL_OLD_ENV_KEY); removePowerShellStartup(shell, profile, POWERSHELL_ENV_KEY); return true; diff --git a/src/features/terminal/terminalManager.ts b/src/features/terminal/terminalManager.ts index dcba585c..81768dbd 100644 --- a/src/features/terminal/terminalManager.ts +++ b/src/features/terminal/terminalManager.ts @@ -16,7 +16,7 @@ import { getConfiguration, onDidChangeConfiguration } from '../../common/workspa import { isActivatableEnvironment } from '../common/activation'; import { identifyTerminalShell } from '../common/shellDetector'; import { getPythonApi } from '../pythonApi'; -import { shellIntegrationForActiveTerminal } from './shells/common/shellUtils'; +import { isWsl, shellIntegrationForActiveTerminal } from './shells/common/shellUtils'; import { ShellEnvsProvider, ShellSetupState, ShellStartupScriptProvider } from './shells/startupProvider'; import { handleSettingUpShellProfile } from './shellStartupSetupHandlers'; import { @@ -129,7 +129,9 @@ export class TerminalManagerImpl implements TerminalManager { await this.handleSetupCheck(shells); } } else { - traceVerbose(`Auto activation type changed to ${actType}`); + traceVerbose(`Auto activation type changed to ${actType}, we are cleaning up shell startup setup`); + // Teardown scripts when switching away from shell startup activation + await Promise.all(this.startupScriptProviders.map((p) => p.teardownScripts())); this.shellSetup.clear(); } } @@ -143,41 +145,42 @@ export class TerminalManagerImpl implements TerminalManager { private async handleSetupCheck(shellType: string | Set): Promise { const shellTypes = typeof shellType === 'string' ? new Set([shellType]) : shellType; const providers = this.startupScriptProviders.filter((p) => shellTypes.has(p.shellType)); - if (providers.length > 0) { + if (providers.length > 0) { const shellsToSetup: ShellStartupScriptProvider[] = []; await Promise.all( providers.map(async (p) => { const state = await p.isSetup(); + const currentSetup = (state === ShellSetupState.Setup); + // Check if we already processed this shell and the state hasn't changed if (this.shellSetup.has(p.shellType)) { - // This ensures modified scripts are detected even after initial setup const cachedSetup = this.shellSetup.get(p.shellType); - if ((state === ShellSetupState.Setup) !== cachedSetup) { - traceVerbose(`Shell profile for ${p.shellType} state changed, updating cache.`); - // State changed - clear cache and re-evaluate - this.shellSetup.delete(p.shellType); - } else { - traceVerbose(`Shell profile for ${p.shellType} already checked.`); + if (currentSetup === cachedSetup) { + traceVerbose(`Shell profile for ${p.shellType} already checked, state unchanged.`); return; } + traceVerbose(`Shell profile for ${p.shellType} state changed from ${cachedSetup} to ${currentSetup}, re-evaluating.`); } traceVerbose(`Checking shell profile for ${p.shellType}.`); if (state === ShellSetupState.NotSetup) { - if (shellIntegrationForActiveTerminal(p.name)) { + traceVerbose(`WSL detected: ${isWsl()}, Shell integration available: ${shellIntegrationForActiveTerminal(p.name)}`); + + if (shellIntegrationForActiveTerminal(p.name) && !isWsl()) { + // Shell integration available and NOT in WSL - skip setup await p.teardownScripts(); this.shellSetup.set(p.shellType, true); traceVerbose( - `Shell integration available for ${p.shellType}, skipping prompt, and profile modification.`, + `Shell integration available for ${p.shellType} (not WSL), skipping prompt, and profile modification.`, ); } else { - // No shell integration, mark for setup + // WSL (regardless of integration) OR no shell integration - needs setup this.shellSetup.set(p.shellType, false); shellsToSetup.push(p); traceVerbose( - `Shell integration is NOT avaoiable. Shell profile for ${p.shellType} is not setup.`, + `Shell integration is NOT available. Shell profile for ${p.shellType} is not setup.`, ); } } else if (state === ShellSetupState.Setup) { - if (shellIntegrationForActiveTerminal(p.name)) { + if (shellIntegrationForActiveTerminal(p.name) && !isWsl()) { await p.teardownScripts(); traceVerbose( `Shell integration available for ${p.shellType}, removed profile script in favor of shell integration.`, From 91ead9ee937922bb76c89840118da87e0181ae98 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Tue, 7 Oct 2025 13:04:53 -0700 Subject: [PATCH 4/4] Fix bug with getAutoActivationType (#897) fixes https://github.com/microsoft/vscode-python-environments/issues/894 --- src/features/terminal/utils.ts | 38 +- src/test/features/terminal/utils.unit.test.ts | 356 ++++++++++++++++++ 2 files changed, 381 insertions(+), 13 deletions(-) create mode 100644 src/test/features/terminal/utils.unit.test.ts diff --git a/src/features/terminal/utils.ts b/src/features/terminal/utils.ts index 8acc125c..9476ac10 100644 --- a/src/features/terminal/utils.ts +++ b/src/features/terminal/utils.ts @@ -103,32 +103,44 @@ export type AutoActivationType = 'off' | 'command' | 'shellStartup'; * - 'off': Auto-activation is disabled * * Priority order: - * 1. python-envs.terminal.autoActivationType setting - * 2. python.terminal.activateEnvironment setting (if false updates python-envs.terminal.autoActivationType) + * 1. python-envs.terminal.autoActivationType + * a. globalRemoteValue + * b. globalLocalValue + * c. globalValue + * 2. python.terminal.activateEnvironment setting (if false, returns 'off' & sets autoActivationType to 'off') * 3. Default to 'command' if no setting is found * * @returns {AutoActivationType} The determined auto-activation type */ export function getAutoActivationType(): AutoActivationType { const pyEnvsConfig = getConfiguration('python-envs'); + const pyEnvsActivationType = pyEnvsConfig.inspect('terminal.autoActivationType'); - const pyEnvsActivationType = pyEnvsConfig.get( - 'terminal.autoActivationType', - undefined, - ); - if (pyEnvsActivationType !== undefined) { - return pyEnvsActivationType; + if (pyEnvsActivationType) { + // Priority order: globalRemoteValue > globalLocalValue > globalValue + const activationType = pyEnvsActivationType as Record; + + if ('globalRemoteValue' in pyEnvsActivationType && activationType.globalRemoteValue !== undefined) { + return activationType.globalRemoteValue as AutoActivationType; + } + if ('globalLocalValue' in pyEnvsActivationType && activationType.globalLocalValue !== undefined) { + return activationType.globalLocalValue as AutoActivationType; + } + if (pyEnvsActivationType.globalValue !== undefined) { + return pyEnvsActivationType.globalValue; + } } + // If none of the python-envs settings are defined, check the legacy python setting const pythonConfig = getConfiguration('python'); const pythonActivateSetting = pythonConfig.get('terminal.activateEnvironment', undefined); - if (pythonActivateSetting !== undefined) { - if (pythonActivateSetting === false) { - pyEnvsConfig.set('terminal.autoActivationType', ACT_TYPE_OFF); - } - return pythonActivateSetting ? ACT_TYPE_COMMAND : ACT_TYPE_OFF; + if (pythonActivateSetting === false) { + // Set autoActivationType to 'off' if python.terminal.activateEnvironment is false + pyEnvsConfig.update('terminal.autoActivationType', ACT_TYPE_OFF); + return ACT_TYPE_OFF; } + // Default to 'command' if no settings are found or if pythonActivateSetting is true/undefined return ACT_TYPE_COMMAND; } diff --git a/src/test/features/terminal/utils.unit.test.ts b/src/test/features/terminal/utils.unit.test.ts new file mode 100644 index 00000000..d97030c0 --- /dev/null +++ b/src/test/features/terminal/utils.unit.test.ts @@ -0,0 +1,356 @@ +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import * as workspaceApis from '../../../common/workspace.apis'; +import { + ACT_TYPE_COMMAND, + ACT_TYPE_OFF, + ACT_TYPE_SHELL, + AutoActivationType, + getAutoActivationType, +} from '../../../features/terminal/utils'; + +interface MockWorkspaceConfig { + get: sinon.SinonStub; + inspect: sinon.SinonStub; + update: sinon.SinonStub; +} + +suite('Terminal Utils - getAutoActivationType', () => { + let mockGetConfiguration: sinon.SinonStub; + let pyEnvsConfig: MockWorkspaceConfig; + let pythonConfig: MockWorkspaceConfig; + + setup(() => { + // Initialize mocks + mockGetConfiguration = sinon.stub(workspaceApis, 'getConfiguration'); + + // Create mock configuration objects + pyEnvsConfig = { + get: sinon.stub(), + inspect: sinon.stub(), + update: sinon.stub(), + }; + + pythonConfig = { + get: sinon.stub(), + inspect: sinon.stub(), + update: sinon.stub(), + }; + + // Set up default configuration returns + mockGetConfiguration.withArgs('python-envs').returns(pyEnvsConfig); + mockGetConfiguration.withArgs('python').returns(pythonConfig); + }); + + teardown(() => { + sinon.restore(); + }); + + suite('Priority Order Tests', () => { + test('should return globalRemoteValue when set (highest priority)', () => { + // Mock - globalRemoteValue is set + const mockInspectResult = { + globalRemoteValue: ACT_TYPE_SHELL, + globalLocalValue: ACT_TYPE_COMMAND, + globalValue: ACT_TYPE_OFF, + }; + pyEnvsConfig.inspect.withArgs('terminal.autoActivationType').returns(mockInspectResult); + + // Run + const result = getAutoActivationType(); + + // Assert + assert.strictEqual(result, ACT_TYPE_SHELL, 'Should return globalRemoteValue when set'); + }); + + test('should return globalLocalValue when globalRemoteValue is undefined', () => { + // Mock - globalRemoteValue is undefined, globalLocalValue is set + const mockInspectResult = { + globalRemoteValue: undefined, + globalLocalValue: ACT_TYPE_SHELL, + globalValue: ACT_TYPE_OFF, + }; + pyEnvsConfig.inspect.withArgs('terminal.autoActivationType').returns(mockInspectResult); + + // Run + const result = getAutoActivationType(); + + // Assert + assert.strictEqual( + result, + ACT_TYPE_SHELL, + 'Should return globalLocalValue when globalRemoteValue is undefined', + ); + }); + + test('should return globalValue when both globalRemoteValue and globalLocalValue are undefined', () => { + // Mock - only globalValue is set + const mockInspectResult = { + globalRemoteValue: undefined, + globalLocalValue: undefined, + globalValue: ACT_TYPE_OFF, + }; + pyEnvsConfig.inspect.withArgs('terminal.autoActivationType').returns(mockInspectResult); + + // Run + const result = getAutoActivationType(); + + // Assert + assert.strictEqual( + result, + ACT_TYPE_OFF, + 'Should return globalValue when higher priority values are undefined', + ); + }); + + test('should ignore globalLocalValue and globalValue when globalRemoteValue exists', () => { + // Mock - all values set, should prioritize globalRemoteValue + const mockInspectResult = { + globalRemoteValue: ACT_TYPE_OFF, + globalLocalValue: ACT_TYPE_SHELL, + globalValue: ACT_TYPE_COMMAND, + }; + pyEnvsConfig.inspect.withArgs('terminal.autoActivationType').returns(mockInspectResult); + + // Run + const result = getAutoActivationType(); + + // Assert + assert.strictEqual(result, ACT_TYPE_OFF, 'Should prioritize globalRemoteValue over other values'); + }); + + test('should ignore globalValue when globalLocalValue exists', () => { + // Mock - globalLocalValue and globalValue set, should prioritize globalLocalValue + const mockInspectResult = { + globalLocalValue: ACT_TYPE_SHELL, + globalValue: ACT_TYPE_COMMAND, + }; + pyEnvsConfig.inspect.withArgs('terminal.autoActivationType').returns(mockInspectResult); + + // Run + const result = getAutoActivationType(); + + // Assert + assert.strictEqual(result, ACT_TYPE_SHELL, 'Should prioritize globalLocalValue over globalValue'); + }); + }); + + suite('Custom Properties Handling', () => { + test('should handle case when globalRemoteValue property does not exist', () => { + // Mock - standard VS Code inspection result without custom properties + const mockInspectResult = { + key: 'terminal.autoActivationType', + globalValue: ACT_TYPE_SHELL, + }; + pyEnvsConfig.inspect.withArgs('terminal.autoActivationType').returns(mockInspectResult); + + // Run + const result = getAutoActivationType(); + + // Assert + assert.strictEqual(result, ACT_TYPE_SHELL, 'Should return globalValue when custom properties do not exist'); + }); + + test('should handle case when globalLocalValue property does not exist', () => { + // Mock - inspection result without globalLocalValue property + const mockInspectResult = { + key: 'terminal.autoActivationType', + globalValue: ACT_TYPE_COMMAND, + }; + pyEnvsConfig.inspect.withArgs('terminal.autoActivationType').returns(mockInspectResult); + + // Run + const result = getAutoActivationType(); + + // Assert + assert.strictEqual( + result, + ACT_TYPE_COMMAND, + 'Should return globalValue when globalLocalValue property does not exist', + ); + }); + + test('should handle case when custom properties exist but are undefined', () => { + // Mock - custom properties exist but have undefined values + const mockInspectResult = { + globalRemoteValue: undefined, + globalLocalValue: undefined, + globalValue: ACT_TYPE_OFF, + }; + pyEnvsConfig.inspect.withArgs('terminal.autoActivationType').returns(mockInspectResult); + + // Run + const result = getAutoActivationType(); + + // Assert + assert.strictEqual( + result, + ACT_TYPE_OFF, + 'Should fall back to globalValue when custom properties are undefined', + ); + }); + }); + + suite('Legacy Python Setting Fallback', () => { + test('should return ACT_TYPE_OFF and update config when python.terminal.activateEnvironment is false', () => { + // Mock - no python-envs settings, python.terminal.activateEnvironment is false + pyEnvsConfig.inspect.withArgs('terminal.autoActivationType').returns(undefined); + pythonConfig.get.withArgs('terminal.activateEnvironment', undefined).returns(false); + + // Run + const result = getAutoActivationType(); + + // Assert + assert.strictEqual(result, ACT_TYPE_OFF, 'Should return ACT_TYPE_OFF when legacy setting is false'); + assert.ok( + pyEnvsConfig.update.calledWithExactly('terminal.autoActivationType', ACT_TYPE_OFF), + 'Should update python-envs config to ACT_TYPE_OFF', + ); + }); + + test('should return ACT_TYPE_COMMAND when python.terminal.activateEnvironment is true', () => { + // Mock - no python-envs settings, python.terminal.activateEnvironment is true + pyEnvsConfig.inspect.withArgs('terminal.autoActivationType').returns(undefined); + pythonConfig.get.withArgs('terminal.activateEnvironment', undefined).returns(true); + + // Run + const result = getAutoActivationType(); + + // Assert + assert.strictEqual(result, ACT_TYPE_COMMAND, 'Should return ACT_TYPE_COMMAND when legacy setting is true'); + assert.ok( + pyEnvsConfig.update.notCalled, + 'Should not update python-envs config when legacy setting is true', + ); + }); + + test('should return ACT_TYPE_COMMAND when python.terminal.activateEnvironment is undefined', () => { + // Mock - no python-envs settings, python.terminal.activateEnvironment is undefined + pyEnvsConfig.inspect.withArgs('terminal.autoActivationType').returns(undefined); + pythonConfig.get.withArgs('terminal.activateEnvironment', undefined).returns(undefined); + + // Run + const result = getAutoActivationType(); + + // Assert + assert.strictEqual(result, ACT_TYPE_COMMAND, 'Should return ACT_TYPE_COMMAND when no settings are found'); + assert.ok( + pyEnvsConfig.update.notCalled, + 'Should not update python-envs config when no legacy setting exists', + ); + }); + }); + + suite('Fallback Scenarios', () => { + test('should return ACT_TYPE_COMMAND when no configuration exists', () => { + // Mock - no configurations exist + pyEnvsConfig.inspect.withArgs('terminal.autoActivationType').returns(undefined); + pythonConfig.get.withArgs('terminal.activateEnvironment', undefined).returns(undefined); + + // Run + const result = getAutoActivationType(); + + // Assert + assert.strictEqual( + result, + ACT_TYPE_COMMAND, + 'Should return default ACT_TYPE_COMMAND when no configurations exist', + ); + }); + + test('should return ACT_TYPE_COMMAND when python-envs config exists but all values are undefined', () => { + // Mock - python-envs config exists but all relevant values are undefined + const mockInspectResult = { + key: 'terminal.autoActivationType', + globalValue: undefined, + workspaceValue: undefined, + workspaceFolderValue: undefined, + }; + pyEnvsConfig.inspect.withArgs('terminal.autoActivationType').returns(mockInspectResult); + pythonConfig.get.withArgs('terminal.activateEnvironment', undefined).returns(undefined); + + // Run + const result = getAutoActivationType(); + + // Assert + assert.strictEqual( + result, + ACT_TYPE_COMMAND, + 'Should return default when python-envs config exists but values are undefined', + ); + }); + + test('should prioritize python-envs settings over legacy python settings', () => { + // Mock - python-envs has globalValue, python has conflicting setting + const mockInspectResult = { + globalValue: ACT_TYPE_SHELL, + }; + pyEnvsConfig.inspect.withArgs('terminal.autoActivationType').returns(mockInspectResult); + pythonConfig.get.withArgs('terminal.activateEnvironment', undefined).returns(false); + + // Run + const result = getAutoActivationType(); + + // Assert + assert.strictEqual( + result, + ACT_TYPE_SHELL, + 'Should prioritize python-envs globalValue over legacy python setting', + ); + assert.ok( + pyEnvsConfig.update.notCalled, + 'Should not update python-envs config when it already has a value', + ); + }); + }); + + suite('Edge Cases', () => { + test('should handle null inspect result', () => { + // Mock - inspect returns null + pyEnvsConfig.inspect.withArgs('terminal.autoActivationType').returns(null); + pythonConfig.get.withArgs('terminal.activateEnvironment', undefined).returns(undefined); + + // Run + const result = getAutoActivationType(); + + // Assert + assert.strictEqual(result, ACT_TYPE_COMMAND, 'Should handle null inspect result gracefully'); + }); + + test('should handle empty object inspect result', () => { + // Mock - inspect returns empty object + pyEnvsConfig.inspect.withArgs('terminal.autoActivationType').returns({}); + pythonConfig.get.withArgs('terminal.activateEnvironment', undefined).returns(undefined); + + // Run + const result = getAutoActivationType(); + + // Assert + assert.strictEqual(result, ACT_TYPE_COMMAND, 'Should handle empty inspect result gracefully'); + }); + + test('should handle all AutoActivationType values correctly', () => { + const testCases: { input: AutoActivationType; expected: AutoActivationType }[] = [ + { input: ACT_TYPE_COMMAND, expected: ACT_TYPE_COMMAND }, + { input: ACT_TYPE_SHELL, expected: ACT_TYPE_SHELL }, + { input: ACT_TYPE_OFF, expected: ACT_TYPE_OFF }, + ]; + + testCases.forEach(({ input, expected }) => { + // Reset stubs for each test case + pyEnvsConfig.inspect.resetHistory(); + pythonConfig.get.resetHistory(); + + // Mock - set globalValue to test input + const mockInspectResult = { globalValue: input }; + pyEnvsConfig.inspect.withArgs('terminal.autoActivationType').returns(mockInspectResult); + + // Run + const result = getAutoActivationType(); + + // Assert + assert.strictEqual(result, expected, `Should handle ${input} value correctly`); + }); + }); + }); +});