* feat: support MCP sampling * support provider registry * use standard config options for MCP provider * update fastmcp to support passing params to requestSampling * move key name definition to base provider * moved check for required api key to provider class * remove unused code * more cleanup * more cleanup * refactor provider * remove not needed files * more cleanup * more cleanup * more cleanup * update docs * fix tests * add tests * format fix * clean files * merge fixes * format fix * feat: add support for MCP Sampling as AI provider * initial mcp ai sdk * fix references to old provider * update models * lint * fix gemini-cli conflicts * ran format * Update src/provider-registry/index.js Co-authored-by: Ralph Khreish <35776126+Crunchyman-ralph@users.noreply.github.com> * fix circular dependency Circular Dependency Issue ✅ FIXED Root Cause: BaseAIProvider was importing from index.js, which includes commands.js and other modules that eventually import back to AI providers Solution: Changed imports to use direct paths to avoid circular dependencies: Updated base-provider.js to import log directly from utils.js Updated gemini-cli.js to import log directly from utils.js Result: Fixed 11 failing tests in mcp-provider.test.js * fix gemini test * fix(claude-code): recover from CLI JSON truncation bug (#913) (#920) Gracefully handle SyntaxError thrown by @anthropic-ai/claude-code when the CLI truncates large JSON outputs (4–16 kB cut-offs).\n\nKey points:\n• Detect JSON parse error + existing buffered text in both doGenerate() and doStream() code paths.\n• Convert the failure into a recoverable 'truncated' finish state and push a provider-warning.\n• Allows Task Master to continue parsing long PRDs / expand-task operations instead of crashing.\n\nA patch changeset (.changeset/claude-code-json-truncation.md) is included for the next release.\n\nRef: eyaltoledano/claude-task-master#913 * docs: fix gemini-cli authentication documentation (#923) Remove erroneous 'gemini auth login' command references and replace with correct 'gemini' command authentication flow. Update documentation to reflect proper OAuth setup process via the gemini CLI interactive interface. * fix tests * fix: update ai-sdk-provider-gemini-cli to 0.0.4 for improved authentication (#932) - Fixed authentication compatibility issues with Google auth - Added support for 'api-key' auth type alongside 'gemini-api-key' - Resolved "Unsupported authType: undefined" runtime errors - Updated @google/gemini-cli-core dependency to 0.1.9 - Improved documentation and removed invalid auth references - Maintained backward compatibility while enhancing type validation * call logging directly Need to patch upstream fastmcp to allow easier access and bootstrap the TM mcp logger to use the fastmcp logger which today is only exposed in the tools handler * fix tests * removing logs until we figure out how to pass mcp logger * format * fix tests * format * clean up * cleanup * readme fix --------- Co-authored-by: Oren Melamed <oren.m@gloat.com> Co-authored-by: Ralph Khreish <35776126+Crunchyman-ralph@users.noreply.github.com> Co-authored-by: Ben Vargas <ben@vargas.com>
418 lines
12 KiB
JavaScript
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);
|
|
});
|
|
});
|
|
});
|