| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205 |
- import { describe, expect, it, vi, beforeEach } from 'vitest';
- // Mock fs — only intercept server-providers.yml; delegate everything else to real fs.
- // This prevents YAML config from leaking host-machine state into tests while keeping
- // the mock scoped to what provider-config actually reads.
- let yamlOverride: string | null = null;
- vi.mock('fs', async (importOriginal) => {
- const actual = await importOriginal<typeof import('fs')>();
- const isYaml = (p: unknown) => typeof p === 'string' && p.endsWith('server-providers.yml');
- return {
- ...actual,
- default: {
- ...actual,
- existsSync: (p: string) => (isYaml(p) ? yamlOverride !== null : actual.existsSync(p)),
- readFileSync: (p: string, ...args: unknown[]) =>
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- isYaml(p) ? (yamlOverride ?? '') : (actual.readFileSync as any)(p, ...args),
- },
- existsSync: (p: string) => (isYaml(p) ? yamlOverride !== null : actual.existsSync(p)),
- readFileSync: (p: string, ...args: unknown[]) =>
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- isYaml(p) ? (yamlOverride ?? '') : (actual.readFileSync as any)(p, ...args),
- };
- });
- describe('provider-config', () => {
- beforeEach(() => {
- vi.resetModules();
- vi.unstubAllEnvs();
- yamlOverride = null;
- });
- describe('resolveApiKey', () => {
- it('returns client key when provided', async () => {
- const { resolveApiKey } = await import('@/lib/server/provider-config');
- expect(resolveApiKey('openai', 'sk-client')).toBe('sk-client');
- });
- it('returns server key from env when no client key', async () => {
- vi.stubEnv('OPENAI_API_KEY', 'sk-server');
- const { resolveApiKey } = await import('@/lib/server/provider-config');
- expect(resolveApiKey('openai')).toBe('sk-server');
- });
- it('returns empty string when neither client nor server key exists', async () => {
- const { resolveApiKey } = await import('@/lib/server/provider-config');
- expect(resolveApiKey('openai')).toBe('');
- });
- it('prefers client key over server key', async () => {
- vi.stubEnv('OPENAI_API_KEY', 'sk-server');
- const { resolveApiKey } = await import('@/lib/server/provider-config');
- expect(resolveApiKey('openai', 'sk-client')).toBe('sk-client');
- });
- it('resolves non-OpenAI providers via their env prefix', async () => {
- vi.stubEnv('ANTHROPIC_API_KEY', 'sk-anthropic');
- const { resolveApiKey } = await import('@/lib/server/provider-config');
- expect(resolveApiKey('anthropic')).toBe('sk-anthropic');
- });
- it('returns empty string for unknown provider with no env var', async () => {
- const { resolveApiKey } = await import('@/lib/server/provider-config');
- expect(resolveApiKey('nonexistent-provider')).toBe('');
- });
- });
- describe('resolveBaseUrl', () => {
- it('returns client URL when provided', async () => {
- const { resolveBaseUrl } = await import('@/lib/server/provider-config');
- expect(resolveBaseUrl('openai', 'https://custom.api.com')).toBe('https://custom.api.com');
- });
- it('returns server URL from env when no client URL', async () => {
- vi.stubEnv('OPENAI_API_KEY', 'sk-test');
- vi.stubEnv('OPENAI_BASE_URL', 'https://proxy.example.com/v1');
- const { resolveBaseUrl } = await import('@/lib/server/provider-config');
- expect(resolveBaseUrl('openai')).toBe('https://proxy.example.com/v1');
- });
- it('returns undefined when neither client nor server URL exists', async () => {
- const { resolveBaseUrl } = await import('@/lib/server/provider-config');
- expect(resolveBaseUrl('openai')).toBeUndefined();
- });
- });
- describe('resolveProxy', () => {
- it('returns undefined when no proxy configured', async () => {
- const { resolveProxy } = await import('@/lib/server/provider-config');
- expect(resolveProxy('openai')).toBeUndefined();
- });
- it('returns proxy URL from YAML config', async () => {
- yamlOverride = `
- providers:
- openai:
- apiKey: sk-yaml
- proxy: http://proxy.internal:8080
- `;
- const { resolveProxy } = await import('@/lib/server/provider-config');
- expect(resolveProxy('openai')).toBe('http://proxy.internal:8080');
- });
- });
- describe('getServerProviders', () => {
- it('returns empty object when no providers configured', async () => {
- const { getServerProviders } = await import('@/lib/server/provider-config');
- expect(getServerProviders()).toEqual({});
- });
- it('returns provider metadata without API keys', async () => {
- vi.stubEnv('OPENAI_API_KEY', 'sk-secret');
- vi.stubEnv('OPENAI_BASE_URL', 'https://proxy.com/v1');
- vi.stubEnv('OPENAI_MODELS', 'gpt-4o,gpt-4o-mini');
- const { getServerProviders } = await import('@/lib/server/provider-config');
- const providers = getServerProviders();
- expect(providers.openai).toBeDefined();
- expect(providers.openai.models).toEqual(['gpt-4o', 'gpt-4o-mini']);
- expect(providers.openai.baseUrl).toBe('https://proxy.com/v1');
- // API key must NOT be exposed
- expect((providers.openai as Record<string, unknown>).apiKey).toBeUndefined();
- });
- it('lists multiple providers', async () => {
- vi.stubEnv('OPENAI_API_KEY', 'sk-openai');
- vi.stubEnv('ANTHROPIC_API_KEY', 'sk-anthropic');
- const { getServerProviders } = await import('@/lib/server/provider-config');
- const providers = getServerProviders();
- expect(Object.keys(providers)).toContain('openai');
- expect(Object.keys(providers)).toContain('anthropic');
- });
- it('omits providers without API key', async () => {
- vi.stubEnv('OPENAI_BASE_URL', 'https://proxy.com/v1');
- // No OPENAI_API_KEY set
- const { getServerProviders } = await import('@/lib/server/provider-config');
- const providers = getServerProviders();
- expect(providers.openai).toBeUndefined();
- });
- });
- describe('env var model parsing', () => {
- it('splits comma-separated models and trims whitespace', async () => {
- vi.stubEnv('OPENAI_API_KEY', 'sk-test');
- vi.stubEnv('OPENAI_MODELS', ' gpt-4o , gpt-4o-mini , ');
- const { getServerProviders } = await import('@/lib/server/provider-config');
- const providers = getServerProviders();
- expect(providers.openai.models).toEqual(['gpt-4o', 'gpt-4o-mini']);
- });
- });
- describe('resolveWebSearchApiKey', () => {
- it('returns client key first', async () => {
- const { resolveWebSearchApiKey } = await import('@/lib/server/provider-config');
- expect(resolveWebSearchApiKey('client-key')).toBe('client-key');
- });
- it('falls back to TAVILY_API_KEY env var', async () => {
- vi.stubEnv('TAVILY_API_KEY', 'tvly-bare-env');
- const { resolveWebSearchApiKey } = await import('@/lib/server/provider-config');
- expect(resolveWebSearchApiKey()).toBe('tvly-bare-env');
- });
- });
- describe('baseUrl-only providers (e.g. mineru)', () => {
- it('includes PDF provider from YAML when only baseUrl is configured (no apiKey)', async () => {
- yamlOverride = `
- pdf:
- mineru:
- baseUrl: http://localhost:8888
- `;
- const { getServerPDFProviders } = await import('@/lib/server/provider-config');
- const providers = getServerPDFProviders();
- expect(providers.mineru).toBeDefined();
- expect(providers.mineru.baseUrl).toBe('http://localhost:8888');
- });
- it('includes provider from env when only BASE_URL is set (no API_KEY)', async () => {
- vi.stubEnv('PDF_MINERU_BASE_URL', 'http://localhost:8888');
- const { getServerPDFProviders } = await import('@/lib/server/provider-config');
- const providers = getServerPDFProviders();
- expect(providers.mineru).toBeDefined();
- expect(providers.mineru.baseUrl).toBe('http://localhost:8888');
- });
- it('excludes PDF provider when only apiKey is configured (no baseUrl)', async () => {
- yamlOverride = `
- pdf:
- mineru:
- apiKey: sk-fake
- `;
- const { getServerPDFProviders } = await import('@/lib/server/provider-config');
- const providers = getServerPDFProviders();
- expect(providers.mineru).toBeUndefined();
- });
- });
- });
|