cc-haha/src/services/api/client.test.ts
程序员阿江(Relakkes) 7713c95621 Prefer ChatGPT OAuth when its provider is selected
Desktop provider choice must override a separate Claude OAuth login. The OpenAI Codex fetch path is explicitly forced by the ChatGPT Official runtime marker, and the Anthropic SDK client no longer carries Claude authToken when that OpenAI transport is selected.

Constraint: Users can have both Claude OAuth and ChatGPT OAuth saved.

Rejected: Rely on isClaudeAISubscriber to choose transport | it makes Claude login override the selected ChatGPT provider.

Confidence: high

Scope-risk: moderate

Tested: bun test src/services/api/client.test.ts

Tested: bun test src/services/openaiAuth/fetch.test.ts src/server/__tests__/conversation-service.test.ts --test-name-pattern "ChatGPT Official|buildOpenAICodexFetch"

Tested: git diff --check
2026-05-18 23:50:31 +08:00

118 lines
4.0 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
}
})
})