provider-config.test.ts 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. import { describe, expect, it, vi, beforeEach } from 'vitest';
  2. // Mock fs — only intercept server-providers.yml; delegate everything else to real fs.
  3. // This prevents YAML config from leaking host-machine state into tests while keeping
  4. // the mock scoped to what provider-config actually reads.
  5. let yamlOverride: string | null = null;
  6. vi.mock('fs', async (importOriginal) => {
  7. const actual = await importOriginal<typeof import('fs')>();
  8. const isYaml = (p: unknown) => typeof p === 'string' && p.endsWith('server-providers.yml');
  9. return {
  10. ...actual,
  11. default: {
  12. ...actual,
  13. existsSync: (p: string) => (isYaml(p) ? yamlOverride !== null : actual.existsSync(p)),
  14. readFileSync: (p: string, ...args: unknown[]) =>
  15. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  16. isYaml(p) ? (yamlOverride ?? '') : (actual.readFileSync as any)(p, ...args),
  17. },
  18. existsSync: (p: string) => (isYaml(p) ? yamlOverride !== null : actual.existsSync(p)),
  19. readFileSync: (p: string, ...args: unknown[]) =>
  20. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  21. isYaml(p) ? (yamlOverride ?? '') : (actual.readFileSync as any)(p, ...args),
  22. };
  23. });
  24. describe('provider-config', () => {
  25. beforeEach(() => {
  26. vi.resetModules();
  27. vi.unstubAllEnvs();
  28. yamlOverride = null;
  29. });
  30. describe('resolveApiKey', () => {
  31. it('returns client key when provided', async () => {
  32. const { resolveApiKey } = await import('@/lib/server/provider-config');
  33. expect(resolveApiKey('openai', 'sk-client')).toBe('sk-client');
  34. });
  35. it('returns server key from env when no client key', async () => {
  36. vi.stubEnv('OPENAI_API_KEY', 'sk-server');
  37. const { resolveApiKey } = await import('@/lib/server/provider-config');
  38. expect(resolveApiKey('openai')).toBe('sk-server');
  39. });
  40. it('returns empty string when neither client nor server key exists', async () => {
  41. const { resolveApiKey } = await import('@/lib/server/provider-config');
  42. expect(resolveApiKey('openai')).toBe('');
  43. });
  44. it('prefers client key over server key', async () => {
  45. vi.stubEnv('OPENAI_API_KEY', 'sk-server');
  46. const { resolveApiKey } = await import('@/lib/server/provider-config');
  47. expect(resolveApiKey('openai', 'sk-client')).toBe('sk-client');
  48. });
  49. it('resolves non-OpenAI providers via their env prefix', async () => {
  50. vi.stubEnv('ANTHROPIC_API_KEY', 'sk-anthropic');
  51. const { resolveApiKey } = await import('@/lib/server/provider-config');
  52. expect(resolveApiKey('anthropic')).toBe('sk-anthropic');
  53. });
  54. it('returns empty string for unknown provider with no env var', async () => {
  55. const { resolveApiKey } = await import('@/lib/server/provider-config');
  56. expect(resolveApiKey('nonexistent-provider')).toBe('');
  57. });
  58. });
  59. describe('resolveBaseUrl', () => {
  60. it('returns client URL when provided', async () => {
  61. const { resolveBaseUrl } = await import('@/lib/server/provider-config');
  62. expect(resolveBaseUrl('openai', 'https://custom.api.com')).toBe('https://custom.api.com');
  63. });
  64. it('returns server URL from env when no client URL', async () => {
  65. vi.stubEnv('OPENAI_API_KEY', 'sk-test');
  66. vi.stubEnv('OPENAI_BASE_URL', 'https://proxy.example.com/v1');
  67. const { resolveBaseUrl } = await import('@/lib/server/provider-config');
  68. expect(resolveBaseUrl('openai')).toBe('https://proxy.example.com/v1');
  69. });
  70. it('returns undefined when neither client nor server URL exists', async () => {
  71. const { resolveBaseUrl } = await import('@/lib/server/provider-config');
  72. expect(resolveBaseUrl('openai')).toBeUndefined();
  73. });
  74. });
  75. describe('resolveProxy', () => {
  76. it('returns undefined when no proxy configured', async () => {
  77. const { resolveProxy } = await import('@/lib/server/provider-config');
  78. expect(resolveProxy('openai')).toBeUndefined();
  79. });
  80. it('returns proxy URL from YAML config', async () => {
  81. yamlOverride = `
  82. providers:
  83. openai:
  84. apiKey: sk-yaml
  85. proxy: http://proxy.internal:8080
  86. `;
  87. const { resolveProxy } = await import('@/lib/server/provider-config');
  88. expect(resolveProxy('openai')).toBe('http://proxy.internal:8080');
  89. });
  90. });
  91. describe('getServerProviders', () => {
  92. it('returns empty object when no providers configured', async () => {
  93. const { getServerProviders } = await import('@/lib/server/provider-config');
  94. expect(getServerProviders()).toEqual({});
  95. });
  96. it('returns provider metadata without API keys', async () => {
  97. vi.stubEnv('OPENAI_API_KEY', 'sk-secret');
  98. vi.stubEnv('OPENAI_BASE_URL', 'https://proxy.com/v1');
  99. vi.stubEnv('OPENAI_MODELS', 'gpt-4o,gpt-4o-mini');
  100. const { getServerProviders } = await import('@/lib/server/provider-config');
  101. const providers = getServerProviders();
  102. expect(providers.openai).toBeDefined();
  103. expect(providers.openai.models).toEqual(['gpt-4o', 'gpt-4o-mini']);
  104. expect(providers.openai.baseUrl).toBe('https://proxy.com/v1');
  105. // API key must NOT be exposed
  106. expect((providers.openai as Record<string, unknown>).apiKey).toBeUndefined();
  107. });
  108. it('lists multiple providers', async () => {
  109. vi.stubEnv('OPENAI_API_KEY', 'sk-openai');
  110. vi.stubEnv('ANTHROPIC_API_KEY', 'sk-anthropic');
  111. const { getServerProviders } = await import('@/lib/server/provider-config');
  112. const providers = getServerProviders();
  113. expect(Object.keys(providers)).toContain('openai');
  114. expect(Object.keys(providers)).toContain('anthropic');
  115. });
  116. it('omits providers without API key', async () => {
  117. vi.stubEnv('OPENAI_BASE_URL', 'https://proxy.com/v1');
  118. // No OPENAI_API_KEY set
  119. const { getServerProviders } = await import('@/lib/server/provider-config');
  120. const providers = getServerProviders();
  121. expect(providers.openai).toBeUndefined();
  122. });
  123. });
  124. describe('env var model parsing', () => {
  125. it('splits comma-separated models and trims whitespace', async () => {
  126. vi.stubEnv('OPENAI_API_KEY', 'sk-test');
  127. vi.stubEnv('OPENAI_MODELS', ' gpt-4o , gpt-4o-mini , ');
  128. const { getServerProviders } = await import('@/lib/server/provider-config');
  129. const providers = getServerProviders();
  130. expect(providers.openai.models).toEqual(['gpt-4o', 'gpt-4o-mini']);
  131. });
  132. });
  133. describe('resolveWebSearchApiKey', () => {
  134. it('returns client key first', async () => {
  135. const { resolveWebSearchApiKey } = await import('@/lib/server/provider-config');
  136. expect(resolveWebSearchApiKey('client-key')).toBe('client-key');
  137. });
  138. it('falls back to TAVILY_API_KEY env var', async () => {
  139. vi.stubEnv('TAVILY_API_KEY', 'tvly-bare-env');
  140. const { resolveWebSearchApiKey } = await import('@/lib/server/provider-config');
  141. expect(resolveWebSearchApiKey()).toBe('tvly-bare-env');
  142. });
  143. });
  144. describe('baseUrl-only providers (e.g. mineru)', () => {
  145. it('includes PDF provider from YAML when only baseUrl is configured (no apiKey)', async () => {
  146. yamlOverride = `
  147. pdf:
  148. mineru:
  149. baseUrl: http://localhost:8888
  150. `;
  151. const { getServerPDFProviders } = await import('@/lib/server/provider-config');
  152. const providers = getServerPDFProviders();
  153. expect(providers.mineru).toBeDefined();
  154. expect(providers.mineru.baseUrl).toBe('http://localhost:8888');
  155. });
  156. it('includes provider from env when only BASE_URL is set (no API_KEY)', async () => {
  157. vi.stubEnv('PDF_MINERU_BASE_URL', 'http://localhost:8888');
  158. const { getServerPDFProviders } = await import('@/lib/server/provider-config');
  159. const providers = getServerPDFProviders();
  160. expect(providers.mineru).toBeDefined();
  161. expect(providers.mineru.baseUrl).toBe('http://localhost:8888');
  162. });
  163. it('excludes PDF provider when only apiKey is configured (no baseUrl)', async () => {
  164. yamlOverride = `
  165. pdf:
  166. mineru:
  167. apiKey: sk-fake
  168. `;
  169. const { getServerPDFProviders } = await import('@/lib/server/provider-config');
  170. const providers = getServerPDFProviders();
  171. expect(providers.mineru).toBeUndefined();
  172. });
  173. });
  174. });