| |
| |
| |
| |
| |
| |
|
|
| import { describe, it, expect } from 'vitest'; |
| import { |
| detectCli, |
| detectAllCLis, |
| findCommand, |
| getCliVersion, |
| getInstallInstructions, |
| validateCliInstallation, |
| } from '../lib/cli-detection.js'; |
| import { classifyError, getUserFriendlyErrorMessage } from '../lib/error-handler.js'; |
|
|
| describe('CLI Detection Framework', () => { |
| describe('findCommand', () => { |
| it('should find existing command', async () => { |
| |
| const result = await findCommand(['node']); |
| expect(result).toBeTruthy(); |
| }); |
|
|
| it('should return null for non-existent command', async () => { |
| const result = await findCommand(['nonexistent-command-12345']); |
| expect(result).toBeNull(); |
| }); |
|
|
| it('should find first available command from alternatives', async () => { |
| const result = await findCommand(['nonexistent-command-12345', 'node']); |
| expect(result).toBeTruthy(); |
| expect(result).toContain('node'); |
| }); |
| }); |
|
|
| describe('getCliVersion', () => { |
| it('should get version for existing command', async () => { |
| const version = await getCliVersion('node', ['--version'], 5000); |
| expect(version).toBeTruthy(); |
| expect(typeof version).toBe('string'); |
| }); |
|
|
| it('should timeout for non-responsive command', async () => { |
| await expect(getCliVersion('sleep', ['10'], 1000)).rejects.toThrow(); |
| }, 15000); |
|
|
| it("should handle command that doesn't exist", async () => { |
| await expect( |
| getCliVersion('nonexistent-command-12345', ['--version'], 2000) |
| ).rejects.toThrow(); |
| }); |
| }); |
|
|
| describe('getInstallInstructions', () => { |
| it('should return instructions for supported platforms', () => { |
| const claudeInstructions = getInstallInstructions('claude', 'darwin'); |
| expect(claudeInstructions).toContain('brew install'); |
|
|
| const codexInstructions = getInstallInstructions('codex', 'linux'); |
| expect(codexInstructions).toContain('npm install'); |
| }); |
|
|
| it('should handle unsupported platform', () => { |
| const instructions = getInstallInstructions('claude', 'unknown-platform' as NodeJS.Platform); |
| expect(instructions).toContain('No installation instructions available'); |
| }); |
| }); |
|
|
| describe('validateCliInstallation', () => { |
| it('should validate properly installed CLI', () => { |
| const cliInfo = { |
| name: 'Test CLI', |
| command: 'node', |
| version: 'v18.0.0', |
| path: '/usr/bin/node', |
| installed: true, |
| authenticated: true, |
| authMethod: 'cli' as const, |
| }; |
|
|
| const result = validateCliInstallation(cliInfo); |
| expect(result.valid).toBe(true); |
| expect(result.issues).toHaveLength(0); |
| }); |
|
|
| it('should detect issues with installation', () => { |
| const cliInfo = { |
| name: 'Test CLI', |
| command: '', |
| version: '', |
| path: '', |
| installed: false, |
| authenticated: false, |
| authMethod: 'none' as const, |
| }; |
|
|
| const result = validateCliInstallation(cliInfo); |
| expect(result.valid).toBe(false); |
| expect(result.issues.length).toBeGreaterThan(0); |
| expect(result.issues).toContain('CLI is not installed'); |
| }); |
| }); |
| }); |
|
|
| describe('Error Handling System', () => { |
| describe('classifyError', () => { |
| it('should classify authentication errors', () => { |
| const authError = new Error('invalid_api_key: Your API key is invalid'); |
| const result = classifyError(authError, 'claude'); |
|
|
| expect(result.type).toBe('authentication'); |
| expect(result.severity).toBe('high'); |
| expect(result.userMessage).toContain('Authentication failed'); |
| expect(result.retryable).toBe(false); |
| expect(result.provider).toBe('claude'); |
| }); |
|
|
| it('should classify billing errors', () => { |
| const billingError = new Error('credit balance is too low'); |
| const result = classifyError(billingError); |
|
|
| expect(result.type).toBe('billing'); |
| expect(result.severity).toBe('high'); |
| expect(result.userMessage).toContain('insufficient credits'); |
| expect(result.retryable).toBe(false); |
| }); |
|
|
| it('should classify rate limit errors', () => { |
| const rateLimitError = new Error('Rate limit reached. Try again later.'); |
| const result = classifyError(rateLimitError); |
|
|
| expect(result.type).toBe('rate_limit'); |
| expect(result.severity).toBe('medium'); |
| expect(result.userMessage).toContain('Rate limit reached'); |
| expect(result.retryable).toBe(true); |
| }); |
|
|
| it('should classify network errors', () => { |
| const networkError = new Error('ECONNREFUSED: Connection refused'); |
| const result = classifyError(networkError); |
|
|
| expect(result.type).toBe('network'); |
| expect(result.severity).toBe('medium'); |
| expect(result.userMessage).toContain('Network connection issue'); |
| expect(result.retryable).toBe(true); |
| }); |
|
|
| it('should handle unknown errors', () => { |
| const unknownError = new Error('Something completely unexpected happened'); |
| const result = classifyError(unknownError); |
|
|
| expect(result.type).toBe('unknown'); |
| expect(result.severity).toBe('medium'); |
| expect(result.userMessage).toContain('unexpected error'); |
| expect(result.retryable).toBe(true); |
| }); |
| }); |
|
|
| describe('getUserFriendlyErrorMessage', () => { |
| it('should include provider name in message', () => { |
| const error = new Error('invalid_api_key'); |
| const message = getUserFriendlyErrorMessage(error, 'claude'); |
|
|
| expect(message).toContain('[CLAUDE]'); |
| }); |
|
|
| it('should include suggested action when available', () => { |
| const error = new Error('invalid_api_key'); |
| const message = getUserFriendlyErrorMessage(error); |
|
|
| expect(message).toContain('Verify your API key'); |
| }); |
| }); |
| }); |
|
|
| describe('Provider-Specific Tests', () => { |
| describe('Claude CLI Detection', () => { |
| it('should detect Claude CLI if installed', async () => { |
| const result = await detectCli('claude'); |
|
|
| if (result.detected) { |
| expect(result.cli.name).toBe('Claude CLI'); |
| expect(result.cli.installed).toBe(true); |
| expect(result.cli.command).toBeTruthy(); |
| } |
| |
| }); |
|
|
| it('should handle missing Claude CLI gracefully', async () => { |
| |
| const result = await detectCli('claude'); |
| expect(typeof result.detected).toBe('boolean'); |
| expect(Array.isArray(result.issues)).toBe(true); |
| }); |
| }); |
|
|
| describe('Codex CLI Detection', () => { |
| it('should detect Codex CLI if installed', async () => { |
| const result = await detectCli('codex'); |
|
|
| if (result.detected) { |
| expect(result.cli.name).toBe('Codex CLI'); |
| expect(result.cli.installed).toBe(true); |
| expect(result.cli.command).toBeTruthy(); |
| } |
| }); |
| }); |
|
|
| describe('Cursor CLI Detection', () => { |
| it('should detect Cursor CLI if installed', async () => { |
| const result = await detectCli('cursor'); |
|
|
| if (result.detected) { |
| expect(result.cli.name).toBe('Cursor CLI'); |
| expect(result.cli.installed).toBe(true); |
| expect(result.cli.command).toBeTruthy(); |
| } |
| }); |
| }); |
| }); |
|
|
| describe('Integration Tests', () => { |
| describe('detectAllCLis', () => { |
| it('should detect all available CLIs', async () => { |
| const results = await detectAllCLis(); |
|
|
| expect(results).toHaveProperty('claude'); |
| expect(results).toHaveProperty('codex'); |
| expect(results).toHaveProperty('cursor'); |
|
|
| |
| Object.values(results).forEach((result) => { |
| expect(result).toHaveProperty('cli'); |
| expect(result).toHaveProperty('detected'); |
| expect(result).toHaveProperty('issues'); |
| expect(result.cli).toHaveProperty('name'); |
| expect(result.cli).toHaveProperty('installed'); |
| expect(result.cli).toHaveProperty('authenticated'); |
| }); |
| }, 30000); |
|
|
| it('should handle concurrent CLI detection', async () => { |
| |
| const promises = [detectAllCLis(), detectAllCLis(), detectAllCLis()]; |
|
|
| const results = await Promise.all(promises); |
|
|
| |
| expect(results).toHaveLength(3); |
| results.forEach((result) => { |
| expect(result).toHaveProperty('claude'); |
| expect(result).toHaveProperty('codex'); |
| expect(result).toHaveProperty('cursor'); |
| }); |
| }, 45000); |
| }); |
| }); |
|
|
| describe('Error Recovery Tests', () => { |
| it('should handle partial CLI detection failures', async () => { |
| |
| const results = await detectAllCLis(); |
|
|
| |
| expect(results).toHaveProperty('claude'); |
| expect(results).toHaveProperty('codex'); |
| expect(results).toHaveProperty('cursor'); |
|
|
| |
| Object.entries(results).forEach(([_provider, result]) => { |
| if (!result.detected && result.issues.length > 0) { |
| expect(result.issues.length).toBeGreaterThan(0); |
| expect(result.issues[0]).toBeTruthy(); |
| } |
| }); |
| }); |
|
|
| it('should handle timeout during CLI detection', async () => { |
| |
| const result = await detectCli('claude', { timeout: 1 }); |
|
|
| |
| expect(typeof result.detected).toBe('boolean'); |
| expect(Array.isArray(result.issues)).toBe(true); |
| }); |
| }); |
|
|
| describe('Security Tests', () => { |
| it('should not expose sensitive information in error messages', () => { |
| const errorWithKey = new Error('invalid_api_key: sk-ant-abc123secret456'); |
| const message = getUserFriendlyErrorMessage(errorWithKey); |
|
|
| |
| expect(message).not.toContain('sk-ant-abc123secret456'); |
| expect(message).toContain('Authentication failed'); |
| }); |
|
|
| it('should sanitize file paths in error messages', () => { |
| const errorWithPath = new Error('Permission denied: /home/user/.ssh/id_rsa'); |
| const message = getUserFriendlyErrorMessage(errorWithPath); |
|
|
| |
| expect(message).not.toContain('/home/user/.ssh/id_rsa'); |
| }); |
| }); |
|
|
| |
| describe('Performance Tests', () => { |
| it('should detect CLIs within reasonable time', async () => { |
| const startTime = Date.now(); |
| const results = await detectAllCLis(); |
| const endTime = Date.now(); |
|
|
| const duration = endTime - startTime; |
| expect(duration).toBeLessThan(10000); |
| expect(results).toHaveProperty('claude'); |
| expect(results).toHaveProperty('codex'); |
| expect(results).toHaveProperty('cursor'); |
| }, 15000); |
|
|
| it('should handle rapid repeated calls', async () => { |
| |
| const promises = Array.from({ length: 10 }, () => detectAllCLis()); |
| const results = await Promise.all(promises); |
|
|
| |
| expect(results).toHaveLength(10); |
| results.forEach((result) => { |
| expect(result).toHaveProperty('claude'); |
| expect(result).toHaveProperty('codex'); |
| expect(result).toHaveProperty('cursor'); |
| }); |
| }, 60000); |
| }); |
|
|
| |
| describe('Edge Cases', () => { |
| it('should handle empty CLI names', async () => { |
| await expect(detectCli('' as unknown as Parameters<typeof detectCli>[0])).rejects.toThrow(); |
| }); |
|
|
| it('should handle null CLI names', async () => { |
| await expect(detectCli(null as unknown as Parameters<typeof detectCli>[0])).rejects.toThrow(); |
| }); |
|
|
| it('should handle undefined CLI names', async () => { |
| await expect( |
| detectCli(undefined as unknown as Parameters<typeof detectCli>[0]) |
| ).rejects.toThrow(); |
| }); |
|
|
| it('should handle malformed error objects', () => { |
| const testCases = [ |
| null, |
| undefined, |
| '', |
| 123, |
| [], |
| { nested: { error: { message: 'test' } } }, |
| { error: 'simple string error' }, |
| ]; |
|
|
| testCases.forEach((error) => { |
| expect(() => { |
| const result = classifyError(error); |
| expect(result).toHaveProperty('type'); |
| expect(result).toHaveProperty('severity'); |
| expect(result).toHaveProperty('userMessage'); |
| }).not.toThrow(); |
| }); |
| }); |
| }); |
|
|