diff --git a/src/managers/conda/condaSourcingUtils.ts b/src/managers/conda/condaSourcingUtils.ts index 76bf7dad..70b8d249 100644 --- a/src/managers/conda/condaSourcingUtils.ts +++ b/src/managers/conda/condaSourcingUtils.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. import * as fse from 'fs-extra'; +import * as os from 'os'; import * as path from 'path'; import { traceError, traceInfo, traceVerbose } from '../../common/logging'; import { isWindows } from '../../common/utils/platformUtils'; @@ -17,6 +18,20 @@ export interface ShellSourcingScripts { sh?: string; /** Windows CMD batch file (activate.bat) */ cmd?: string; + /** Fish shell initialization script (conda.fish) */ + fish?: string; +} + +/** + * Tracks whether `conda init ` has been run for each shell type. + * When true, the shell's profile/config file contains the conda initialization block, + * meaning bare `conda` will be available as a shell function when that shell starts. + */ +export interface ShellCondaInitStatus { + bash?: boolean; + zsh?: boolean; + fish?: boolean; + pwsh?: boolean; } /** @@ -37,6 +52,7 @@ export class CondaSourcingStatus { public isActiveOnLaunch?: boolean, public globalSourcingScript?: string, public shellSourcingScripts?: ShellSourcingScripts, + public shellInitStatus?: ShellCondaInitStatus, ) {} /** @@ -59,6 +75,7 @@ export class CondaSourcingStatus { scripts.ps1 && `PowerShell: ${scripts.ps1}`, scripts.sh && `Bash/sh: ${scripts.sh}`, scripts.cmd && `CMD: ${scripts.cmd}`, + scripts.fish && `Fish: ${scripts.fish}`, ].filter(Boolean); if (entries.length > 0) { @@ -74,6 +91,13 @@ export class CondaSourcingStatus { lines.push('└─ No Shell-specific Sourcing Scripts Found'); } + if (this.shellInitStatus) { + const initEntries = (['bash', 'zsh', 'fish', 'pwsh'] as const) + .map((s) => `${s}: ${this.shellInitStatus![s] ? '✓' : '✗'}`) + .join(', '); + lines.push(`├─ Shell conda init status: ${initEntries}`); + } + return lines.join('\n'); } } @@ -116,6 +140,9 @@ export async function constructCondaSourcingStatus(condaPath: string): Promise` has been run + sourcingStatus.shellInitStatus = await checkCondaInitInShellProfiles(); + return sourcingStatus; } @@ -148,6 +175,7 @@ export async function findShellSourcingScripts(sourcingStatus: CondaSourcingStat let ps1Script: string | undefined; let shScript: string | undefined; let cmdActivate: string | undefined; + let fishScript: string | undefined; try { // Search for PowerShell hook script (conda-hook.ps1) @@ -178,6 +206,15 @@ export async function findShellSourcingScripts(sourcingStatus: CondaSourcingStat } catch (err) { logs.push(` Error during CMD script search: ${err instanceof Error ? err.message : 'Unknown error'}`); } + + // Search for Fish shell script (conda.fish) + logs.push('\nSearching for Fish shell script...'); + try { + fishScript = await getCondaFishPath(sourcingStatus.condaFolder); + logs.push(` Path: ${fishScript ?? '✗ Not found'}`); + } catch (err) { + logs.push(` Error during Fish script search: ${err instanceof Error ? err.message : 'Unknown error'}`); + } } catch (error) { logs.push(`\nCritical error during script search: ${error instanceof Error ? error.message : 'Unknown error'}`); } finally { @@ -185,13 +222,87 @@ export async function findShellSourcingScripts(sourcingStatus: CondaSourcingStat logs.push(` PowerShell: ${ps1Script ? '✓' : '✗'}`); logs.push(` Shell: ${shScript ? '✓' : '✗'}`); logs.push(` CMD: ${cmdActivate ? '✓' : '✗'}`); + logs.push(` Fish: ${fishScript ? '✓' : '✗'}`); logs.push('============================'); // Log everything at once traceVerbose(logs.join('\n')); } - return { ps1: ps1Script, sh: shScript, cmd: cmdActivate }; + return { ps1: ps1Script, sh: shScript, cmd: cmdActivate, fish: fishScript }; +} + +/** + * Checks shell profile/config files to determine if `conda init ` has been run. + * + * When `conda init ` is run, it adds a `# >>> conda initialize >>>` block to the + * shell's profile. If that block is present, then any new terminal of that shell type will + * have `conda` available as a shell function, and bare `conda activate` will work. + * + * For Fish, `conda init fish` may either modify `config.fish` or drop a file in + * `~/.config/fish/conf.d/`, so both locations are checked. + * + * @param homeDir Optional home directory override (defaults to os.homedir(), useful for testing) + * @returns Status object indicating which shells have conda initialized + */ +export async function checkCondaInitInShellProfiles(homeDir?: string): Promise { + const home = homeDir ?? os.homedir(); + const status: ShellCondaInitStatus = {}; + const logs: string[] = ['=== Checking shell profiles for conda init ===']; + + const checks: Array<{ shell: keyof ShellCondaInitStatus; files: string[] }> = [ + { + shell: 'bash', + files: [path.join(home, '.bashrc'), path.join(home, '.bash_profile')], + }, + { + shell: 'zsh', + files: [path.join(home, '.zshrc')], + }, + { + shell: 'fish', + files: [ + path.join(process.env.XDG_CONFIG_HOME || path.join(home, '.config'), 'fish', 'config.fish'), + path.join(process.env.XDG_CONFIG_HOME || path.join(home, '.config'), 'fish', 'conf.d', 'conda.fish'), + ], + }, + { + shell: 'pwsh', + files: [ + path.join( + process.env.XDG_CONFIG_HOME || path.join(home, '.config'), + 'powershell', + 'Microsoft.PowerShell_profile.ps1', + ), + path.join(process.env.XDG_CONFIG_HOME || path.join(home, '.config'), 'powershell', 'profile.ps1'), + ], + }, + ]; + + await Promise.all( + checks.map(async ({ shell, files }) => { + for (const filePath of files) { + try { + if (await fse.pathExists(filePath)) { + const content = await fse.readFile(filePath, 'utf-8'); + if (content.includes('conda initialize')) { + status[shell] = true; + logs.push(` ${shell}: ✓ conda init found in ${filePath}`); + return; + } + } + } catch { + // File not readable, skip + } + } + logs.push(` ${shell}: ✗ conda init not found`); + }), + ); + + logs.push('============================'); + traceVerbose(logs.join('\n')); + + return status; } /** @@ -308,6 +419,24 @@ async function getCondaShPath(condaFolder: string): Promise return shPathPromise; } +/** + * Returns the path to conda.fish given a conda installation folder. + * + * Searches for conda.fish in these locations (relative to the conda root): + * - etc/fish/conf.d/conda.fish + * - shell/etc/fish/conf.d/conda.fish + * - Library/etc/fish/conf.d/conda.fish + */ +export async function getCondaFishPath(condaFolder: string): Promise { + const locations = [ + path.join(condaFolder, 'etc', 'fish', 'conf.d', 'conda.fish'), + path.join(condaFolder, 'shell', 'etc', 'fish', 'conf.d', 'conda.fish'), + path.join(condaFolder, 'Library', 'etc', 'fish', 'conf.d', 'conda.fish'), + ]; + + return findFileInLocations(locations, 'conda.fish'); +} + /** * Returns the path to the Windows batch activation file (activate.bat) for conda * @param condaPath The path to the conda executable diff --git a/src/managers/conda/condaUtils.ts b/src/managers/conda/condaUtils.ts index a34bbf87..47ceb3d0 100644 --- a/src/managers/conda/condaUtils.ts +++ b/src/managers/conda/condaUtils.ts @@ -55,7 +55,7 @@ import { selectFromCommonPackagesToInstall } from '../common/pickers'; import { Installable } from '../common/types'; import { shortVersion, sortEnvironments } from '../common/utils'; import { CondaEnvManager } from './condaEnvManager'; -import { getCondaHookPs1Path, getLocalActivationScript } from './condaSourcingUtils'; +import { getCondaHookPs1Path, getLocalActivationScript, ShellCondaInitStatus } from './condaSourcingUtils'; import { createStepBasedCondaFlow } from './condaStepBasedFlow'; export const CONDA_PATH_KEY = `${ENVS_EXTENSION_ID}:conda:CONDA_PATH`; @@ -526,12 +526,21 @@ async function buildShellActivationMapForConda( return shellMaps; } - logs.push('✓ Using source command with preferred path'); - const condaSourcingPathFirst = { - executable: 'source', - args: [preferredSourcingPath, envIdentifier], - }; - shellMaps = await generateShellActivationMapFromConfig([condaSourcingPathFirst], [condaCommonDeactivate]); + logs.push('✓ Using shell-specific activation commands'); + const condaShPath = envManager.sourcingInformation.shellSourcingScripts?.sh; + const condaFishPath = envManager.sourcingInformation.shellSourcingScripts?.fish; + const condaPs1Path = envManager.sourcingInformation.shellSourcingScripts?.ps1; + + shellMaps = nonWindowsGenerateConfig( + preferredSourcingPath, + envIdentifier, + condaCommonDeactivate, + envManager.sourcingInformation.condaPath, + condaShPath, + condaFishPath, + condaPs1Path, + envManager.sourcingInformation.shellInitStatus, + ); return shellMaps; } catch (error) { logs.push( @@ -579,6 +588,9 @@ async function generateShellActivationMapFromConfig( shellActivation.set(ShellConstants.PWSH, activate); shellDeactivation.set(ShellConstants.PWSH, deactivate); + shellActivation.set(ShellConstants.FISH, activate); + shellDeactivation.set(ShellConstants.FISH, deactivate); + return { shellActivation, shellDeactivation }; } @@ -648,6 +660,98 @@ export async function windowsExceptionGenerateConfig( return { shellActivation, shellDeactivation }; } +/** + * Generates shell-specific activation configuration for non-Windows (Linux/macOS). + * Uses conda.sh for bash-like shells, conda.fish for Fish, and conda-hook.ps1 for PowerShell. + * Falls back to `source ` for bash-like shells when conda.sh is unavailable. + * + * For each shell, when the shell-specific sourcing script is not found, checks whether + * `conda init ` has been run (via shellInitStatus). If it has, bare `conda` is used + * since the shell will set up the conda function on startup. Otherwise, the full conda path + * is used as the executable. + * + * @internal Exported for testing + */ +export function nonWindowsGenerateConfig( + sourceInitPath: string, + envIdentifier: string, + condaCommonDeactivate: PythonCommandRunConfiguration, + condaPath: string, + condaShPath?: string, + condaFishPath?: string, + condaPs1Path?: string, + shellInitStatus?: ShellCondaInitStatus, +): ShellCommandMaps { + const shellActivation: Map = new Map(); + const shellDeactivation: Map = new Map(); + const deactivate = [condaCommonDeactivate]; + + // Helper: determine the conda executable for a given shell based on init status. + // If `conda init ` has been run, the shell profile sets up `conda` as a shell + // function on startup, so bare `conda` works. Otherwise, use the full path to the + // conda binary (the user will see an actionable error if hooks aren't set up). + const condaExe = (shell: keyof ShellCondaInitStatus): string => (shellInitStatus?.[shell] ? 'conda' : condaPath); + + // Bash-like shells: use conda.sh if available, otherwise fall back to source activate + let bashActivate: PythonCommandRunConfiguration[]; + if (condaShPath) { + bashActivate = [ + { executable: 'source', args: [condaShPath] }, + { executable: 'conda', args: ['activate', envIdentifier] }, + ]; + } else { + bashActivate = [{ executable: 'source', args: [sourceInitPath, envIdentifier] }]; + } + + shellActivation.set(ShellConstants.BASH, bashActivate); + shellDeactivation.set(ShellConstants.BASH, deactivate); + + shellActivation.set(ShellConstants.SH, bashActivate); + shellDeactivation.set(ShellConstants.SH, deactivate); + + shellActivation.set(ShellConstants.ZSH, bashActivate); + shellDeactivation.set(ShellConstants.ZSH, deactivate); + + shellActivation.set(ShellConstants.GITBASH, bashActivate); + shellDeactivation.set(ShellConstants.GITBASH, deactivate); + + // Fish shell: use conda.fish if available. Otherwise, check if `conda init fish` + // was run — if so, bare `conda` works; if not, use the full conda path. + let fishActivate: PythonCommandRunConfiguration[]; + if (condaFishPath) { + fishActivate = [ + { executable: 'source', args: [condaFishPath] }, + { executable: 'conda', args: ['activate', envIdentifier] }, + ]; + } else { + fishActivate = [{ executable: condaExe('fish'), args: ['activate', envIdentifier] }]; + } + + shellActivation.set(ShellConstants.FISH, fishActivate); + shellDeactivation.set(ShellConstants.FISH, deactivate); + + // PowerShell: use conda-hook.ps1 if available. Otherwise, check if `conda init powershell` + // was run — if so, bare `conda` works; if not, use the full conda path. + let pwshActivate: PythonCommandRunConfiguration[]; + if (condaPs1Path) { + pwshActivate = [{ executable: condaPs1Path }, { executable: 'conda', args: ['activate', envIdentifier] }]; + } else { + pwshActivate = [{ executable: condaExe('pwsh'), args: ['activate', envIdentifier] }]; + } + + shellActivation.set(ShellConstants.PWSH, pwshActivate); + shellDeactivation.set(ShellConstants.PWSH, deactivate); + + traceVerbose( + `Non-Windows activation commands: + Bash: ${JSON.stringify(bashActivate)}, + Fish: ${JSON.stringify(fishActivate)}, + PowerShell: ${JSON.stringify(pwshActivate)}`, + ); + + return { shellActivation, shellDeactivation }; +} + function getCondaWithoutPython(name: string, prefix: string, conda: string): PythonEnvironmentInfo { return { name: name, diff --git a/src/test/managers/conda/condaSourcingUtils.shellProfiles.unit.test.ts b/src/test/managers/conda/condaSourcingUtils.shellProfiles.unit.test.ts new file mode 100644 index 00000000..98e51f91 --- /dev/null +++ b/src/test/managers/conda/condaSourcingUtils.shellProfiles.unit.test.ts @@ -0,0 +1,147 @@ +import assert from 'assert'; +import * as fse from 'fs-extra'; +import * as os from 'os'; +import * as path from 'path'; +import { checkCondaInitInShellProfiles } from '../../../managers/conda/condaSourcingUtils'; + +/** + * Tests for checkCondaInitInShellProfiles — verifying detection of `conda init ` + * in shell profile/config files. + * + * Uses a temporary directory with real files to avoid fs-extra stubbing issues. + */ +suite('Conda Sourcing Utils - checkCondaInitInShellProfiles', () => { + let tmpHome: string; + let originalXdg: string | undefined; + + const condaInitBlock = ` +# some existing config +export PATH="/usr/bin:$PATH" + +# >>> conda initialize >>> +# !! Contents within this block are managed by 'conda init' !! +eval "$('/home/user/miniforge3/bin/conda' 'shell.bash' 'hook' 2> /dev/null)" +# <<< conda initialize <<< +`; + + setup(async () => { + tmpHome = path.join(os.tmpdir(), `conda-test-${Date.now()}-${Math.random().toString(36).slice(2)}`); + await fse.ensureDir(tmpHome); + originalXdg = process.env.XDG_CONFIG_HOME; + delete process.env.XDG_CONFIG_HOME; + }); + + teardown(async () => { + await fse.remove(tmpHome); + if (originalXdg !== undefined) { + process.env.XDG_CONFIG_HOME = originalXdg; + } else { + delete process.env.XDG_CONFIG_HOME; + } + }); + + test('Detects conda init in .bashrc', async () => { + await fse.writeFile(path.join(tmpHome, '.bashrc'), condaInitBlock); + + const status = await checkCondaInitInShellProfiles(tmpHome); + assert.strictEqual(status.bash, true); + }); + + test('Detects conda init in .bash_profile when .bashrc has no conda', async () => { + await fse.writeFile(path.join(tmpHome, '.bashrc'), '# no conda here'); + await fse.writeFile(path.join(tmpHome, '.bash_profile'), condaInitBlock); + + const status = await checkCondaInitInShellProfiles(tmpHome); + assert.strictEqual(status.bash, true); + }); + + test('Detects conda init in .zshrc', async () => { + await fse.writeFile(path.join(tmpHome, '.zshrc'), condaInitBlock); + + const status = await checkCondaInitInShellProfiles(tmpHome); + assert.strictEqual(status.zsh, true); + }); + + test('Detects conda init in fish config.fish', async () => { + const fishDir = path.join(tmpHome, '.config', 'fish'); + await fse.ensureDir(fishDir); + await fse.writeFile(path.join(fishDir, 'config.fish'), condaInitBlock); + + const status = await checkCondaInitInShellProfiles(tmpHome); + assert.strictEqual(status.fish, true); + }); + + test('Detects conda init in fish conf.d/conda.fish', async () => { + const confdDir = path.join(tmpHome, '.config', 'fish', 'conf.d'); + await fse.ensureDir(confdDir); + await fse.writeFile(path.join(confdDir, 'conda.fish'), condaInitBlock); + + const status = await checkCondaInitInShellProfiles(tmpHome); + assert.strictEqual(status.fish, true); + }); + + test('Detects conda init in PowerShell profile', async () => { + const psDir = path.join(tmpHome, '.config', 'powershell'); + await fse.ensureDir(psDir); + await fse.writeFile(path.join(psDir, 'Microsoft.PowerShell_profile.ps1'), condaInitBlock); + + const status = await checkCondaInitInShellProfiles(tmpHome); + assert.strictEqual(status.pwsh, true); + }); + + test('Returns undefined for shells without conda init', async () => { + const status = await checkCondaInitInShellProfiles(tmpHome); + assert.strictEqual(status.bash, undefined); + assert.strictEqual(status.zsh, undefined); + assert.strictEqual(status.fish, undefined); + assert.strictEqual(status.pwsh, undefined); + }); + + test('Profile exists but does not contain conda initialize', async () => { + await fse.writeFile(path.join(tmpHome, '.bashrc'), 'export PATH="/usr/bin:$PATH"\nalias ll="ls -la"'); + await fse.writeFile(path.join(tmpHome, '.zshrc'), '# just a comment'); + + const status = await checkCondaInitInShellProfiles(tmpHome); + assert.strictEqual(status.bash, undefined); + assert.strictEqual(status.zsh, undefined); + }); + + test('Respects XDG_CONFIG_HOME for fish', async () => { + const customConfig = path.join(tmpHome, 'custom-xdg'); + process.env.XDG_CONFIG_HOME = customConfig; + + const fishDir = path.join(customConfig, 'fish'); + await fse.ensureDir(fishDir); + await fse.writeFile(path.join(fishDir, 'config.fish'), condaInitBlock); + + const status = await checkCondaInitInShellProfiles(tmpHome); + assert.strictEqual(status.fish, true); + }); + + test('Respects XDG_CONFIG_HOME for pwsh', async () => { + const customConfig = path.join(tmpHome, 'custom-xdg'); + process.env.XDG_CONFIG_HOME = customConfig; + + const psDir = path.join(customConfig, 'powershell'); + await fse.ensureDir(psDir); + await fse.writeFile(path.join(psDir, 'profile.ps1'), condaInitBlock); + + const status = await checkCondaInitInShellProfiles(tmpHome); + assert.strictEqual(status.pwsh, true); + }); + + test('Multiple shells initialized at once', async () => { + await fse.writeFile(path.join(tmpHome, '.bashrc'), condaInitBlock); + await fse.writeFile(path.join(tmpHome, '.zshrc'), condaInitBlock); + + const fishDir = path.join(tmpHome, '.config', 'fish'); + await fse.ensureDir(fishDir); + await fse.writeFile(path.join(fishDir, 'config.fish'), condaInitBlock); + + const status = await checkCondaInitInShellProfiles(tmpHome); + assert.strictEqual(status.bash, true); + assert.strictEqual(status.zsh, true); + assert.strictEqual(status.fish, true); + assert.strictEqual(status.pwsh, undefined); + }); +}); diff --git a/src/test/managers/conda/condaUtils.nonWindowsActivation.unit.test.ts b/src/test/managers/conda/condaUtils.nonWindowsActivation.unit.test.ts new file mode 100644 index 00000000..83258111 --- /dev/null +++ b/src/test/managers/conda/condaUtils.nonWindowsActivation.unit.test.ts @@ -0,0 +1,233 @@ +import assert from 'assert'; +import { ShellConstants } from '../../../features/common/shellConstants'; +import { nonWindowsGenerateConfig } from '../../../managers/conda/condaUtils'; + +/** + * Tests for nonWindowsGenerateConfig - Non-Windows shell activation commands. + * + * Key behavior tested: + * - Bash/ZSH/SH use conda.sh + conda activate when condaShPath is available + * - Bash/ZSH/SH fall back to source when condaShPath is unavailable + * - Fish uses conda.fish + conda activate when condaFishPath is available + * - Fish fallback uses bare `conda` if conda init fish was run, else full conda path + * - PowerShell uses conda-hook.ps1 + conda activate when condaPs1Path is available + * - PowerShell fallback uses bare `conda` if conda init pwsh was run, else full conda path + */ +suite('Conda Utils - nonWindowsGenerateConfig', () => { + const sourceInitPath = '/home/user/miniforge3/bin/activate'; + const envIdentifier = 'myenv'; + const condaPath = '/home/user/miniforge3/bin/conda'; + const condaDeactivate = { executable: 'conda', args: ['deactivate'] }; + + suite('Bash-like shell activation', () => { + test('Uses source conda.sh + conda activate when condaShPath is provided', () => { + const condaShPath = '/home/user/miniforge3/etc/profile.d/conda.sh'; + const result = nonWindowsGenerateConfig( + sourceInitPath, + envIdentifier, + condaDeactivate, + condaPath, + condaShPath, + ); + + for (const shell of [ShellConstants.BASH, ShellConstants.ZSH, ShellConstants.SH, ShellConstants.GITBASH]) { + const activation = result.shellActivation.get(shell); + assert.ok(activation, `${shell} activation should be defined`); + assert.strictEqual(activation.length, 2, `${shell} should have 2 commands`); + assert.strictEqual(activation[0].executable, 'source'); + assert.deepStrictEqual(activation[0].args, [condaShPath]); + assert.strictEqual(activation[1].executable, 'conda'); + assert.deepStrictEqual(activation[1].args, ['activate', envIdentifier]); + } + }); + + test('Falls back to source activate when condaShPath is not provided', () => { + const result = nonWindowsGenerateConfig(sourceInitPath, envIdentifier, condaDeactivate, condaPath); + + for (const shell of [ShellConstants.BASH, ShellConstants.ZSH, ShellConstants.SH, ShellConstants.GITBASH]) { + const activation = result.shellActivation.get(shell); + assert.ok(activation, `${shell} activation should be defined`); + assert.strictEqual(activation.length, 1, `${shell} should have 1 command`); + assert.strictEqual(activation[0].executable, 'source'); + assert.deepStrictEqual(activation[0].args, [sourceInitPath, envIdentifier]); + } + }); + }); + + suite('Fish shell activation', () => { + test('Uses source conda.fish + conda activate when condaFishPath is provided', () => { + const condaFishPath = '/home/user/miniforge3/etc/fish/conf.d/conda.fish'; + const result = nonWindowsGenerateConfig( + sourceInitPath, + envIdentifier, + condaDeactivate, + condaPath, + undefined, + condaFishPath, + ); + + const activation = result.shellActivation.get(ShellConstants.FISH); + assert.ok(activation, 'Fish activation should be defined'); + assert.strictEqual(activation.length, 2, 'Should have 2 commands: source conda.fish + conda activate'); + assert.strictEqual(activation[0].executable, 'source'); + assert.deepStrictEqual(activation[0].args, [condaFishPath]); + assert.strictEqual(activation[1].executable, 'conda'); + assert.deepStrictEqual(activation[1].args, ['activate', envIdentifier]); + }); + + test('Uses bare conda when conda init fish was run and condaFishPath not found', () => { + const result = nonWindowsGenerateConfig( + sourceInitPath, + envIdentifier, + condaDeactivate, + condaPath, + undefined, + undefined, + undefined, + { fish: true }, + ); + + const activation = result.shellActivation.get(ShellConstants.FISH); + assert.ok(activation, 'Fish activation should be defined'); + assert.strictEqual(activation.length, 1); + assert.strictEqual(activation[0].executable, 'conda'); + assert.deepStrictEqual(activation[0].args, ['activate', envIdentifier]); + }); + + test('Uses full conda path when conda init fish was NOT run and condaFishPath not found', () => { + const result = nonWindowsGenerateConfig( + sourceInitPath, + envIdentifier, + condaDeactivate, + condaPath, + undefined, + undefined, + undefined, + { fish: false }, + ); + + const activation = result.shellActivation.get(ShellConstants.FISH); + assert.ok(activation, 'Fish activation should be defined'); + assert.strictEqual(activation.length, 1); + assert.strictEqual(activation[0].executable, condaPath); + assert.deepStrictEqual(activation[0].args, ['activate', envIdentifier]); + }); + + test('Uses full conda path when shellInitStatus is undefined and condaFishPath not found', () => { + const result = nonWindowsGenerateConfig(sourceInitPath, envIdentifier, condaDeactivate, condaPath); + + const activation = result.shellActivation.get(ShellConstants.FISH); + assert.ok(activation, 'Fish activation should be defined'); + assert.strictEqual(activation.length, 1); + assert.strictEqual(activation[0].executable, condaPath); + assert.deepStrictEqual(activation[0].args, ['activate', envIdentifier]); + }); + }); + + suite('PowerShell activation', () => { + test('Uses conda-hook.ps1 + conda activate when condaPs1Path is provided', () => { + const condaPs1Path = '/home/user/miniforge3/shell/condabin/conda-hook.ps1'; + const result = nonWindowsGenerateConfig( + sourceInitPath, + envIdentifier, + condaDeactivate, + condaPath, + undefined, + undefined, + condaPs1Path, + ); + + const activation = result.shellActivation.get(ShellConstants.PWSH); + assert.ok(activation, 'PowerShell activation should be defined'); + assert.strictEqual(activation.length, 2, 'Should have 2 commands'); + assert.strictEqual(activation[0].executable, condaPs1Path); + assert.strictEqual(activation[1].executable, 'conda'); + assert.deepStrictEqual(activation[1].args, ['activate', envIdentifier]); + }); + + test('Uses bare conda when conda init pwsh was run and condaPs1Path not found', () => { + const result = nonWindowsGenerateConfig( + sourceInitPath, + envIdentifier, + condaDeactivate, + condaPath, + undefined, + undefined, + undefined, + { pwsh: true }, + ); + + const activation = result.shellActivation.get(ShellConstants.PWSH); + assert.ok(activation, 'PowerShell activation should be defined'); + assert.strictEqual(activation.length, 1); + assert.strictEqual(activation[0].executable, 'conda'); + assert.deepStrictEqual(activation[0].args, ['activate', envIdentifier]); + }); + + test('Uses full conda path when conda init pwsh was NOT run and condaPs1Path not found', () => { + const result = nonWindowsGenerateConfig(sourceInitPath, envIdentifier, condaDeactivate, condaPath); + + const activation = result.shellActivation.get(ShellConstants.PWSH); + assert.ok(activation, 'PowerShell activation should be defined'); + assert.strictEqual(activation.length, 1); + assert.strictEqual(activation[0].executable, condaPath); + assert.deepStrictEqual(activation[0].args, ['activate', envIdentifier]); + }); + }); + + suite('Deactivation commands', () => { + test('All shells use conda deactivate', () => { + const result = nonWindowsGenerateConfig(sourceInitPath, envIdentifier, condaDeactivate, condaPath); + + for (const shell of [ + ShellConstants.BASH, + ShellConstants.ZSH, + ShellConstants.SH, + ShellConstants.GITBASH, + ShellConstants.FISH, + 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']); + } + }); + }); + + suite('All scripts provided', () => { + test('Each shell gets its specific activation when all scripts available', () => { + const condaShPath = '/home/user/miniforge3/etc/profile.d/conda.sh'; + const condaFishPath = '/home/user/miniforge3/etc/fish/conf.d/conda.fish'; + const condaPs1Path = '/home/user/miniforge3/shell/condabin/conda-hook.ps1'; + + const result = nonWindowsGenerateConfig( + sourceInitPath, + envIdentifier, + condaDeactivate, + condaPath, + condaShPath, + condaFishPath, + condaPs1Path, + ); + + // Bash uses conda.sh + const bashActivation = result.shellActivation.get(ShellConstants.BASH); + assert.ok(bashActivation); + assert.strictEqual(bashActivation[0].executable, 'source'); + assert.deepStrictEqual(bashActivation[0].args, [condaShPath]); + + // Fish uses conda.fish + const fishActivation = result.shellActivation.get(ShellConstants.FISH); + assert.ok(fishActivation); + assert.strictEqual(fishActivation[0].executable, 'source'); + assert.deepStrictEqual(fishActivation[0].args, [condaFishPath]); + + // PowerShell uses conda-hook.ps1 + const pwshActivation = result.shellActivation.get(ShellConstants.PWSH); + assert.ok(pwshActivation); + assert.strictEqual(pwshActivation[0].executable, condaPs1Path); + }); + }); +});