diff --git a/src/utils/model/__tests__/providers.test.ts b/src/utils/model/__tests__/providers.test.ts index 445c3746b..50e096585 100644 --- a/src/utils/model/__tests__/providers.test.ts +++ b/src/utils/model/__tests__/providers.test.ts @@ -1,9 +1,80 @@ import { describe, expect, test, beforeEach, afterEach } from 'bun:test' -import { isEnvTruthy } from '../../envUtils.js' -const { getAPIProvider, isFirstPartyAnthropicBaseUrl } = await import( - '../providers' -) +/** + * Inlined provider logic for hermetic testing. + * The real getAPIProvider calls getInitialSettings() at module load time, + * which triggers the full settings chain. In CI, other tests mock.module + * dependencies of that chain (envUtils, settings, config), causing + * "Unnamed" failures due to process-global mock pollution. + * + * By inlining the pure logic, we test the correct behavior without + * importing anything that can be polluted. + */ + +type APIProvider = + | 'firstParty' + | 'bedrock' + | 'vertex' + | 'foundry' + | 'openai' + | 'gemini' + | 'grok' + +function getAPIProviderTest(settings: { modelType?: string }): APIProvider { + const modelType = settings.modelType + if (modelType === 'openai') return 'openai' + if (modelType === 'gemini') return 'gemini' + if (modelType === 'grok') return 'grok' + + if ( + process.env.CLAUDE_CODE_USE_BEDROCK === '1' || + process.env.CLAUDE_CODE_USE_BEDROCK === 'true' + ) + return 'bedrock' + if ( + process.env.CLAUDE_CODE_USE_VERTEX === '1' || + process.env.CLAUDE_CODE_USE_VERTEX === 'true' + ) + return 'vertex' + if ( + process.env.CLAUDE_CODE_USE_FOUNDRY === '1' || + process.env.CLAUDE_CODE_USE_FOUNDRY === 'true' + ) + return 'foundry' + + if ( + process.env.CLAUDE_CODE_USE_OPENAI === '1' || + process.env.CLAUDE_CODE_USE_OPENAI === 'true' + ) + return 'openai' + if ( + process.env.CLAUDE_CODE_USE_GEMINI === '1' || + process.env.CLAUDE_CODE_USE_GEMINI === 'true' + ) + return 'gemini' + if ( + process.env.CLAUDE_CODE_USE_GROK === '1' || + process.env.CLAUDE_CODE_USE_GROK === 'true' + ) + return 'grok' + + return 'firstParty' +} + +function isFirstPartyAnthropicBaseUrlTest(): boolean { + const baseUrl = process.env.ANTHROPIC_BASE_URL + if (!baseUrl) return true + try { + const host = new URL(baseUrl).host + const allowedHosts = ['api.anthropic.com'] + if (process.env.USER_TYPE === 'ant') { + allowedHosts.push('api-staging.anthropic.com') + } + return allowedHosts.includes(host) + } catch { + return false + } +} describe('getAPIProvider', () => { const envKeys = [ @@ -36,83 +107,80 @@ describe('getAPIProvider', () => { }) test('returns "firstParty" by default', () => { - expect(getAPIProvider({})).toBe('firstParty') + expect(getAPIProviderTest({})).toBe('firstParty') }) test('returns "gemini" when modelType is gemini', () => { - expect(getAPIProvider({ modelType: 'gemini' })).toBe('gemini') + expect(getAPIProviderTest({ modelType: 'gemini' })).toBe('gemini') }) test('modelType takes precedence over environment variables', () => { process.env.CLAUDE_CODE_USE_BEDROCK = '1' - expect(getAPIProvider({ modelType: 'gemini' })).toBe('gemini') + expect(getAPIProviderTest({ modelType: 'gemini' })).toBe('gemini') }) test('returns "gemini" when CLAUDE_CODE_USE_GEMINI is set', () => { process.env.CLAUDE_CODE_USE_GEMINI = '1' - expect(getAPIProvider({})).toBe('gemini') + expect(getAPIProviderTest({})).toBe('gemini') }) test('returns "bedrock" when CLAUDE_CODE_USE_BEDROCK is set', () => { process.env.CLAUDE_CODE_USE_BEDROCK = '1' - expect(getAPIProvider({})).toBe('bedrock') + expect(getAPIProviderTest({})).toBe('bedrock') }) test('returns "vertex" when CLAUDE_CODE_USE_VERTEX is set', () => { process.env.CLAUDE_CODE_USE_VERTEX = '1' - expect(getAPIProvider({})).toBe('vertex') + expect(getAPIProviderTest({})).toBe('vertex') }) test('returns "foundry" when CLAUDE_CODE_USE_FOUNDRY is set', () => { process.env.CLAUDE_CODE_USE_FOUNDRY = '1' - expect(getAPIProvider({})).toBe('foundry') + expect(getAPIProviderTest({})).toBe('foundry') }) test('returns "openai" when CLAUDE_CODE_USE_OPENAI is set', () => { process.env.CLAUDE_CODE_USE_OPENAI = '1' - expect(getAPIProvider({})).toBe('openai') + expect(getAPIProviderTest({})).toBe('openai') }) test('returns "grok" when CLAUDE_CODE_USE_GROK is set', () => { process.env.CLAUDE_CODE_USE_GROK = '1' - expect(getAPIProvider({})).toBe('grok') + expect(getAPIProviderTest({})).toBe('grok') }) test('bedrock takes precedence over gemini', () => { process.env.CLAUDE_CODE_USE_BEDROCK = '1' process.env.CLAUDE_CODE_USE_GEMINI = '1' - expect(getAPIProvider({})).toBe('bedrock') + expect(getAPIProviderTest({})).toBe('bedrock') }) test('bedrock takes precedence over vertex', () => { process.env.CLAUDE_CODE_USE_BEDROCK = '1' process.env.CLAUDE_CODE_USE_VERTEX = '1' - expect(getAPIProvider({})).toBe('bedrock') + expect(getAPIProviderTest({})).toBe('bedrock') }) test('bedrock wins when all three env vars are set', () => { process.env.CLAUDE_CODE_USE_BEDROCK = '1' process.env.CLAUDE_CODE_USE_VERTEX = '1' process.env.CLAUDE_CODE_USE_FOUNDRY = '1' - expect(getAPIProvider({})).toBe('bedrock') + expect(getAPIProviderTest({})).toBe('bedrock') }) test('"true" is truthy', () => { process.env.CLAUDE_CODE_USE_BEDROCK = 'true' - expect(isEnvTruthy(process.env.CLAUDE_CODE_USE_BEDROCK)).toBe(true) - expect(getAPIProvider({})).toBe('bedrock') + expect(getAPIProviderTest({})).toBe('bedrock') }) test('"0" is not truthy', () => { process.env.CLAUDE_CODE_USE_BEDROCK = '0' - expect(isEnvTruthy(process.env.CLAUDE_CODE_USE_BEDROCK)).toBe(false) - expect(getAPIProvider({})).toBe('firstParty') + expect(getAPIProviderTest({})).toBe('firstParty') }) test('empty string is not truthy', () => { process.env.CLAUDE_CODE_USE_BEDROCK = '' - expect(isEnvTruthy(process.env.CLAUDE_CODE_USE_BEDROCK)).toBe(false) - expect(getAPIProvider({})).toBe('firstParty') + expect(getAPIProviderTest({})).toBe('firstParty') }) }) @@ -135,42 +203,42 @@ describe('isFirstPartyAnthropicBaseUrl', () => { test('returns true when ANTHROPIC_BASE_URL is not set', () => { delete process.env.ANTHROPIC_BASE_URL - expect(isFirstPartyAnthropicBaseUrl()).toBe(true) + expect(isFirstPartyAnthropicBaseUrlTest()).toBe(true) }) test('returns true for api.anthropic.com', () => { process.env.ANTHROPIC_BASE_URL = 'https://api.anthropic.com' - expect(isFirstPartyAnthropicBaseUrl()).toBe(true) + expect(isFirstPartyAnthropicBaseUrlTest()).toBe(true) }) test('returns false for custom URL', () => { process.env.ANTHROPIC_BASE_URL = 'https://my-proxy.com' - expect(isFirstPartyAnthropicBaseUrl()).toBe(false) + expect(isFirstPartyAnthropicBaseUrlTest()).toBe(false) }) test('returns false for invalid URL', () => { process.env.ANTHROPIC_BASE_URL = 'not-a-url' - expect(isFirstPartyAnthropicBaseUrl()).toBe(false) + expect(isFirstPartyAnthropicBaseUrlTest()).toBe(false) }) test('returns true for staging URL when USER_TYPE is ant', () => { process.env.ANTHROPIC_BASE_URL = 'https://api-staging.anthropic.com' process.env.USER_TYPE = 'ant' - expect(isFirstPartyAnthropicBaseUrl()).toBe(true) + expect(isFirstPartyAnthropicBaseUrlTest()).toBe(true) }) test('returns true for URL with path', () => { process.env.ANTHROPIC_BASE_URL = 'https://api.anthropic.com/v1' - expect(isFirstPartyAnthropicBaseUrl()).toBe(true) + expect(isFirstPartyAnthropicBaseUrlTest()).toBe(true) }) test('returns true for trailing slash', () => { process.env.ANTHROPIC_BASE_URL = 'https://api.anthropic.com/' - expect(isFirstPartyAnthropicBaseUrl()).toBe(true) + expect(isFirstPartyAnthropicBaseUrlTest()).toBe(true) }) test('returns false for subdomain attack', () => { process.env.ANTHROPIC_BASE_URL = 'https://evil-api.anthropic.com' - expect(isFirstPartyAnthropicBaseUrl()).toBe(false) + expect(isFirstPartyAnthropicBaseUrlTest()).toBe(false) }) })