mirror of
https://github.com/eyaltoledano/claude-task-master.git
synced 2025-12-05 19:40:59 +00:00
266 lines
8.2 KiB
TypeScript
266 lines
8.2 KiB
TypeScript
/**
|
|
* @fileoverview Unit tests for BaseProvider abstract class
|
|
*/
|
|
|
|
import { beforeEach, describe, expect, it } from 'vitest';
|
|
import {
|
|
ERROR_CODES,
|
|
TaskMasterError
|
|
} from '../../src/errors/task-master-error';
|
|
import { MockProvider } from '../mocks/mock-provider';
|
|
|
|
describe('BaseProvider', () => {
|
|
describe('constructor', () => {
|
|
it('should require an API key', () => {
|
|
expect(() => {
|
|
new MockProvider({ apiKey: '' });
|
|
}).toThrow(TaskMasterError);
|
|
});
|
|
|
|
it('should initialize with provided API key and model', () => {
|
|
const provider = new MockProvider({
|
|
apiKey: 'test-key',
|
|
model: 'mock-model-v2'
|
|
});
|
|
|
|
expect(provider.getModel()).toBe('mock-model-v2');
|
|
});
|
|
|
|
it('should use default model if not provided', () => {
|
|
const provider = new MockProvider({ apiKey: 'test-key' });
|
|
expect(provider.getModel()).toBe('mock-model-v1');
|
|
});
|
|
});
|
|
|
|
describe('generateCompletion', () => {
|
|
let provider: MockProvider;
|
|
|
|
beforeEach(() => {
|
|
provider = new MockProvider({ apiKey: 'test-key' });
|
|
});
|
|
|
|
it('should successfully generate a completion', async () => {
|
|
const response = await provider.generateCompletion('Test prompt');
|
|
|
|
expect(response).toMatchObject({
|
|
content: 'Mock response to: Test prompt',
|
|
provider: 'mock',
|
|
model: 'mock-model-v1',
|
|
inputTokens: expect.any(Number),
|
|
outputTokens: expect.any(Number),
|
|
totalTokens: expect.any(Number),
|
|
duration: expect.any(Number),
|
|
timestamp: expect.any(String)
|
|
});
|
|
});
|
|
|
|
it('should validate empty prompts', async () => {
|
|
await expect(provider.generateCompletion('')).rejects.toThrow(
|
|
'Prompt must be a non-empty string'
|
|
);
|
|
});
|
|
|
|
it('should validate prompt type', async () => {
|
|
await expect(provider.generateCompletion(null as any)).rejects.toThrow(
|
|
'Prompt must be a non-empty string'
|
|
);
|
|
});
|
|
|
|
it('should validate temperature range', async () => {
|
|
await expect(
|
|
provider.generateCompletion('Test', { temperature: 3 })
|
|
).rejects.toThrow('Temperature must be between 0 and 2');
|
|
});
|
|
|
|
it('should validate maxTokens range', async () => {
|
|
await expect(
|
|
provider.generateCompletion('Test', { maxTokens: 0 })
|
|
).rejects.toThrow('Max tokens must be between 1 and 100000');
|
|
});
|
|
|
|
it('should validate topP range', async () => {
|
|
await expect(
|
|
provider.generateCompletion('Test', { topP: 1.5 })
|
|
).rejects.toThrow('Top-p must be between 0 and 1');
|
|
});
|
|
});
|
|
|
|
describe('retry logic', () => {
|
|
it('should retry on rate limit errors', async () => {
|
|
const provider = new MockProvider({
|
|
apiKey: 'test-key',
|
|
failAfterAttempts: 2,
|
|
simulateRateLimit: true,
|
|
responseDelay: 10
|
|
});
|
|
|
|
const response = await provider.generateCompletion('Test prompt');
|
|
|
|
expect(response.content).toBe('Mock response to: Test prompt');
|
|
expect(provider.getAttemptCount()).toBe(3); // 2 failures + 1 success
|
|
});
|
|
|
|
it('should retry on timeout errors', async () => {
|
|
const provider = new MockProvider({
|
|
apiKey: 'test-key',
|
|
failAfterAttempts: 1,
|
|
simulateTimeout: true
|
|
});
|
|
|
|
const response = await provider.generateCompletion('Test prompt');
|
|
|
|
expect(response.content).toBe('Mock response to: Test prompt');
|
|
expect(provider.getAttemptCount()).toBe(2); // 1 failure + 1 success
|
|
});
|
|
|
|
it('should fail after max retries', async () => {
|
|
const provider = new MockProvider({
|
|
apiKey: 'test-key',
|
|
shouldFail: true
|
|
});
|
|
|
|
await expect(provider.generateCompletion('Test prompt')).rejects.toThrow(
|
|
'mock provider error'
|
|
);
|
|
});
|
|
|
|
it('should calculate exponential backoff delays', () => {
|
|
const provider = new MockProvider({ apiKey: 'test-key' });
|
|
|
|
// Access protected method through type assertion
|
|
const calculateDelay = (provider as any).calculateBackoffDelay.bind(
|
|
provider
|
|
);
|
|
|
|
const delay1 = calculateDelay(1);
|
|
const delay2 = calculateDelay(2);
|
|
const delay3 = calculateDelay(3);
|
|
|
|
// Check exponential growth (with jitter, so use ranges)
|
|
expect(delay1).toBeGreaterThanOrEqual(900);
|
|
expect(delay1).toBeLessThanOrEqual(1100);
|
|
|
|
expect(delay2).toBeGreaterThanOrEqual(1800);
|
|
expect(delay2).toBeLessThanOrEqual(2200);
|
|
|
|
expect(delay3).toBeGreaterThanOrEqual(3600);
|
|
expect(delay3).toBeLessThanOrEqual(4400);
|
|
});
|
|
});
|
|
|
|
describe('error handling', () => {
|
|
it('should wrap provider errors properly', async () => {
|
|
const provider = new MockProvider({
|
|
apiKey: 'test-key',
|
|
shouldFail: true
|
|
});
|
|
|
|
try {
|
|
await provider.generateCompletion('Test prompt');
|
|
expect.fail('Should have thrown an error');
|
|
} catch (error) {
|
|
expect(error).toBeInstanceOf(TaskMasterError);
|
|
const tmError = error as TaskMasterError;
|
|
expect(tmError.code).toBe(ERROR_CODES.PROVIDER_ERROR);
|
|
expect(tmError.context.operation).toBe('generateCompletion');
|
|
expect(tmError.context.resource).toBe('mock');
|
|
}
|
|
});
|
|
|
|
it('should identify rate limit errors correctly', () => {
|
|
const provider = new MockProvider({ apiKey: 'test-key' });
|
|
const isRateLimitError = (provider as any).isRateLimitError.bind(
|
|
provider
|
|
);
|
|
|
|
expect(isRateLimitError(new Error('Rate limit exceeded'))).toBe(true);
|
|
expect(isRateLimitError(new Error('Too many requests'))).toBe(true);
|
|
expect(isRateLimitError(new Error('Status: 429'))).toBe(true);
|
|
expect(isRateLimitError(new Error('Some other error'))).toBe(false);
|
|
});
|
|
|
|
it('should identify timeout errors correctly', () => {
|
|
const provider = new MockProvider({ apiKey: 'test-key' });
|
|
const isTimeoutError = (provider as any).isTimeoutError.bind(provider);
|
|
|
|
expect(isTimeoutError(new Error('Request timeout'))).toBe(true);
|
|
expect(isTimeoutError(new Error('Operation timed out'))).toBe(true);
|
|
expect(isTimeoutError(new Error('ECONNRESET'))).toBe(true);
|
|
expect(isTimeoutError(new Error('Some other error'))).toBe(false);
|
|
});
|
|
|
|
it('should identify network errors correctly', () => {
|
|
const provider = new MockProvider({ apiKey: 'test-key' });
|
|
const isNetworkError = (provider as any).isNetworkError.bind(provider);
|
|
|
|
expect(isNetworkError(new Error('Network error'))).toBe(true);
|
|
expect(isNetworkError(new Error('ENOTFOUND'))).toBe(true);
|
|
expect(isNetworkError(new Error('ECONNREFUSED'))).toBe(true);
|
|
expect(isNetworkError(new Error('Some other error'))).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('model management', () => {
|
|
it('should get and set model', () => {
|
|
const provider = new MockProvider({ apiKey: 'test-key' });
|
|
|
|
expect(provider.getModel()).toBe('mock-model-v1');
|
|
|
|
provider.setModel('mock-model-v2');
|
|
expect(provider.getModel()).toBe('mock-model-v2');
|
|
});
|
|
});
|
|
|
|
describe('provider information', () => {
|
|
it('should return provider info', () => {
|
|
const provider = new MockProvider({ apiKey: 'test-key' });
|
|
const info = provider.getProviderInfo();
|
|
|
|
expect(info.name).toBe('mock');
|
|
expect(info.displayName).toBe('Mock Provider');
|
|
expect(info.requiresApiKey).toBe(true);
|
|
expect(info.models).toHaveLength(2);
|
|
});
|
|
|
|
it('should return available models', () => {
|
|
const provider = new MockProvider({ apiKey: 'test-key' });
|
|
const models = provider.getAvailableModels();
|
|
|
|
expect(models).toHaveLength(2);
|
|
expect(models[0].id).toBe('mock-model-v1');
|
|
expect(models[1].id).toBe('mock-model-v2');
|
|
});
|
|
|
|
it('should validate credentials', async () => {
|
|
const validProvider = new MockProvider({ apiKey: 'valid-key' });
|
|
const invalidProvider = new MockProvider({ apiKey: 'invalid-key' });
|
|
|
|
expect(await validProvider.validateCredentials()).toBe(true);
|
|
expect(await invalidProvider.validateCredentials()).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('template method pattern', () => {
|
|
it('should follow the template method flow', async () => {
|
|
const provider = new MockProvider({
|
|
apiKey: 'test-key',
|
|
responseDelay: 50
|
|
});
|
|
|
|
const startTime = Date.now();
|
|
const response = await provider.generateCompletion('Test prompt', {
|
|
temperature: 0.5,
|
|
maxTokens: 100
|
|
});
|
|
const endTime = Date.now();
|
|
|
|
// Verify the response was processed through the template
|
|
expect(response.content).toBeDefined();
|
|
expect(response.duration).toBeGreaterThanOrEqual(50);
|
|
expect(response.duration).toBeLessThanOrEqual(endTime - startTime + 10);
|
|
expect(response.timestamp).toBeDefined();
|
|
expect(response.provider).toBe('mock');
|
|
});
|
|
});
|
|
});
|