Files
claude-task-master/tests/unit/ai-providers/provider-registry.test.js

418 lines
12 KiB
JavaScript

/**
* Tests for ProviderRegistry - Singleton for managing AI providers
*
* This test suite covers:
* 1. Singleton pattern behavior
* 2. Provider registration and validation
* 3. Provider retrieval and management
* 4. Provider unregistration
* 5. Registry reset (for testing)
* 6. Interface validation for registered providers
*/
import { jest } from '@jest/globals';
// Import ProviderRegistry
const { default: ProviderRegistry } = await import(
'../../../src/provider-registry/index.js'
);
// Mock provider classes for testing
class MockValidProvider {
constructor() {
this.name = 'MockValidProvider';
}
generateText() {
return Promise.resolve({ text: 'mock text' });
}
streamText() {
return Promise.resolve('mock stream');
}
generateObject() {
return Promise.resolve({ object: {} });
}
getRequiredApiKeyName() {
return 'MOCK_API_KEY';
}
}
class MockInvalidProvider {
constructor() {
this.name = 'MockInvalidProvider';
}
// Missing required methods: generateText, streamText, generateObject
}
describe('ProviderRegistry', () => {
let registry;
beforeEach(() => {
// Get a fresh instance and reset it
registry = ProviderRegistry.getInstance();
registry.reset();
});
afterEach(() => {
// Clean up after each test
registry.reset();
});
describe('Singleton Pattern', () => {
test('getInstance returns the same instance', () => {
const instance1 = ProviderRegistry.getInstance();
const instance2 = ProviderRegistry.getInstance();
expect(instance1).toBe(instance2);
expect(instance1).toBe(registry);
});
test('multiple calls to getInstance return same instance', () => {
const instances = Array.from({ length: 5 }, () =>
ProviderRegistry.getInstance()
);
instances.forEach((instance) => {
expect(instance).toBe(registry);
});
});
});
describe('Initialization', () => {
test('registry is not auto-initialized when mocked', () => {
// When mocked, the auto-initialization at import may not occur
expect(registry._initialized).toBe(false);
});
test('initialize sets initialized flag', () => {
expect(registry._initialized).toBe(false);
const result = registry.initialize();
expect(registry._initialized).toBe(true);
expect(result).toBe(registry);
});
test('initialize can be called multiple times safely', () => {
// First call initializes
registry.initialize();
expect(registry._initialized).toBe(true);
// Second call should not throw
expect(() => registry.initialize()).not.toThrow();
});
test('initialize returns self for chaining', () => {
const result = registry.initialize();
expect(result).toBe(registry);
});
});
describe('Provider Registration', () => {
test('registerProvider adds valid provider successfully', () => {
const mockProvider = new MockValidProvider();
const options = { priority: 'high' };
const result = registry.registerProvider('mock', mockProvider, options);
expect(result).toBe(registry); // Should return self for chaining
expect(registry.hasProvider('mock')).toBe(true);
});
test('registerProvider validates provider name', () => {
const mockProvider = new MockValidProvider();
// Test empty string
expect(() => registry.registerProvider('', mockProvider)).toThrow(
'Provider name must be a non-empty string'
);
// Test null
expect(() => registry.registerProvider(null, mockProvider)).toThrow(
'Provider name must be a non-empty string'
);
// Test non-string
expect(() => registry.registerProvider(123, mockProvider)).toThrow(
'Provider name must be a non-empty string'
);
});
test('registerProvider validates provider instance', () => {
expect(() => registry.registerProvider('mock', null)).toThrow(
'Provider instance is required'
);
expect(() => registry.registerProvider('mock', undefined)).toThrow(
'Provider instance is required'
);
});
test('registerProvider validates provider interface', () => {
const invalidProvider = new MockInvalidProvider();
expect(() => registry.registerProvider('mock', invalidProvider)).toThrow(
'Provider must implement BaseAIProvider interface'
);
});
test('registerProvider stores provider with metadata', () => {
const mockProvider = new MockValidProvider();
const options = { priority: 'high', custom: 'value' };
const beforeRegistration = new Date();
registry.registerProvider('mock', mockProvider, options);
const storedEntry = registry._providers.get('mock');
expect(storedEntry.instance).toBe(mockProvider);
expect(storedEntry.options).toEqual(options);
expect(storedEntry.registeredAt).toBeInstanceOf(Date);
expect(storedEntry.registeredAt.getTime()).toBeGreaterThanOrEqual(
beforeRegistration.getTime()
);
});
test('registerProvider can overwrite existing providers', () => {
const provider1 = new MockValidProvider();
const provider2 = new MockValidProvider();
registry.registerProvider('mock', provider1);
expect(registry.getProvider('mock')).toBe(provider1);
registry.registerProvider('mock', provider2);
expect(registry.getProvider('mock')).toBe(provider2);
});
test('registerProvider handles missing options', () => {
const mockProvider = new MockValidProvider();
registry.registerProvider('mock', mockProvider);
const storedEntry = registry._providers.get('mock');
expect(storedEntry.options).toEqual({});
});
});
describe('Provider Retrieval', () => {
beforeEach(() => {
const mockProvider = new MockValidProvider();
registry.registerProvider('mock', mockProvider, { test: 'value' });
});
test('hasProvider returns correct boolean values', () => {
expect(registry.hasProvider('mock')).toBe(true);
expect(registry.hasProvider('nonexistent')).toBe(false);
expect(registry.hasProvider('')).toBe(false);
expect(registry.hasProvider(null)).toBe(false);
});
test('getProvider returns correct provider instance', () => {
const provider = registry.getProvider('mock');
expect(provider).toBeInstanceOf(MockValidProvider);
expect(provider.name).toBe('MockValidProvider');
});
test('getProvider returns null for nonexistent provider', () => {
expect(registry.getProvider('nonexistent')).toBe(null);
expect(registry.getProvider('')).toBe(null);
expect(registry.getProvider(null)).toBe(null);
});
test('getAllProviders returns copy of providers map', () => {
const mockProvider2 = new MockValidProvider();
registry.registerProvider('mock2', mockProvider2);
const allProviders = registry.getAllProviders();
expect(allProviders).toBeInstanceOf(Map);
expect(allProviders.size).toBe(2);
expect(allProviders.has('mock')).toBe(true);
expect(allProviders.has('mock2')).toBe(true);
// Should be a copy, not the original
expect(allProviders).not.toBe(registry._providers);
});
test('getAllProviders returns empty map when no providers', () => {
registry.reset();
const allProviders = registry.getAllProviders();
expect(allProviders).toBeInstanceOf(Map);
expect(allProviders.size).toBe(0);
});
});
describe('Provider Unregistration', () => {
beforeEach(() => {
const mockProvider = new MockValidProvider();
registry.registerProvider('mock', mockProvider);
});
test('unregisterProvider removes existing provider', () => {
expect(registry.hasProvider('mock')).toBe(true);
const result = registry.unregisterProvider('mock');
expect(result).toBe(true);
expect(registry.hasProvider('mock')).toBe(false);
});
test('unregisterProvider returns false for nonexistent provider', () => {
const result = registry.unregisterProvider('nonexistent');
expect(result).toBe(false);
});
test('unregisterProvider handles edge cases', () => {
expect(registry.unregisterProvider('')).toBe(false);
expect(registry.unregisterProvider(null)).toBe(false);
expect(registry.unregisterProvider(undefined)).toBe(false);
});
});
describe('Registry Reset', () => {
beforeEach(() => {
const mockProvider = new MockValidProvider();
registry.registerProvider('mock', mockProvider);
registry.initialize();
});
test('reset clears all providers', () => {
expect(registry.hasProvider('mock')).toBe(true);
expect(registry._initialized).toBe(true);
registry.reset();
expect(registry.hasProvider('mock')).toBe(false);
expect(registry._providers.size).toBe(0);
});
test('reset clears initialization flag', () => {
expect(registry._initialized).toBe(true);
registry.reset();
expect(registry._initialized).toBe(false);
});
// No log assertion for reset, just call reset
test('reset can be called without error', () => {
expect(() => registry.reset()).not.toThrow();
});
test('reset allows re-initialization', () => {
registry.reset();
expect(registry._initialized).toBe(false);
registry.initialize();
expect(registry._initialized).toBe(true);
});
});
describe('Interface Validation', () => {
test('validates generateText method exists', () => {
const providerWithoutGenerateText = {
streamText: jest.fn(),
generateObject: jest.fn()
};
expect(() =>
registry.registerProvider('invalid', providerWithoutGenerateText)
).toThrow('Provider must implement BaseAIProvider interface');
});
test('validates streamText method exists', () => {
const providerWithoutStreamText = {
generateText: jest.fn(),
generateObject: jest.fn()
};
expect(() =>
registry.registerProvider('invalid', providerWithoutStreamText)
).toThrow('Provider must implement BaseAIProvider interface');
});
test('validates generateObject method exists', () => {
const providerWithoutGenerateObject = {
generateText: jest.fn(),
streamText: jest.fn()
};
expect(() =>
registry.registerProvider('invalid', providerWithoutGenerateObject)
).toThrow('Provider must implement BaseAIProvider interface');
});
test('validates methods are functions', () => {
const providerWithNonFunctionMethods = {
generateText: 'not a function',
streamText: jest.fn(),
generateObject: jest.fn()
};
expect(() =>
registry.registerProvider('invalid', providerWithNonFunctionMethods)
).toThrow('Provider must implement BaseAIProvider interface');
});
test('accepts provider with all required methods', () => {
const validProvider = {
generateText: jest.fn(),
streamText: jest.fn(),
generateObject: jest.fn()
};
expect(() =>
registry.registerProvider('valid', validProvider)
).not.toThrow();
});
});
describe('Edge Cases and Error Handling', () => {
test('handles provider registration after reset', () => {
const mockProvider = new MockValidProvider();
registry.registerProvider('mock', mockProvider);
expect(registry.hasProvider('mock')).toBe(true);
registry.reset();
expect(registry.hasProvider('mock')).toBe(false);
registry.registerProvider('mock', mockProvider);
expect(registry.hasProvider('mock')).toBe(true);
});
test('handles multiple registrations and unregistrations', () => {
const provider1 = new MockValidProvider();
const provider2 = new MockValidProvider();
registry.registerProvider('provider1', provider1);
registry.registerProvider('provider2', provider2);
expect(registry.getAllProviders().size).toBe(2);
registry.unregisterProvider('provider1');
expect(registry.hasProvider('provider1')).toBe(false);
expect(registry.hasProvider('provider2')).toBe(true);
registry.unregisterProvider('provider2');
expect(registry.getAllProviders().size).toBe(0);
});
test('maintains provider isolation', () => {
const provider1 = new MockValidProvider();
const provider2 = new MockValidProvider();
registry.registerProvider('provider1', provider1);
registry.registerProvider('provider2', provider2);
const retrieved1 = registry.getProvider('provider1');
const retrieved2 = registry.getProvider('provider2');
expect(retrieved1).toBe(provider1);
expect(retrieved2).toBe(provider2);
expect(retrieved1).not.toBe(retrieved2);
});
});
});