From c9b09b98dbd102b21ef81a7f4075931af5ece8f3 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Wed, 18 Feb 2026 16:42:49 -0800 Subject: [PATCH 1/4] fix: use conda.sh for Git Bash activation on Windows (Fixes #1247) --- src/managers/conda/condaSourcingUtils.ts | 47 +++++++++++++++++------- src/managers/conda/condaUtils.ts | 12 +++++- 2 files changed, 45 insertions(+), 14 deletions(-) diff --git a/src/managers/conda/condaSourcingUtils.ts b/src/managers/conda/condaSourcingUtils.ts index a8f888b4..76bf7dad 100644 --- a/src/managers/conda/condaSourcingUtils.ts +++ b/src/managers/conda/condaSourcingUtils.ts @@ -6,6 +6,19 @@ import * as path from 'path'; import { traceError, traceInfo, traceVerbose } from '../../common/logging'; import { isWindows } from '../../common/utils/platformUtils'; +/** + * Shell-specific sourcing scripts for conda activation. + * Each field is optional since not all scripts may be available on all systems. + */ +export interface ShellSourcingScripts { + /** PowerShell hook script (conda-hook.ps1) */ + ps1?: string; + /** Bash/sh initialization script (conda.sh) */ + sh?: string; + /** Windows CMD batch file (activate.bat) */ + cmd?: string; +} + /** * Represents the status of conda sourcing in the current environment */ @@ -16,14 +29,14 @@ export class CondaSourcingStatus { * @param condaFolder Path to the conda installation folder (derived from condaPath) * @param isActiveOnLaunch Whether conda was activated before VS Code launch * @param globalSourcingScript Path to the global sourcing script (if exists) - * @param shellSourcingScripts List of paths to shell-specific sourcing scripts + * @param shellSourcingScripts Shell-specific sourcing scripts (if found) */ constructor( public readonly condaPath: string, public readonly condaFolder: string, public isActiveOnLaunch?: boolean, public globalSourcingScript?: string, - public shellSourcingScripts?: string[], + public shellSourcingScripts?: ShellSourcingScripts, ) {} /** @@ -40,15 +53,23 @@ export class CondaSourcingStatus { lines.push(`├─ Global Sourcing Script: ${this.globalSourcingScript}`); } - if (this.shellSourcingScripts?.length) { - lines.push('└─ Shell-specific Sourcing Scripts:'); - this.shellSourcingScripts.forEach((script, index, array) => { - const isLast = index === array.length - 1; - if (script) { - // Only include scripts that exist - lines.push(` ${isLast ? '└─' : '├─'} ${script}`); - } - }); + if (this.shellSourcingScripts) { + const scripts = this.shellSourcingScripts; + const entries = [ + scripts.ps1 && `PowerShell: ${scripts.ps1}`, + scripts.sh && `Bash/sh: ${scripts.sh}`, + scripts.cmd && `CMD: ${scripts.cmd}`, + ].filter(Boolean); + + if (entries.length > 0) { + lines.push('└─ Shell-specific Sourcing Scripts:'); + entries.forEach((entry, index, array) => { + const isLast = index === array.length - 1; + lines.push(` ${isLast ? '└─' : '├─'} ${entry}`); + }); + } else { + lines.push('└─ No Shell-specific Sourcing Scripts Found'); + } } else { lines.push('└─ No Shell-specific Sourcing Scripts Found'); } @@ -120,7 +141,7 @@ export async function findGlobalSourcingScript(condaFolder: string): Promise { +export async function findShellSourcingScripts(sourcingStatus: CondaSourcingStatus): Promise { const logs: string[] = []; logs.push('=== Conda Sourcing Shell Script Search ==='); @@ -170,7 +191,7 @@ export async function findShellSourcingScripts(sourcingStatus: CondaSourcingStat traceVerbose(logs.join('\n')); } - return [ps1Script, shScript, cmdActivate] as string[]; + return { ps1: ps1Script, sh: shScript, cmd: cmdActivate }; } /** diff --git a/src/managers/conda/condaUtils.ts b/src/managers/conda/condaUtils.ts index ee45911b..a22a9168 100644 --- a/src/managers/conda/condaUtils.ts +++ b/src/managers/conda/condaUtils.ts @@ -512,10 +512,16 @@ async function buildShellActivationMapForConda( // P3: Handle Windows specifically ;this is carryover from vscode-python if (isWindows()) { logs.push('✓ Using Windows-specific activation configuration'); + // Get conda.sh for bash-based shells (Git Bash, WSL bash) + const condaShPath = envManager.sourcingInformation.shellSourcingScripts?.sh; + if (!condaShPath) { + logs.push('conda.sh not found, falling back to global sourcing script for bash activation'); + } shellMaps = await windowsExceptionGenerateConfig( preferredSourcingPath, envIdentifier, envManager.sourcingInformation.condaFolder, + condaShPath, ); return shellMaps; } @@ -580,6 +586,7 @@ async function windowsExceptionGenerateConfig( sourceInitPath: string, prefix: string, condaFolder: string, + condaShPath?: string, ): Promise { const shellActivation: Map = new Map(); const shellDeactivation: Map = new Map(); @@ -593,7 +600,10 @@ async function windowsExceptionGenerateConfig( const pwshActivate = [{ executable: activation }, { executable: 'conda', args: ['activate', quotedPrefix] }]; const cmdActivate = [{ executable: sourceInitPath }, { executable: 'conda', args: ['activate', quotedPrefix] }]; - const bashActivate = [{ executable: 'source', args: [sourceInitPath.replace(/\\/g, '/'), quotedPrefix] }]; + // Use conda.sh for bash-based shells (Git Bash) instead of activate.bat + // conda.sh is the proper initialization script for bash shells + const bashSourcePath = condaShPath ?? sourceInitPath; + const bashActivate = [{ executable: 'source', args: [bashSourcePath.replace(/\\/g, '/'), quotedPrefix] }]; traceVerbose( `Windows activation commands: PowerShell: ${JSON.stringify(pwshActivate)}, From 521578b61a2020d2848132375274c0bf75432657 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Wed, 18 Feb 2026 16:53:48 -0800 Subject: [PATCH 2/4] fix: address review feedback - use two commands for conda.sh and add tests --- src/managers/conda/condaUtils.ts | 21 +- .../condaUtils.windowsActivation.unit.test.ts | 179 ++++++++++++++++++ 2 files changed, 195 insertions(+), 5 deletions(-) create mode 100644 src/test/managers/conda/condaUtils.windowsActivation.unit.test.ts diff --git a/src/managers/conda/condaUtils.ts b/src/managers/conda/condaUtils.ts index a22a9168..33df12df 100644 --- a/src/managers/conda/condaUtils.ts +++ b/src/managers/conda/condaUtils.ts @@ -582,7 +582,12 @@ async function generateShellActivationMapFromConfig( return { shellActivation, shellDeactivation }; } -async function windowsExceptionGenerateConfig( +/** + * Generates shell-specific activation configuration for Windows. + * Handles PowerShell, CMD, and Git Bash with appropriate scripts. + * @internal Exported for testing + */ +export async function windowsExceptionGenerateConfig( sourceInitPath: string, prefix: string, condaFolder: string, @@ -600,10 +605,16 @@ async function windowsExceptionGenerateConfig( const pwshActivate = [{ executable: activation }, { executable: 'conda', args: ['activate', quotedPrefix] }]; const cmdActivate = [{ executable: sourceInitPath }, { executable: 'conda', args: ['activate', quotedPrefix] }]; - // Use conda.sh for bash-based shells (Git Bash) instead of activate.bat - // conda.sh is the proper initialization script for bash shells - const bashSourcePath = condaShPath ?? sourceInitPath; - const bashActivate = [{ executable: 'source', args: [bashSourcePath.replace(/\\/g, '/'), quotedPrefix] }]; + // When condaShPath is available, it is an initialization script (conda.sh) and does not + // itself activate an environment. In that case, first source conda.sh, then + // run "conda activate ". When falling back to sourceInitPath, + // retain the existing single "source " behavior. + const bashActivate: PythonCommandRunConfiguration[] = condaShPath + ? [ + { executable: 'source', args: [condaShPath.replace(/\\/g, '/')] }, + { executable: 'conda', args: ['activate', quotedPrefix] }, + ] + : [{ executable: 'source', args: [sourceInitPath.replace(/\\/g, '/'), quotedPrefix] }]; traceVerbose( `Windows activation commands: PowerShell: ${JSON.stringify(pwshActivate)}, diff --git a/src/test/managers/conda/condaUtils.windowsActivation.unit.test.ts b/src/test/managers/conda/condaUtils.windowsActivation.unit.test.ts new file mode 100644 index 00000000..60315ff2 --- /dev/null +++ b/src/test/managers/conda/condaUtils.windowsActivation.unit.test.ts @@ -0,0 +1,179 @@ +import assert from 'assert'; +import * as sinon from 'sinon'; +import { ShellConstants } from '../../../features/common/shellConstants'; +import * as condaSourcingUtils from '../../../managers/conda/condaSourcingUtils'; +import { windowsExceptionGenerateConfig } from '../../../managers/conda/condaUtils'; + +/** + * Tests for windowsExceptionGenerateConfig - Windows shell activation commands. + * + * Key behavior tested: + * - Git Bash uses conda.sh (initialization script) + conda activate when condaShPath is available + * - Git Bash falls back to source when condaShPath is not available + * - PowerShell uses ps1 hook + conda activate + * - CMD uses activate.bat + conda activate + */ +suite('Conda Utils - windowsExceptionGenerateConfig', () => { + let getCondaHookPs1PathStub: sinon.SinonStub; + + setup(() => { + // Mock getCondaHookPs1Path to avoid filesystem access + getCondaHookPs1PathStub = sinon.stub(condaSourcingUtils, 'getCondaHookPs1Path'); + }); + + teardown(() => { + sinon.restore(); + }); + + suite('Git Bash activation with conda.sh', () => { + test('Uses source conda.sh + conda activate when condaShPath is provided', async () => { + // Arrange + getCondaHookPs1PathStub.resolves('C:\\conda\\shell\\condabin\\conda-hook.ps1'); + const sourceInitPath = 'C:\\conda\\Scripts\\activate.bat'; + const prefix = 'myenv'; + const condaFolder = 'C:\\conda'; + const condaShPath = 'C:\\conda\\etc\\profile.d\\conda.sh'; + + // Act + const result = await windowsExceptionGenerateConfig(sourceInitPath, prefix, condaFolder, condaShPath); + + // Assert + const gitBashActivation = result.shellActivation.get(ShellConstants.GITBASH); + assert.ok(gitBashActivation, 'Git Bash activation should be defined'); + assert.strictEqual(gitBashActivation.length, 2, 'Should have 2 commands: source conda.sh + conda activate'); + + // First command: source conda.sh (no env arg - it's an initialization script) + assert.strictEqual(gitBashActivation[0].executable, 'source'); + assert.deepStrictEqual(gitBashActivation[0].args, ['C:/conda/etc/profile.d/conda.sh']); + + // Second command: conda activate + assert.strictEqual(gitBashActivation[1].executable, 'conda'); + assert.deepStrictEqual(gitBashActivation[1].args, ['activate', 'myenv']); + }); + + test('Falls back to single source command when condaShPath is undefined', async () => { + // Arrange + getCondaHookPs1PathStub.resolves(undefined); + const sourceInitPath = 'C:\\conda\\Scripts\\activate.bat'; + const prefix = 'myenv'; + const condaFolder = 'C:\\conda'; + const condaShPath = undefined; // Not available + + // Act + const result = await windowsExceptionGenerateConfig(sourceInitPath, prefix, condaFolder, condaShPath); + + // Assert + const gitBashActivation = result.shellActivation.get(ShellConstants.GITBASH); + assert.ok(gitBashActivation, 'Git Bash activation should be defined'); + assert.strictEqual(gitBashActivation.length, 1, 'Should have 1 command when condaShPath not available'); + + // Single command: source + assert.strictEqual(gitBashActivation[0].executable, 'source'); + assert.deepStrictEqual(gitBashActivation[0].args, ['C:/conda/Scripts/activate.bat', 'myenv']); + }); + + test('Converts Windows backslashes to forward slashes for bash', async () => { + // Arrange + getCondaHookPs1PathStub.resolves(undefined); + const condaShPath = 'C:\\Tools\\miniforge3\\etc\\profile.d\\conda.sh'; + + // Act + const result = await windowsExceptionGenerateConfig( + 'C:\\Tools\\miniforge3\\Scripts\\activate.bat', + 'pipes', + 'C:\\Tools\\miniforge3', + condaShPath, + ); + + // Assert + const gitBashActivation = result.shellActivation.get(ShellConstants.GITBASH); + assert.ok(gitBashActivation, 'Git Bash activation should be defined'); + // Verify forward slashes are used + const sourcePath = gitBashActivation[0].args?.[0]; + assert.ok(sourcePath, 'Source path should be defined'); + assert.ok(!sourcePath.includes('\\'), 'Path should not contain backslashes'); + assert.ok(sourcePath.includes('/'), 'Path should contain forward slashes'); + }); + }); + + suite('PowerShell activation', () => { + test('Uses ps1 hook when available', async () => { + // Arrange + const ps1HookPath = 'C:\\conda\\shell\\condabin\\conda-hook.ps1'; + getCondaHookPs1PathStub.resolves(ps1HookPath); + + // Act + const result = await windowsExceptionGenerateConfig( + 'C:\\conda\\Scripts\\activate.bat', + 'myenv', + 'C:\\conda', + undefined, + ); + + // Assert + const pwshActivation = result.shellActivation.get(ShellConstants.PWSH); + assert.ok(pwshActivation, 'PowerShell activation should be defined'); + assert.strictEqual(pwshActivation.length, 2, 'Should have 2 commands'); + assert.strictEqual(pwshActivation[0].executable, ps1HookPath); + assert.strictEqual(pwshActivation[1].executable, 'conda'); + assert.deepStrictEqual(pwshActivation[1].args, ['activate', 'myenv']); + }); + + test('Falls back to sourceInitPath when ps1 hook not found', async () => { + // Arrange + getCondaHookPs1PathStub.resolves(undefined); + const sourceInitPath = 'C:\\conda\\Scripts\\activate.bat'; + + // Act + const result = await windowsExceptionGenerateConfig(sourceInitPath, 'myenv', 'C:\\conda', undefined); + + // Assert + const pwshActivation = result.shellActivation.get(ShellConstants.PWSH); + assert.ok(pwshActivation, 'PowerShell activation should be defined'); + assert.strictEqual(pwshActivation[0].executable, sourceInitPath); + }); + }); + + suite('CMD activation', () => { + test('Uses activate.bat + conda activate', async () => { + // Arrange + getCondaHookPs1PathStub.resolves(undefined); + const sourceInitPath = 'C:\\conda\\Scripts\\activate.bat'; + + // Act + const result = await windowsExceptionGenerateConfig(sourceInitPath, 'myenv', 'C:\\conda', undefined); + + // Assert + const cmdActivation = result.shellActivation.get(ShellConstants.CMD); + assert.ok(cmdActivation, 'CMD activation should be defined'); + assert.strictEqual(cmdActivation.length, 2, 'Should have 2 commands'); + assert.strictEqual(cmdActivation[0].executable, sourceInitPath); + assert.strictEqual(cmdActivation[1].executable, 'conda'); + assert.deepStrictEqual(cmdActivation[1].args, ['activate', 'myenv']); + }); + }); + + suite('Deactivation commands', () => { + test('All shells use conda deactivate', async () => { + // Arrange + getCondaHookPs1PathStub.resolves(undefined); + + // Act + const result = await windowsExceptionGenerateConfig( + 'C:\\conda\\Scripts\\activate.bat', + 'myenv', + 'C:\\conda', + undefined, + ); + + // Assert: All shells should have conda deactivate + for (const shell of [ShellConstants.GITBASH, ShellConstants.CMD, ShellConstants.PWSH]) { + const deactivation = result.shellDeactivation.get(shell); + assert.ok(deactivation, `${shell} deactivation should be defined`); + assert.strictEqual(deactivation.length, 1, `${shell} should have 1 deactivation command`); + assert.strictEqual(deactivation[0].executable, 'conda'); + assert.deepStrictEqual(deactivation[0].args, ['deactivate']); + } + }); + }); +}); From f2c73f86941754433e9ce5dc597985948589eaa7 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Wed, 18 Feb 2026 17:04:16 -0800 Subject: [PATCH 3/4] fix: address second round of review feedback --- src/managers/conda/condaUtils.ts | 30 +++++++++++------ .../condaUtils.windowsActivation.unit.test.ts | 32 ++++++++++++++++--- 2 files changed, 47 insertions(+), 15 deletions(-) diff --git a/src/managers/conda/condaUtils.ts b/src/managers/conda/condaUtils.ts index 33df12df..a34bbf87 100644 --- a/src/managers/conda/condaUtils.ts +++ b/src/managers/conda/condaUtils.ts @@ -512,10 +512,10 @@ async function buildShellActivationMapForConda( // P3: Handle Windows specifically ;this is carryover from vscode-python if (isWindows()) { logs.push('✓ Using Windows-specific activation configuration'); - // Get conda.sh for bash-based shells (Git Bash, WSL bash) + // Get conda.sh for bash-based shells on Windows (e.g., Git Bash) const condaShPath = envManager.sourcingInformation.shellSourcingScripts?.sh; if (!condaShPath) { - logs.push('conda.sh not found, falling back to global sourcing script for bash activation'); + logs.push('conda.sh not found, using preferred sourcing script path for bash activation'); } shellMaps = await windowsExceptionGenerateConfig( preferredSourcingPath, @@ -607,14 +607,24 @@ export async function windowsExceptionGenerateConfig( // When condaShPath is available, it is an initialization script (conda.sh) and does not // itself activate an environment. In that case, first source conda.sh, then - // run "conda activate ". When falling back to sourceInitPath, - // retain the existing single "source " behavior. - const bashActivate: PythonCommandRunConfiguration[] = condaShPath - ? [ - { executable: 'source', args: [condaShPath.replace(/\\/g, '/')] }, - { executable: 'conda', args: ['activate', quotedPrefix] }, - ] - : [{ executable: 'source', args: [sourceInitPath.replace(/\\/g, '/'), quotedPrefix] }]; + // run "conda activate ". + // When falling back to sourceInitPath, only emit a bash "source" command if the script + // is bash-compatible; on Windows, sourceInitPath may point to "activate.bat", which + // cannot be sourced by Git Bash, so in that case we skip emitting a Git Bash activation. + let bashActivate: PythonCommandRunConfiguration[]; + if (condaShPath) { + bashActivate = [ + { executable: 'source', args: [condaShPath.replace(/\\/g, '/')] }, + { executable: 'conda', args: ['activate', quotedPrefix] }, + ]; + } else if (sourceInitPath.toLowerCase().endsWith('.bat')) { + traceVerbose( + `Skipping Git Bash activation fallback because sourceInitPath is a batch script: ${sourceInitPath}`, + ); + bashActivate = []; + } else { + bashActivate = [{ executable: 'source', args: [sourceInitPath.replace(/\\/g, '/'), quotedPrefix] }]; + } traceVerbose( `Windows activation commands: PowerShell: ${JSON.stringify(pwshActivate)}, diff --git a/src/test/managers/conda/condaUtils.windowsActivation.unit.test.ts b/src/test/managers/conda/condaUtils.windowsActivation.unit.test.ts index 60315ff2..ef5c7d93 100644 --- a/src/test/managers/conda/condaUtils.windowsActivation.unit.test.ts +++ b/src/test/managers/conda/condaUtils.windowsActivation.unit.test.ts @@ -9,7 +9,8 @@ import { windowsExceptionGenerateConfig } from '../../../managers/conda/condaUti * * Key behavior tested: * - Git Bash uses conda.sh (initialization script) + conda activate when condaShPath is available - * - Git Bash falls back to source when condaShPath is not available + * - Git Bash skips activation when condaShPath is not available and sourceInitPath is .bat + * - Git Bash falls back to source when condaShPath is not available and source is not .bat * - PowerShell uses ps1 hook + conda activate * - CMD uses activate.bat + conda activate */ @@ -51,8 +52,8 @@ suite('Conda Utils - windowsExceptionGenerateConfig', () => { assert.deepStrictEqual(gitBashActivation[1].args, ['activate', 'myenv']); }); - test('Falls back to single source command when condaShPath is undefined', async () => { - // Arrange + test('Skips Git Bash activation when condaShPath is undefined and sourceInitPath is .bat', async () => { + // Arrange: sourceInitPath is a .bat file which Git Bash cannot source getCondaHookPs1PathStub.resolves(undefined); const sourceInitPath = 'C:\\conda\\Scripts\\activate.bat'; const prefix = 'myenv'; @@ -62,14 +63,35 @@ suite('Conda Utils - windowsExceptionGenerateConfig', () => { // Act const result = await windowsExceptionGenerateConfig(sourceInitPath, prefix, condaFolder, condaShPath); + // Assert: Git Bash activation should be empty since .bat cannot be sourced + const gitBashActivation = result.shellActivation.get(ShellConstants.GITBASH); + assert.ok(gitBashActivation, 'Git Bash activation should be defined'); + assert.strictEqual( + gitBashActivation.length, + 0, + 'Git Bash activation should be empty when sourceInitPath is .bat', + ); + }); + + test('Falls back to single source command when condaShPath is undefined and source is not .bat', async () => { + // Arrange: sourceInitPath is a bash-compatible script (no .bat extension) + getCondaHookPs1PathStub.resolves(undefined); + const sourceInitPath = 'C:\\conda\\Scripts\\activate'; // No .bat extension + const prefix = 'myenv'; + const condaFolder = 'C:\\conda'; + const condaShPath = undefined; // Not available + + // Act + const result = await windowsExceptionGenerateConfig(sourceInitPath, prefix, condaFolder, condaShPath); + // Assert const gitBashActivation = result.shellActivation.get(ShellConstants.GITBASH); assert.ok(gitBashActivation, 'Git Bash activation should be defined'); - assert.strictEqual(gitBashActivation.length, 1, 'Should have 1 command when condaShPath not available'); + assert.strictEqual(gitBashActivation.length, 1, 'Should have 1 command when source is bash-compatible'); // Single command: source assert.strictEqual(gitBashActivation[0].executable, 'source'); - assert.deepStrictEqual(gitBashActivation[0].args, ['C:/conda/Scripts/activate.bat', 'myenv']); + assert.deepStrictEqual(gitBashActivation[0].args, ['C:/conda/Scripts/activate', 'myenv']); }); test('Converts Windows backslashes to forward slashes for bash', async () => { From bc75eb64c52287886c01d4cc1e96aa7eaa777a99 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Wed, 18 Feb 2026 17:24:21 -0800 Subject: [PATCH 4/4] fix: return empty string for empty command arrays in getShellCommandAsString --- .../terminal/shells/common/shellUtils.ts | 5 ++++ .../shells/common/shellUtils.unit.test.ts | 26 ++++++++++++++++--- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/features/terminal/shells/common/shellUtils.ts b/src/features/terminal/shells/common/shellUtils.ts index 09867f29..766fd65a 100644 --- a/src/features/terminal/shells/common/shellUtils.ts +++ b/src/features/terminal/shells/common/shellUtils.ts @@ -24,6 +24,11 @@ const shellDelimiterByShell = new Map([ ]); export function getShellCommandAsString(shell: string, command: PythonCommandRunConfiguration[]): string { + // Return empty string for empty command arrays (e.g., when activation is intentionally skipped) + if (command.length === 0) { + return ''; + } + const delimiter = shellDelimiterByShell.get(shell) ?? defaultShellDelimiter; const parts = []; for (const cmd of command) { diff --git a/src/test/features/terminal/shells/common/shellUtils.unit.test.ts b/src/test/features/terminal/shells/common/shellUtils.unit.test.ts index 97281b80..861eb82a 100644 --- a/src/test/features/terminal/shells/common/shellUtils.unit.test.ts +++ b/src/test/features/terminal/shells/common/shellUtils.unit.test.ts @@ -99,9 +99,7 @@ suite('Shell Utils', () => { }); suite('getShellCommandAsString', () => { - const sampleCommand: PythonCommandRunConfiguration[] = [ - { executable: 'source', args: ['/path/to/activate'] }, - ]; + const sampleCommand: PythonCommandRunConfiguration[] = [{ executable: 'source', args: ['/path/to/activate'] }]; suite('leading space for history ignore', () => { test('should add leading space for bash commands', () => { @@ -184,5 +182,27 @@ suite('Shell Utils', () => { assert.ok(!result.startsWith(' '), 'Fish command should not start with a leading space'); }); }); + + suite('empty command handling', () => { + test('should return empty string for empty command array (bash)', () => { + const result = getShellCommandAsString(ShellConstants.BASH, []); + assert.strictEqual(result, '', 'Empty command array should return empty string'); + }); + + test('should return empty string for empty command array (gitbash)', () => { + const result = getShellCommandAsString(ShellConstants.GITBASH, []); + assert.strictEqual(result, '', 'Empty command array should return empty string'); + }); + + test('should return empty string for empty command array (pwsh)', () => { + const result = getShellCommandAsString(ShellConstants.PWSH, []); + assert.strictEqual(result, '', 'Empty command array should return empty string'); + }); + + test('should return empty string for empty command array (cmd)', () => { + const result = getShellCommandAsString(ShellConstants.CMD, []); + assert.strictEqual(result, '', 'Empty command array should return empty string'); + }); + }); }); });