134 lines
5.0 KiB
TypeScript
134 lines
5.0 KiB
TypeScript
|
|
import { describe, it, expect, vi } from 'vitest';
|
||
|
|
import { createProviderFromConfig } from '../src/llm-config.js';
|
||
|
|
import type { SecretStore } from '@mcpctl/shared';
|
||
|
|
|
||
|
|
function mockSecretStore(secrets: Record<string, string> = {}): SecretStore {
|
||
|
|
return {
|
||
|
|
get: vi.fn(async (key: string) => secrets[key] ?? null),
|
||
|
|
set: vi.fn(async () => {}),
|
||
|
|
delete: vi.fn(async () => true),
|
||
|
|
backend: () => 'mock',
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
describe('createProviderFromConfig', () => {
|
||
|
|
it('returns empty registry for undefined config', async () => {
|
||
|
|
const store = mockSecretStore();
|
||
|
|
const registry = await createProviderFromConfig(undefined, store);
|
||
|
|
expect(registry.getActive()).toBeNull();
|
||
|
|
expect(registry.list()).toEqual([]);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('returns empty registry for provider=none', async () => {
|
||
|
|
const store = mockSecretStore();
|
||
|
|
const registry = await createProviderFromConfig({ provider: 'none' }, store);
|
||
|
|
expect(registry.getActive()).toBeNull();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('creates gemini-cli provider', async () => {
|
||
|
|
const store = mockSecretStore();
|
||
|
|
const registry = await createProviderFromConfig(
|
||
|
|
{ provider: 'gemini-cli', model: 'gemini-2.5-flash', binaryPath: '/usr/bin/gemini' },
|
||
|
|
store,
|
||
|
|
);
|
||
|
|
expect(registry.getActive()).not.toBeNull();
|
||
|
|
expect(registry.getActive()!.name).toBe('gemini-cli');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('creates ollama provider', async () => {
|
||
|
|
const store = mockSecretStore();
|
||
|
|
const registry = await createProviderFromConfig(
|
||
|
|
{ provider: 'ollama', model: 'llama3.2', url: 'http://localhost:11434' },
|
||
|
|
store,
|
||
|
|
);
|
||
|
|
expect(registry.getActive()!.name).toBe('ollama');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('creates anthropic provider with API key from secret store', async () => {
|
||
|
|
const store = mockSecretStore({ 'anthropic-api-key': 'sk-ant-test' });
|
||
|
|
const registry = await createProviderFromConfig(
|
||
|
|
{ provider: 'anthropic', model: 'claude-haiku-3-5-20241022' },
|
||
|
|
store,
|
||
|
|
);
|
||
|
|
expect(registry.getActive()!.name).toBe('anthropic');
|
||
|
|
expect(store.get).toHaveBeenCalledWith('anthropic-api-key');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('returns empty registry when anthropic API key is missing', async () => {
|
||
|
|
const store = mockSecretStore();
|
||
|
|
const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
||
|
|
const registry = await createProviderFromConfig(
|
||
|
|
{ provider: 'anthropic', model: 'claude-haiku-3-5-20241022' },
|
||
|
|
store,
|
||
|
|
);
|
||
|
|
expect(registry.getActive()).toBeNull();
|
||
|
|
expect(stderrSpy).toHaveBeenCalledWith(expect.stringContaining('Anthropic API key not found'));
|
||
|
|
stderrSpy.mockRestore();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('creates openai provider with API key from secret store', async () => {
|
||
|
|
const store = mockSecretStore({ 'openai-api-key': 'sk-test' });
|
||
|
|
const registry = await createProviderFromConfig(
|
||
|
|
{ provider: 'openai', model: 'gpt-4o', url: 'https://api.openai.com' },
|
||
|
|
store,
|
||
|
|
);
|
||
|
|
expect(registry.getActive()!.name).toBe('openai');
|
||
|
|
expect(store.get).toHaveBeenCalledWith('openai-api-key');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('returns empty registry when openai API key is missing', async () => {
|
||
|
|
const store = mockSecretStore();
|
||
|
|
const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
||
|
|
const registry = await createProviderFromConfig(
|
||
|
|
{ provider: 'openai' },
|
||
|
|
store,
|
||
|
|
);
|
||
|
|
expect(registry.getActive()).toBeNull();
|
||
|
|
stderrSpy.mockRestore();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('creates deepseek provider with API key from secret store', async () => {
|
||
|
|
const store = mockSecretStore({ 'deepseek-api-key': 'sk-ds-test' });
|
||
|
|
const registry = await createProviderFromConfig(
|
||
|
|
{ provider: 'deepseek', model: 'deepseek-chat' },
|
||
|
|
store,
|
||
|
|
);
|
||
|
|
expect(registry.getActive()!.name).toBe('deepseek');
|
||
|
|
expect(store.get).toHaveBeenCalledWith('deepseek-api-key');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('returns empty registry when deepseek API key is missing', async () => {
|
||
|
|
const store = mockSecretStore();
|
||
|
|
const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
||
|
|
const registry = await createProviderFromConfig(
|
||
|
|
{ provider: 'deepseek' },
|
||
|
|
store,
|
||
|
|
);
|
||
|
|
expect(registry.getActive()).toBeNull();
|
||
|
|
stderrSpy.mockRestore();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('creates vllm provider using OpenAI provider', async () => {
|
||
|
|
const store = mockSecretStore();
|
||
|
|
const registry = await createProviderFromConfig(
|
||
|
|
{ provider: 'vllm', model: 'my-model', url: 'http://gpu-server:8000' },
|
||
|
|
store,
|
||
|
|
);
|
||
|
|
// vLLM reuses OpenAI provider under the hood
|
||
|
|
expect(registry.getActive()).not.toBeNull();
|
||
|
|
expect(registry.getActive()!.name).toBe('openai');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('returns empty registry when vllm URL is missing', async () => {
|
||
|
|
const store = mockSecretStore();
|
||
|
|
const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
||
|
|
const registry = await createProviderFromConfig(
|
||
|
|
{ provider: 'vllm' },
|
||
|
|
store,
|
||
|
|
);
|
||
|
|
expect(registry.getActive()).toBeNull();
|
||
|
|
expect(stderrSpy).toHaveBeenCalledWith(expect.stringContaining('vLLM URL not configured'));
|
||
|
|
stderrSpy.mockRestore();
|
||
|
|
});
|
||
|
|
});
|