cmte
Version:
Design by Committee™ except it's just you and LLMs
199 lines (185 loc) • 6.44 kB
JavaScript
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
}));
});
});
});