Files
automaker/apps/server/src/tests/cli-integration.test.ts
DhanushSantosh 2250367ddc chore: update npm audit level in CI workflow
- Changed the npm audit command in the security audit workflow to check for critical vulnerabilities instead of moderate ones.
- This adjustment enhances the security posture of the application by ensuring that critical issues are identified and addressed promptly.
2026-01-07 20:24:49 +05:30

374 lines
12 KiB
TypeScript

/**
* CLI Integration Tests
*
* Comprehensive tests for CLI detection, authentication, and operations
* across all providers (Claude, Codex, Cursor)
*/
import { describe, it, expect, beforeEach, afterEach } 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 () => {
// Test with a command that should exist
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); // Give extra time for test timeout
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 any);
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();
}
// If not installed, that's also a valid test result
});
it('should handle missing Claude CLI gracefully', async () => {
// This test will pass regardless of whether Claude is installed
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');
// Each should have the expected structure
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); // Longer timeout for CLI detection
it('should handle concurrent CLI detection', async () => {
// Run detection multiple times concurrently
const promises = [detectAllCLis(), detectAllCLis(), detectAllCLis()];
const results = await Promise.all(promises);
// All should return consistent results
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 () => {
// Mock a scenario where some CLIs fail to detect
const results = await detectAllCLis();
// Should still return results for all providers
expect(results).toHaveProperty('claude');
expect(results).toHaveProperty('codex');
expect(results).toHaveProperty('cursor');
// Should provide error information for failures
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 () => {
// Test with very short timeout
const result = await detectCli('claude', { timeout: 1 });
// Should handle gracefully without throwing
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);
// Should not expose the actual API key
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);
// Should not expose sensitive file paths
expect(message).not.toContain('/home/user/.ssh/id_rsa');
});
});
// Performance Tests
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); // Should complete in under 10 seconds
expect(results).toHaveProperty('claude');
expect(results).toHaveProperty('codex');
expect(results).toHaveProperty('cursor');
}, 15000);
it('should handle rapid repeated calls', async () => {
// Make multiple rapid calls
const promises = Array.from({ length: 10 }, () => detectAllCLis());
const results = await Promise.all(promises);
// All should complete successfully
expect(results).toHaveLength(10);
results.forEach((result) => {
expect(result).toHaveProperty('claude');
expect(result).toHaveProperty('codex');
expect(result).toHaveProperty('cursor');
});
}, 60000);
});
// Edge Cases
describe('Edge Cases', () => {
it('should handle empty CLI names', async () => {
await expect(detectCli('' as any)).rejects.toThrow();
});
it('should handle null CLI names', async () => {
await expect(detectCli(null as any)).rejects.toThrow();
});
it('should handle undefined CLI names', async () => {
await expect(detectCli(undefined as any)).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();
});
});
});