Skip to main content
Glama
shell-utils.spec.ts18.7 kB
import fs from 'fs'; import os from 'os'; import path from 'path'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { addShellExport, enableDeferredMcpLoading, getShellConfigPath } from './shell-utils.js'; // Mock fs and os modules vi.mock('fs'); vi.mock('os'); describe('shell-utils', () => { const mockHomeDir = '/home/testuser'; beforeEach(() => { vi.resetAllMocks(); vi.mocked(os.homedir).mockReturnValue(mockHomeDir); }); afterEach(() => { vi.restoreAllMocks(); }); describe('getShellConfigPath', () => { it('should return .zshrc for zsh shell', () => { // Arrange const originalShell = process.env.SHELL; process.env.SHELL = '/bin/zsh'; // Act const result = getShellConfigPath(); // Assert expect(result).toBe(path.join(mockHomeDir, '.zshrc')); // Cleanup process.env.SHELL = originalShell; }); it('should return .bashrc for bash shell on Linux', () => { // Arrange const originalShell = process.env.SHELL; const originalPlatform = process.platform; process.env.SHELL = '/bin/bash'; Object.defineProperty(process, 'platform', { value: 'linux' }); vi.mocked(fs.existsSync).mockReturnValue(false); // Act const result = getShellConfigPath(); // Assert expect(result).toBe(path.join(mockHomeDir, '.bashrc')); // Cleanup process.env.SHELL = originalShell; Object.defineProperty(process, 'platform', { value: originalPlatform }); }); it('should return .bash_profile for bash shell on macOS if it exists', () => { // Arrange const originalShell = process.env.SHELL; const originalPlatform = process.platform; process.env.SHELL = '/bin/bash'; Object.defineProperty(process, 'platform', { value: 'darwin' }); vi.mocked(fs.existsSync).mockReturnValue(true); // Act const result = getShellConfigPath(); // Assert expect(result).toBe(path.join(mockHomeDir, '.bash_profile')); // Cleanup process.env.SHELL = originalShell; Object.defineProperty(process, 'platform', { value: originalPlatform }); }); it('should return .config/fish/config.fish for fish shell', () => { // Arrange const originalShell = process.env.SHELL; process.env.SHELL = '/usr/bin/fish'; // Act const result = getShellConfigPath(); // Assert expect(result).toBe( path.join(mockHomeDir, '.config', 'fish', 'config.fish') ); // Cleanup process.env.SHELL = originalShell; }); it('should fallback to .zshrc if it exists when shell is unknown', () => { // Arrange const originalShell = process.env.SHELL; process.env.SHELL = '/bin/unknown'; vi.mocked(fs.existsSync).mockImplementation((p) => String(p).includes('.zshrc') ); // Act const result = getShellConfigPath(); // Assert expect(result).toBe(path.join(mockHomeDir, '.zshrc')); // Cleanup process.env.SHELL = originalShell; }); it('should return null if no shell config is found', () => { // Arrange const originalShell = process.env.SHELL; process.env.SHELL = '/bin/unknown'; vi.mocked(fs.existsSync).mockReturnValue(false); // Act const result = getShellConfigPath(); // Assert expect(result).toBeNull(); // Cleanup process.env.SHELL = originalShell; }); it('should return PowerShell profile on Windows with PSModulePath', () => { // Arrange const originalShell = process.env.SHELL; const originalPSModulePath = process.env.PSModulePath; const originalPlatform = process.platform; process.env.SHELL = ''; process.env.PSModulePath = 'C:\\some\\path'; Object.defineProperty(process, 'platform', { value: 'win32' }); vi.mocked(fs.existsSync).mockReturnValue(false); // Act const result = getShellConfigPath(); // Assert expect(result).toBe( path.join( mockHomeDir, 'Documents', 'PowerShell', 'Microsoft.PowerShell_profile.ps1' ) ); // Cleanup process.env.SHELL = originalShell; process.env.PSModulePath = originalPSModulePath; Object.defineProperty(process, 'platform', { value: originalPlatform }); }); it('should return Git Bash .bashrc on Windows without PSModulePath', () => { // Arrange const originalShell = process.env.SHELL; const originalPSModulePath = process.env.PSModulePath; const originalPlatform = process.platform; process.env.SHELL = ''; delete process.env.PSModulePath; Object.defineProperty(process, 'platform', { value: 'win32' }); vi.mocked(fs.existsSync).mockImplementation((p) => String(p).includes('.bashrc') ); // Act const result = getShellConfigPath(); // Assert expect(result).toBe(path.join(mockHomeDir, '.bashrc')); // Cleanup process.env.SHELL = originalShell; process.env.PSModulePath = originalPSModulePath; Object.defineProperty(process, 'platform', { value: originalPlatform }); }); }); describe('addShellExport', () => { it('should add export to shell config file', () => { // Arrange const originalShell = process.env.SHELL; process.env.SHELL = '/bin/zsh'; const shellConfigPath = path.join(mockHomeDir, '.zshrc'); vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(fs.readFileSync).mockReturnValue('# existing content\n'); vi.mocked(fs.appendFileSync).mockImplementation(() => {}); // Act const result = addShellExport('MY_VAR', 'my_value', 'My comment'); // Assert expect(result.success).toBe(true); expect(result.shellConfigFile).toBe(shellConfigPath); expect(result.alreadyExists).toBeUndefined(); expect(fs.appendFileSync).toHaveBeenCalledWith( shellConfigPath, "\n# My comment\nexport MY_VAR='my_value'\n" ); // Cleanup process.env.SHELL = originalShell; }); it('should skip if export already exists', () => { // Arrange const originalShell = process.env.SHELL; process.env.SHELL = '/bin/zsh'; vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(fs.readFileSync).mockReturnValue( '# existing\nexport MY_VAR=old_value\n' ); // Act const result = addShellExport('MY_VAR', 'my_value'); // Assert expect(result.success).toBe(true); expect(result.alreadyExists).toBe(true); expect(fs.appendFileSync).not.toHaveBeenCalled(); // Cleanup process.env.SHELL = originalShell; }); it('should NOT skip when variable name only appears in a comment', () => { // Arrange const originalShell = process.env.SHELL; process.env.SHELL = '/bin/zsh'; const shellConfigPath = path.join(mockHomeDir, '.zshrc'); vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(fs.readFileSync).mockReturnValue( '# MY_VAR is mentioned here but not exported\n# Another comment about MY_VAR\n' ); vi.mocked(fs.appendFileSync).mockImplementation(() => {}); // Act const result = addShellExport('MY_VAR', 'my_value'); // Assert expect(result.success).toBe(true); expect(result.alreadyExists).toBeUndefined(); expect(fs.appendFileSync).toHaveBeenCalledWith( shellConfigPath, "\nexport MY_VAR='my_value'\n" ); // Cleanup process.env.SHELL = originalShell; }); it('should NOT skip when variable name is a substring of another variable', () => { // Arrange const originalShell = process.env.SHELL; process.env.SHELL = '/bin/zsh'; const shellConfigPath = path.join(mockHomeDir, '.zshrc'); vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(fs.readFileSync).mockReturnValue( 'export MY_VAR_EXTENDED=some_value\nexport OTHER_MY_VAR=another\n' ); vi.mocked(fs.appendFileSync).mockImplementation(() => {}); // Act const result = addShellExport('MY_VAR', 'my_value'); // Assert expect(result.success).toBe(true); expect(result.alreadyExists).toBeUndefined(); expect(fs.appendFileSync).toHaveBeenCalledWith( shellConfigPath, "\nexport MY_VAR='my_value'\n" ); // Cleanup process.env.SHELL = originalShell; }); it('should detect existing export with leading whitespace', () => { // Arrange const originalShell = process.env.SHELL; process.env.SHELL = '/bin/zsh'; vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(fs.readFileSync).mockReturnValue(' export MY_VAR=old_value\n'); // Act const result = addShellExport('MY_VAR', 'my_value'); // Assert expect(result.success).toBe(true); expect(result.alreadyExists).toBe(true); expect(fs.appendFileSync).not.toHaveBeenCalled(); // Cleanup process.env.SHELL = originalShell; }); it('should return failure if shell config not found', () => { // Arrange const originalShell = process.env.SHELL; process.env.SHELL = '/bin/zsh'; vi.mocked(fs.existsSync).mockReturnValue(false); // Act const result = addShellExport('MY_VAR', 'my_value'); // Assert expect(result.success).toBe(false); expect(result.message).toContain('not found'); // Cleanup process.env.SHELL = originalShell; }); it('should return failure if shell type cannot be determined', () => { // Arrange const originalShell = process.env.SHELL; process.env.SHELL = '/bin/unknown'; vi.mocked(fs.existsSync).mockReturnValue(false); // Act const result = addShellExport('MY_VAR', 'my_value'); // Assert expect(result.success).toBe(false); expect(result.message).toContain('Could not determine shell type'); // Cleanup process.env.SHELL = originalShell; }); it('should use PowerShell syntax for .ps1 files', () => { // Arrange const originalShell = process.env.SHELL; const originalPSModulePath = process.env.PSModulePath; const originalPlatform = process.platform; process.env.SHELL = ''; process.env.PSModulePath = 'C:\\some\\path'; Object.defineProperty(process, 'platform', { value: 'win32' }); vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(fs.readFileSync).mockReturnValue('# existing content\n'); vi.mocked(fs.appendFileSync).mockImplementation(() => {}); // Act const result = addShellExport('MY_VAR', 'my_value', 'My comment'); // Assert expect(result.success).toBe(true); expect(fs.appendFileSync).toHaveBeenCalledWith( expect.stringContaining('.ps1'), '\n# My comment\n$env:MY_VAR = "my_value"\n' ); // Cleanup process.env.SHELL = originalShell; process.env.PSModulePath = originalPSModulePath; Object.defineProperty(process, 'platform', { value: originalPlatform }); }); it('should NOT skip PowerShell when variable name only appears in a comment', () => { // Arrange const originalShell = process.env.SHELL; const originalPSModulePath = process.env.PSModulePath; const originalPlatform = process.platform; process.env.SHELL = ''; process.env.PSModulePath = 'C:\\some\\path'; Object.defineProperty(process, 'platform', { value: 'win32' }); vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(fs.readFileSync).mockReturnValue( '# MY_VAR is mentioned here but not set\n# $env:MY_VAR in a comment\n' ); vi.mocked(fs.appendFileSync).mockImplementation(() => {}); // Act const result = addShellExport('MY_VAR', 'my_value'); // Assert expect(result.success).toBe(true); expect(result.alreadyExists).toBeUndefined(); expect(fs.appendFileSync).toHaveBeenCalledWith( expect.stringContaining('.ps1'), '\n$env:MY_VAR = "my_value"\n' ); // Cleanup process.env.SHELL = originalShell; process.env.PSModulePath = originalPSModulePath; Object.defineProperty(process, 'platform', { value: originalPlatform }); }); it('should detect existing PowerShell export', () => { // Arrange const originalShell = process.env.SHELL; const originalPSModulePath = process.env.PSModulePath; const originalPlatform = process.platform; process.env.SHELL = ''; process.env.PSModulePath = 'C:\\some\\path'; Object.defineProperty(process, 'platform', { value: 'win32' }); vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(fs.readFileSync).mockReturnValue('$env:MY_VAR = "old_value"\n'); // Act const result = addShellExport('MY_VAR', 'my_value'); // Assert expect(result.success).toBe(true); expect(result.alreadyExists).toBe(true); expect(fs.appendFileSync).not.toHaveBeenCalled(); // Cleanup process.env.SHELL = originalShell; process.env.PSModulePath = originalPSModulePath; Object.defineProperty(process, 'platform', { value: originalPlatform }); }); it('should create PowerShell profile and directory if they do not exist', () => { // Arrange const originalShell = process.env.SHELL; const originalPSModulePath = process.env.PSModulePath; const originalPlatform = process.platform; process.env.SHELL = ''; process.env.PSModulePath = 'C:\\some\\path'; Object.defineProperty(process, 'platform', { value: 'win32' }); // Nothing exists initially vi.mocked(fs.existsSync).mockReturnValue(false); vi.mocked(fs.mkdirSync).mockImplementation(() => undefined); vi.mocked(fs.writeFileSync).mockImplementation(() => {}); vi.mocked(fs.readFileSync).mockReturnValue(''); vi.mocked(fs.appendFileSync).mockImplementation(() => {}); // Act const result = addShellExport('MY_VAR', 'my_value'); // Assert expect(result.success).toBe(true); // Should create the profile directory expect(fs.mkdirSync).toHaveBeenCalledWith(expect.any(String), { recursive: true }); // Should create the empty profile file expect(fs.writeFileSync).toHaveBeenCalledWith( expect.stringContaining('.ps1'), '' ); // Cleanup process.env.SHELL = originalShell; process.env.PSModulePath = originalPSModulePath; Object.defineProperty(process, 'platform', { value: originalPlatform }); }); it('should use fish shell syntax for config.fish files', () => { // Arrange const originalShell = process.env.SHELL; process.env.SHELL = '/usr/bin/fish'; const shellConfigPath = path.join( mockHomeDir, '.config', 'fish', 'config.fish' ); vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(fs.readFileSync).mockReturnValue('# existing content\n'); vi.mocked(fs.appendFileSync).mockImplementation(() => {}); // Act const result = addShellExport('MY_VAR', 'my_value', 'My comment'); // Assert expect(result.success).toBe(true); expect(fs.appendFileSync).toHaveBeenCalledWith( shellConfigPath, "\n# My comment\nset -gx MY_VAR 'my_value'\n" ); // Cleanup process.env.SHELL = originalShell; }); it('should detect existing fish shell export', () => { // Arrange const originalShell = process.env.SHELL; process.env.SHELL = '/usr/bin/fish'; vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(fs.readFileSync).mockReturnValue('set -gx MY_VAR old_value\n'); // Act const result = addShellExport('MY_VAR', 'my_value'); // Assert expect(result.success).toBe(true); expect(result.alreadyExists).toBe(true); expect(fs.appendFileSync).not.toHaveBeenCalled(); // Cleanup process.env.SHELL = originalShell; }); it('should detect existing fish shell export with different flag order', () => { // Arrange const originalShell = process.env.SHELL; process.env.SHELL = '/usr/bin/fish'; vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(fs.readFileSync).mockReturnValue('set -xg MY_VAR old_value\n'); // Act const result = addShellExport('MY_VAR', 'my_value'); // Assert expect(result.success).toBe(true); expect(result.alreadyExists).toBe(true); expect(fs.appendFileSync).not.toHaveBeenCalled(); // Cleanup process.env.SHELL = originalShell; }); it('should NOT skip fish when variable name only appears in a comment', () => { // Arrange const originalShell = process.env.SHELL; process.env.SHELL = '/usr/bin/fish'; const shellConfigPath = path.join( mockHomeDir, '.config', 'fish', 'config.fish' ); vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(fs.readFileSync).mockReturnValue( '# MY_VAR is mentioned here but not set\n# set -gx MY_VAR in a comment\n' ); vi.mocked(fs.appendFileSync).mockImplementation(() => {}); // Act const result = addShellExport('MY_VAR', 'my_value'); // Assert expect(result.success).toBe(true); expect(result.alreadyExists).toBeUndefined(); expect(fs.appendFileSync).toHaveBeenCalledWith( shellConfigPath, "\nset -gx MY_VAR 'my_value'\n" ); // Cleanup process.env.SHELL = originalShell; }); it('should create fish config directory if it does not exist', () => { // Arrange const originalShell = process.env.SHELL; process.env.SHELL = '/usr/bin/fish'; // Fish config directory doesn't exist initially vi.mocked(fs.existsSync).mockReturnValue(false); vi.mocked(fs.mkdirSync).mockImplementation(() => undefined); vi.mocked(fs.readFileSync).mockReturnValue(''); vi.mocked(fs.appendFileSync).mockImplementation(() => {}); // Act const result = addShellExport('MY_VAR', 'my_value'); // Assert - fish config file not found since we don't auto-create it (unlike PowerShell) expect(result.success).toBe(false); expect(result.message).toContain('not found'); // Cleanup process.env.SHELL = originalShell; }); }); describe('enableDeferredMcpLoading', () => { it('should add ENABLE_EXPERIMENTAL_MCP_CLI export', () => { // Arrange const originalShell = process.env.SHELL; process.env.SHELL = '/bin/zsh'; const shellConfigPath = path.join(mockHomeDir, '.zshrc'); vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(fs.readFileSync).mockReturnValue('# existing content\n'); vi.mocked(fs.appendFileSync).mockImplementation(() => {}); // Act const result = enableDeferredMcpLoading(); // Assert expect(result.success).toBe(true); expect(result.shellConfigFile).toBe(shellConfigPath); expect(fs.appendFileSync).toHaveBeenCalledWith( shellConfigPath, "\n# Claude Code deferred MCP loading (added by Taskmaster)\nexport ENABLE_EXPERIMENTAL_MCP_CLI='true'\n" ); // Cleanup process.env.SHELL = originalShell; }); it('should skip if already configured', () => { // Arrange const originalShell = process.env.SHELL; process.env.SHELL = '/bin/zsh'; vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(fs.readFileSync).mockReturnValue( 'export ENABLE_EXPERIMENTAL_MCP_CLI=true\n' ); // Act const result = enableDeferredMcpLoading(); // Assert expect(result.success).toBe(true); expect(result.alreadyExists).toBe(true); expect(fs.appendFileSync).not.toHaveBeenCalled(); // Cleanup process.env.SHELL = originalShell; }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/eyaltoledano/claude-task-master'

If you have feedback or need assistance with the MCP directory API, please join our Discord server