UNPKG

cmte

Version:

Design by Committee™ except it's just you and LLMs

199 lines (185 loc) 6.44 kB
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { ClaudeAdapter } from "../claude-adapter.js"; import { Anthropic } from '@anthropic-ai/sdk'; // Mock logger vi.mock('../../../utils/logger.js', () => ({ logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() } })); // Mock Anthropic SDK vi.mock('@anthropic-ai/sdk', () => ({ Anthropic: vi.fn().mockImplementation(() => ({ messages: { create: vi.fn() } })) })); // Helper to create a mock async stream async function* createMockStream(chunks) { for (const chunk of chunks) { yield { type: 'content_block_delta', delta: { type: 'text_delta', text: chunk } }; } } // Store original env const originalEnv = process.env; // Describe block for integration tests describe('ClaudeAdapter Integration Tests', () => { let adapter; let mockAnthropicCreate; // Increase timeout for tests hitting the network vi.setConfig({ testTimeout: 30000 }); // 30 seconds beforeEach(() => { // Reset environment process.env = { ...originalEnv }; process.env.NODE_ENV = 'test'; process.env.ANTHROPIC_API_KEY = 'test-key'; // Reset mocks vi.clearAllMocks(); // Setup mock for Anthropic messages.create mockAnthropicCreate = vi.fn(); Anthropic.mockImplementation(() => ({ messages: { create: mockAnthropicCreate } })); // Create adapter instance adapter = new ClaudeAdapter({ provider: 'anthropic', model: 'test-model', lite: false, apiDryRun: false }); }); afterEach(() => { process.env = originalEnv; vi.clearAllMocks(); // Reset timeout to default if needed elsewhere, or keep it increased for all tests in this file // vi.setConfig({ testTimeout: 5000 }); }); describe('constructor', () => { it('should initialize with API key from environment', () => { expect(Anthropic).toHaveBeenCalledWith({ apiKey: 'test-key' }); }); it('should throw error if API key is missing in non-test environment', () => { process.env.NODE_ENV = 'development'; process.env.ANTHROPIC_API_KEY = undefined; expect(() => new ClaudeAdapter({ provider: 'anthropic', model: 'test-model', lite: false, apiDryRun: false })).toThrow('ANTHROPIC_API_KEY environment variable is required'); }); }); describe('healthCheck', () => { it('should return true when client is healthy', async () => { // Health check calls completePrompt, which calls completeMessages, which now streams mockAnthropicCreate.mockResolvedValueOnce(createMockStream(['test'])); const result = await adapter.healthCheck(); expect(result).toBe(true); }); it('should return false when client is not initialized', async () => { adapter.anthropic = null; const result = await adapter.healthCheck(); expect(result).toBe(false); }); it('should return false when health check fails', async () => { // Mock the completeMessages method before creating the adapter const originalCompleteMessages = ClaudeAdapter.prototype.completeMessages; ClaudeAdapter.prototype.completeMessages = vi.fn().mockRejectedValueOnce(new Error('API Error')); const result = await adapter.healthCheck(); expect(result).toBe(false); // Restore the original implementation ClaudeAdapter.prototype.completeMessages = originalCompleteMessages; }); }); describe('completeMessages', () => { const testMessages = [{ role: 'user', content: 'Hello' }]; it('should handle successful message completion', async () => { // Mock the stream response mockAnthropicCreate.mockResolvedValueOnce(createMockStream(['Hello', ', ', 'human!'])); const result = await adapter.completeMessages(testMessages); expect(result).toBe('Hello, human!'); expect(mockAnthropicCreate).toHaveBeenCalledWith({ model: 'test-model', messages: testMessages, temperature: 0.7, max_tokens: 10000, stream: true // Ensure mock is called with stream: true }); }); it('should use lite model when configured', async () => { adapter = new ClaudeAdapter({ provider: 'anthropic', model: 'claude-3-haiku-20240307', lite: true, apiDryRun: false }); // Mock the stream response mockAnthropicCreate.mockResolvedValueOnce(createMockStream(['Hello!'])); await adapter.completeMessages(testMessages); expect(mockAnthropicCreate).toHaveBeenCalledWith(expect.objectContaining({ model: 'claude-3-haiku-20240307', stream: true })); }); it('should handle API dry run mode', async () => { adapter = new ClaudeAdapter({ provider: 'anthropic', model: 'test-model', lite: false, apiDryRun: true }); const result = await adapter.completeMessages(testMessages); expect(result).toContain('[API Dry Run]'); expect(mockAnthropicCreate).not.toHaveBeenCalled(); }); it('should throw error when client is not initialized', async () => { adapter.anthropic = null; await expect(adapter.completeMessages(testMessages)).rejects.toThrow('Claude client not initialized'); }); it('should use custom model config when provided', async () => { // Mock the stream response mockAnthropicCreate.mockResolvedValueOnce(createMockStream(['Hello!'])); await adapter.completeMessages(testMessages, { temperature: 0.5, maxTokens: 1000, model: 'custom-model' }); expect(mockAnthropicCreate).toHaveBeenCalledWith(expect.objectContaining({ temperature: 0.5, max_tokens: 1000, model: 'custom-model', stream: true })); }); }); describe('completePrompt', () => { it('should convert prompt to message format and handle stream', async () => { // Mock the stream response mockAnthropicCreate.mockResolvedValueOnce(createMockStream(['Response'])); await adapter.completePrompt('Test prompt'); expect(mockAnthropicCreate).toHaveBeenCalledWith(expect.objectContaining({ messages: [{ role: 'user', content: 'Test prompt' }], stream: true })); }); }); });