UNPKG

npmplus-mcp-server

Version:

Production-ready MCP server for intelligent JavaScript package management. Works with Claude, Windsurf, Cursor, VS Code, and any MCP-compatible AI editor.

449 lines (334 loc) 15.8 kB
import { describe, it, expect, beforeEach, jest } from '@jest/globals'; import { PackageManagerService } from '../../services/PackageManagerService.js'; import { PackageManagerType } from '../../models/Package.js'; import { TEST_CONSTANTS } from '../setup.js'; // Mock execa at the module level jest.mock('execa', () => ({ execa: jest.fn() })); // Mock fs jest.mock('fs', () => ({ existsSync: jest.fn() })); import { execa } from 'execa'; import { existsSync } from 'fs'; const mockedExeca = execa as jest.MockedFunction<typeof execa>; const mockedExistsSync = existsSync as jest.MockedFunction<typeof existsSync>; describe('PackageManagerService', () => { let service: PackageManagerService; beforeEach(() => { service = new PackageManagerService(TEST_CONSTANTS.TIMEOUT); jest.clearAllMocks(); // Reset the mock implementations mockedExeca.mockResolvedValue({ stdout: '', stderr: '', exitCode: 0 } as any); mockedExistsSync.mockReturnValue(false); }); describe('detectPackageManager', () => { it('should detect pnpm from pnpm-lock.yaml', async () => { mockedExistsSync.mockReset(); mockedExistsSync .mockReturnValueOnce(true) // pnpm-lock.yaml exists .mockReturnValueOnce(false); // yarn.lock doesn't exist const result = await service.detectPackageManager('/test/path'); expect(result).toBe('pnpm'); }); it('should detect yarn from yarn.lock', async () => { mockedExistsSync.mockReset(); mockedExistsSync .mockReturnValueOnce(false) // pnpm-lock.yaml doesn't exist .mockReturnValueOnce(true); // yarn.lock exists const result = await service.detectPackageManager('/test/path'); expect(result).toBe('yarn'); }); it('should default to npm when no lock files found', async () => { mockedExistsSync.mockReset(); mockedExistsSync.mockReturnValue(false); const result = await service.detectPackageManager('/test/path'); expect(result).toBe('npm'); }); it('should use current directory when no path provided', async () => { mockedExistsSync.mockReturnValue(false); await service.detectPackageManager(); // Should have checked for lock files in current directory expect(mockedExistsSync).toHaveBeenCalled(); }); }); describe('install', () => { const mockSuccessfulResult = { stdout: 'Package installed successfully', stderr: '', exitCode: 0, }; it('should install packages with npm', async () => { mockedExeca.mockResolvedValue(mockSuccessfulResult as any); const result = await service.install(['lodash', 'axios'], '/test/path', false, false, 'npm'); expect(mockedExeca).toHaveBeenCalledWith('npm', ['install', 'lodash', 'axios'], { cwd: '/test/path', timeout: TEST_CONSTANTS.TIMEOUT, reject: true, stdio: 'pipe', }); expect(result).toEqual({ success: true, packages: ['lodash', 'axios'], operation: 'install', packageManager: 'npm', output: 'Package installed successfully', duration: expect.any(Number), }); }); it('should install dev dependencies with npm', async () => { mockedExeca.mockResolvedValue(mockSuccessfulResult as any); await service.install(['typescript'], undefined, true, false, 'npm'); expect(mockedExeca).toHaveBeenCalledWith('npm', ['install', '--save-dev', 'typescript'], { cwd: TEST_CONSTANTS.MOCK_CWD, timeout: TEST_CONSTANTS.TIMEOUT, reject: true, stdio: 'pipe', }); }); it('should install global packages with npm', async () => { mockedExeca.mockResolvedValue(mockSuccessfulResult as any); await service.install(['typescript'], undefined, false, true, 'npm'); expect(mockedExeca).toHaveBeenCalledWith('npm', ['install', '--global', 'typescript'], { cwd: TEST_CONSTANTS.MOCK_CWD, timeout: TEST_CONSTANTS.TIMEOUT, reject: true, stdio: 'pipe', }); }); it('should install packages with yarn', async () => { mockedExeca.mockResolvedValue(mockSuccessfulResult as any); await service.install(['lodash'], undefined, false, false, 'yarn'); expect(mockedExeca).toHaveBeenCalledWith('yarn', ['add', 'lodash'], expect.any(Object)); }); it('should install dev dependencies with yarn', async () => { mockedExeca.mockResolvedValue(mockSuccessfulResult as any); await service.install(['typescript'], undefined, true, false, 'yarn'); expect(mockedExeca).toHaveBeenCalledWith('yarn', ['add', '--dev', 'typescript'], expect.any(Object)); }); it('should install global packages with yarn', async () => { mockedExeca.mockResolvedValue(mockSuccessfulResult as any); await service.install(['typescript'], undefined, false, true, 'yarn'); expect(mockedExeca).toHaveBeenCalledWith('yarn', ['add', 'global', 'typescript'], expect.any(Object)); }); it('should install packages with pnpm', async () => { mockedExeca.mockResolvedValue(mockSuccessfulResult as any); await service.install(['lodash'], undefined, false, false, 'pnpm'); expect(mockedExeca).toHaveBeenCalledWith('pnpm', ['add', 'lodash'], expect.any(Object)); }); it('should handle installation errors', async () => { const error = new Error('Installation failed'); mockedExeca.mockRejectedValue(error); const result = await service.install(['nonexistent'], undefined, false, false, 'npm'); expect(result).toEqual({ success: false, packages: ['nonexistent'], operation: 'install', packageManager: 'npm', output: 'Installation failed', errors: ['Installation failed'], duration: expect.any(Number), }); }); it('should handle stderr in errors', async () => { const error = { message: 'Command failed', stderr: 'Package not found' }; mockedExeca.mockRejectedValue(error); const result = await service.install(['nonexistent'], undefined, false, false, 'npm'); expect(result.output).toBe('Package not found'); }); }); describe('update', () => { const mockSuccessfulResult = { stdout: 'Packages updated successfully', stderr: '', exitCode: 0, }; it('should update specific packages with npm', async () => { mockedExeca.mockResolvedValue(mockSuccessfulResult as any); const result = await service.update(['lodash', 'axios'], undefined, 'npm'); expect(mockedExeca).toHaveBeenCalledWith('npm', ['update', 'lodash', 'axios'], expect.any(Object)); expect(result.success).toBe(true); expect(result.packages).toEqual(['lodash', 'axios']); }); it('should update all packages when none specified', async () => { mockedExeca.mockResolvedValue(mockSuccessfulResult as any); const result = await service.update(undefined, undefined, 'npm'); expect(mockedExeca).toHaveBeenCalledWith('npm', ['update'], expect.any(Object)); expect(result.packages).toEqual(['all packages']); }); it('should use yarn upgrade command', async () => { mockedExeca.mockResolvedValue(mockSuccessfulResult as any); await service.update(['lodash'], undefined, 'yarn'); expect(mockedExeca).toHaveBeenCalledWith('yarn', ['upgrade', 'lodash'], expect.any(Object)); }); it('should use pnpm update command', async () => { mockedExeca.mockResolvedValue(mockSuccessfulResult as any); await service.update(['lodash'], undefined, 'pnpm'); expect(mockedExeca).toHaveBeenCalledWith('pnpm', ['update', 'lodash'], expect.any(Object)); }); }); describe('remove', () => { const mockSuccessfulResult = { stdout: 'Packages removed successfully', stderr: '', exitCode: 0, }; it('should remove packages with npm', async () => { mockedExeca.mockResolvedValue(mockSuccessfulResult as any); const result = await service.remove(['lodash'], undefined, false, 'npm'); expect(mockedExeca).toHaveBeenCalledWith('npm', ['uninstall', 'lodash'], expect.any(Object)); expect(result.success).toBe(true); }); it('should remove global packages with npm', async () => { mockedExeca.mockResolvedValue(mockSuccessfulResult as any); await service.remove(['typescript'], undefined, true, 'npm'); expect(mockedExeca).toHaveBeenCalledWith('npm', ['uninstall', '--global', 'typescript'], expect.any(Object)); }); it('should remove packages with yarn', async () => { mockedExeca.mockResolvedValue(mockSuccessfulResult as any); await service.remove(['lodash'], undefined, false, 'yarn'); expect(mockedExeca).toHaveBeenCalledWith('yarn', ['remove', 'lodash'], expect.any(Object)); }); it('should remove global packages with yarn', async () => { mockedExeca.mockResolvedValue(mockSuccessfulResult as any); await service.remove(['typescript'], undefined, true, 'yarn'); expect(mockedExeca).toHaveBeenCalledWith('yarn', ['remove', 'global', 'typescript'], expect.any(Object)); }); it('should remove packages with pnpm', async () => { mockedExeca.mockResolvedValue(mockSuccessfulResult as any); await service.remove(['lodash'], undefined, false, 'pnpm'); expect(mockedExeca).toHaveBeenCalledWith('pnpm', ['remove', 'lodash'], expect.any(Object)); }); }); describe('checkOutdated', () => { it('should check outdated packages', async () => { const mockResult = { stdout: 'Package Current Wanted Latest\nlodash 4.17.20 4.17.21 4.17.21', stderr: '', exitCode: 0, }; mockedExeca.mockResolvedValue(mockResult as any); const result = await service.checkOutdated(undefined, false, 'npm'); expect(mockedExeca).toHaveBeenCalledWith('npm', ['outdated'], { cwd: TEST_CONSTANTS.MOCK_CWD, timeout: TEST_CONSTANTS.TIMEOUT, reject: false, // Don't reject on non-zero exit stdio: 'pipe', }); expect(result.success).toBe(true); expect(result.operation).toBe('outdated'); }); it('should check global outdated packages', async () => { const mockResult = { stdout: 'No outdated packages', stderr: '', exitCode: 0 }; mockedExeca.mockResolvedValue(mockResult as any); await service.checkOutdated(undefined, true, 'npm'); expect(mockedExeca).toHaveBeenCalledWith('npm', ['outdated', '--global'], expect.any(Object)); }); }); describe('audit', () => { it('should audit dependencies', async () => { const mockResult = { stdout: 'found 0 vulnerabilities', stderr: '', exitCode: 0, }; mockedExeca.mockResolvedValue(mockResult as any); const result = await service.audit(undefined, false, false, false, 'npm'); expect(mockedExeca).toHaveBeenCalledWith('npm', ['audit'], { cwd: TEST_CONSTANTS.MOCK_CWD, timeout: TEST_CONSTANTS.TIMEOUT, reject: false, // Don't reject on vulnerabilities found stdio: 'pipe', }); expect(result.success).toBe(true); expect(result.operation).toBe('audit'); }); it('should audit and fix vulnerabilities', async () => { const mockResult = { stdout: 'fixed 2 vulnerabilities', stderr: '', exitCode: 0 }; mockedExeca.mockResolvedValue(mockResult as any); await service.audit(undefined, true, false, false, 'npm'); expect(mockedExeca).toHaveBeenCalledWith('npm', ['audit', 'fix'], expect.any(Object)); }); it('should audit with force fix', async () => { const mockResult = { stdout: 'force fixed vulnerabilities', stderr: '', exitCode: 0 }; mockedExeca.mockResolvedValue(mockResult as any); await service.audit(undefined, true, true, false, 'npm'); expect(mockedExeca).toHaveBeenCalledWith('npm', ['audit', 'fix', '--force'], expect.any(Object)); }); it('should audit production only', async () => { const mockResult = { stdout: 'production audit complete', stderr: '', exitCode: 0 }; mockedExeca.mockResolvedValue(mockResult as any); await service.audit(undefined, false, false, true, 'npm'); expect(mockedExeca).toHaveBeenCalledWith('npm', ['audit', '--production'], expect.any(Object)); }); }); describe('cleanCache', () => { it('should clean npm cache', async () => { const mockResult = { stdout: 'cache cleaned', stderr: '', exitCode: 0 }; mockedExeca.mockResolvedValue(mockResult as any); const result = await service.cleanCache(undefined, false, 'npm'); expect(mockedExeca).toHaveBeenCalledWith('npm', ['cache', 'clean', '--force'], expect.any(Object)); expect(result.success).toBe(true); }); it('should clean yarn cache', async () => { const mockResult = { stdout: 'cache cleaned', stderr: '', exitCode: 0 }; mockedExeca.mockResolvedValue(mockResult as any); await service.cleanCache(undefined, false, 'yarn'); expect(mockedExeca).toHaveBeenCalledWith('yarn', ['cache', 'clean'], expect.any(Object)); }); it('should clean pnpm store', async () => { const mockResult = { stdout: 'store pruned', stderr: '', exitCode: 0 }; mockedExeca.mockResolvedValue(mockResult as any); await service.cleanCache(undefined, false, 'pnpm'); expect(mockedExeca).toHaveBeenCalledWith('pnpm', ['store', 'prune'], expect.any(Object)); }); }); describe('Error Handling', () => { it('should handle command execution timeout', async () => { const timeoutError = new Error('Command timed out'); mockedExeca.mockRejectedValue(timeoutError); const result = await service.install(['large-package'], undefined, false, false, 'npm'); expect(result.success).toBe(false); expect(result.errors).toContain('Command timed out'); }); it('should handle network errors gracefully', async () => { const networkError = { message: 'Network error', stderr: 'Unable to connect to registry' }; mockedExeca.mockRejectedValue(networkError); const result = await service.install(['package'], undefined, false, false, 'npm'); expect(result.success).toBe(false); expect(result.output).toBe('Unable to connect to registry'); }); it('should handle unknown package manager', async () => { // @ts-ignore - Testing invalid package manager const result = await service.cleanCache(undefined, false, 'unknown' as PackageManagerType); expect(result.success).toBe(false); expect(result.errors?.[0]).toContain('Unknown package manager: unknown'); }); }); describe('Performance', () => { it('should complete operations within reasonable time', async () => { const mockResult = { stdout: 'success', stderr: '', exitCode: 0 }; // Add a small delay to simulate real execution time mockedExeca.mockResolvedValue(mockResult as any); const start = Date.now(); const result = await service.install(['lodash'], undefined, false, false, 'npm'); const duration = Date.now() - start; expect(result.duration).toBeGreaterThanOrEqual(0); expect(duration).toBeLessThan(1000); // Should complete quickly with mocked execa }); it('should respect timeout configuration', () => { const customTimeout = 30000; const customService = new PackageManagerService(customTimeout); expect(customService).toBeDefined(); // Timeout is private, so we can't directly test it, but we can verify the service is created }); }); });