cc-haha/src/services/api/client.test.ts
程序员阿江(Relakkes) bec556679f fix: bypass local provider proxy for loopback targets
Preserve loopback NO_PROXY entries when Electron or manual network settings inject proxy environment variables, and make the Anthropic client skip Bun proxy fetch options for local desktop provider proxy base URLs.

Tested: bun test src/utils/proxy.test.ts src/services/api/client.test.ts src/server/__tests__/network-settings.test.ts src/server/__tests__/conversation-service.test.ts

Tested: cd desktop && bun run test -- electron/services/sidecarManager.test.ts --run

Tested: bun run check:server

Tested: bun run check:native

Not-tested: Windows dev-sidecar real-machine retest for #896; current Mac can only prove loopback proxy bypass behavior and native packaging.

Confidence: medium

Scope-risk: moderate
2026-06-23 15:55:03 +08:00

182 lines
6.7 KiB
TypeScript

import { describe, expect, mock, test } from 'bun:test'
mock.module('src/utils/http.js', () => ({
getAuthHeaders: mock(() => ({})),
getMCPUserAgent: mock(() => 'client-test-agent'),
getUserAgent: mock(() => 'client-test-agent'),
getWebFetchUserAgent: mock(() => 'client-test-agent'),
withOAuth401Retry: mock(async <T>(fn: () => Promise<T>) => fn()),
}))
describe('resolveAnthropicClientApiKey', () => {
test('does not inherit a local api key when a provider auth token is explicit', async () => {
const { resolveAnthropicClientApiKey } = await import('./client.js')
const getFallbackApiKey = mock(() => 'sk-keychain-fallback')
const apiKey = resolveAnthropicClientApiKey({
envAuthToken: 'provider-bearer-token',
envApiKey: undefined,
getFallbackApiKey,
})
expect(apiKey).toBeNull()
expect(getFallbackApiKey).not.toHaveBeenCalled()
})
test('preserves an explicit api key when the caller opts into dual auth', async () => {
const { resolveAnthropicClientApiKey } = await import('./client.js')
const getFallbackApiKey = mock(() => 'sk-keychain-fallback')
const apiKey = resolveAnthropicClientApiKey({
explicitApiKey: 'sk-explicit-api-key',
envAuthToken: 'provider-bearer-token',
getFallbackApiKey,
})
expect(apiKey).toBe('sk-explicit-api-key')
expect(getFallbackApiKey).not.toHaveBeenCalled()
})
test('falls back to the local api key when no provider auth token is present', async () => {
const { resolveAnthropicClientApiKey } = await import('./client.js')
const getFallbackApiKey = mock(() => 'sk-keychain-fallback')
const apiKey = resolveAnthropicClientApiKey({
envAuthToken: undefined,
envApiKey: undefined,
getFallbackApiKey,
})
expect(apiKey).toBe('sk-keychain-fallback')
expect(getFallbackApiKey).toHaveBeenCalled()
})
})
describe('shouldUseOpenAICodexTransport', () => {
test('lets ChatGPT Official marker override a saved Claude subscriber login', async () => {
const { shouldUseOpenAICodexTransport } = await import('./client.js')
expect(shouldUseOpenAICodexTransport({
hasOpenAIAuth: true,
isClaudeSubscriber: true,
forceOpenAICodex: true,
isOpenAIModel: true,
hasAnthropicAuthToken: false,
hasExplicitApiKey: false,
hasFallbackApiKey: false,
})).toBe(true)
})
test('keeps Claude subscriber transport when ChatGPT Official is not selected', async () => {
const { shouldUseOpenAICodexTransport } = await import('./client.js')
expect(shouldUseOpenAICodexTransport({
hasOpenAIAuth: true,
isClaudeSubscriber: true,
forceOpenAICodex: false,
isOpenAIModel: true,
hasAnthropicAuthToken: false,
hasExplicitApiKey: false,
hasFallbackApiKey: false,
})).toBe(false)
})
})
describe('getAnthropicClient', () => {
test('passes bearer-token provider auth without an SDK api key', async () => {
const { getAnthropicClient } = await import('./client.js')
const originalAuthToken = process.env.ANTHROPIC_AUTH_TOKEN
const originalApiKey = process.env.ANTHROPIC_API_KEY
const originalSimple = process.env.CLAUDE_CODE_SIMPLE
process.env.ANTHROPIC_AUTH_TOKEN = 'provider-bearer-token'
process.env.CLAUDE_CODE_SIMPLE = '1'
delete process.env.ANTHROPIC_API_KEY
try {
const client = await getAnthropicClient({
maxRetries: 0,
model: 'claude-sonnet-4-6',
})
expect(client.apiKey).toBeNull()
expect(client._options.defaultHeaders).toMatchObject({
Authorization: 'Bearer provider-bearer-token',
})
} finally {
if (originalAuthToken === undefined) delete process.env.ANTHROPIC_AUTH_TOKEN
else process.env.ANTHROPIC_AUTH_TOKEN = originalAuthToken
if (originalApiKey === undefined) delete process.env.ANTHROPIC_API_KEY
else process.env.ANTHROPIC_API_KEY = originalApiKey
if (originalSimple === undefined) delete process.env.CLAUDE_CODE_SIMPLE
else process.env.CLAUDE_CODE_SIMPLE = originalSimple
}
})
test('bypasses system proxy for local desktop provider proxy base URLs', async () => {
const { getAnthropicClient } = await import('./client.js')
const originalAuthToken = process.env.ANTHROPIC_AUTH_TOKEN
const originalApiKey = process.env.ANTHROPIC_API_KEY
const originalBaseUrl = process.env.ANTHROPIC_BASE_URL
const originalHttpProxy = process.env.HTTP_PROXY
const originalHttpsProxy = process.env.HTTPS_PROXY
const originalNoProxy = process.env.NO_PROXY
const originalLowerHttpProxy = process.env.http_proxy
const originalLowerHttpsProxy = process.env.https_proxy
const originalLowerNoProxy = process.env.no_proxy
const originalSimple = process.env.CLAUDE_CODE_SIMPLE
process.env.ANTHROPIC_AUTH_TOKEN = 'provider-bearer-token'
process.env.ANTHROPIC_BASE_URL = 'http://127.0.0.1:3456/proxy/providers/provider-1'
process.env.HTTP_PROXY = 'http://127.0.0.1:1181'
process.env.HTTPS_PROXY = 'http://127.0.0.1:1181'
process.env.NO_PROXY = 'localhost,127.0.0.1,::1'
process.env.CLAUDE_CODE_SIMPLE = '1'
delete process.env.ANTHROPIC_API_KEY
delete process.env.http_proxy
delete process.env.https_proxy
delete process.env.no_proxy
try {
const client = await getAnthropicClient({
maxRetries: 0,
model: 'deepseek-v4-pro',
})
expect(client._options.fetchOptions?.proxy).toBeUndefined()
} finally {
if (originalAuthToken === undefined) delete process.env.ANTHROPIC_AUTH_TOKEN
else process.env.ANTHROPIC_AUTH_TOKEN = originalAuthToken
if (originalApiKey === undefined) delete process.env.ANTHROPIC_API_KEY
else process.env.ANTHROPIC_API_KEY = originalApiKey
if (originalBaseUrl === undefined) delete process.env.ANTHROPIC_BASE_URL
else process.env.ANTHROPIC_BASE_URL = originalBaseUrl
if (originalHttpProxy === undefined) delete process.env.HTTP_PROXY
else process.env.HTTP_PROXY = originalHttpProxy
if (originalHttpsProxy === undefined) delete process.env.HTTPS_PROXY
else process.env.HTTPS_PROXY = originalHttpsProxy
if (originalNoProxy === undefined) delete process.env.NO_PROXY
else process.env.NO_PROXY = originalNoProxy
if (originalLowerHttpProxy === undefined) delete process.env.http_proxy
else process.env.http_proxy = originalLowerHttpProxy
if (originalLowerHttpsProxy === undefined) delete process.env.https_proxy
else process.env.https_proxy = originalLowerHttpsProxy
if (originalLowerNoProxy === undefined) delete process.env.no_proxy
else process.env.no_proxy = originalLowerNoProxy
if (originalSimple === undefined) delete process.env.CLAUDE_CODE_SIMPLE
else process.env.CLAUDE_CODE_SIMPLE = originalSimple
}
})
})