mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-18 13:23:33 +08:00
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
182 lines
6.7 KiB
TypeScript
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
|
|
}
|
|
})
|
|
})
|