test: stabilize suite and add coverage heatmap (#373)
* test: stabilize suite and add coverage heatmap * ci: run full bun test suite in pr checks
This commit is contained in:
@@ -40,7 +40,7 @@ export default function TextInput(props: Props): React.ReactNode {
|
||||
// Hoisted to mount-time — this component re-renders on every keystroke.
|
||||
const accessibilityEnabled = useMemo(() => isEnvTruthy(process.env.CLAUDE_CODE_ACCESSIBILITY), []);
|
||||
const settings = useSettings();
|
||||
const reducedMotion = settings.prefersReducedMotion ?? false;
|
||||
const reducedMotion = settings?.prefersReducedMotion ?? false;
|
||||
const voiceState = feature('VOICE_MODE') ?
|
||||
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
||||
useVoiceState(s => s.voiceState) : 'idle' as const;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { describe, expect, test, afterEach } from 'bun:test'
|
||||
import { getRateLimitResetDelayMs, parseOpenAIDuration } from './withRetry.js'
|
||||
import { afterEach, describe, expect, mock, test } from 'bun:test'
|
||||
import { APIError } from '@anthropic-ai/sdk'
|
||||
|
||||
// Helper to build a mock APIError with specific headers
|
||||
@@ -28,42 +27,71 @@ afterEach(() => {
|
||||
if (originalEnv[key] === undefined) delete process.env[key]
|
||||
else process.env[key] = originalEnv[key]
|
||||
}
|
||||
mock.restore()
|
||||
})
|
||||
|
||||
async function importFreshWithRetryModule(
|
||||
provider:
|
||||
| 'firstParty'
|
||||
| 'openai'
|
||||
| 'github'
|
||||
| 'bedrock'
|
||||
| 'vertex'
|
||||
| 'gemini'
|
||||
| 'codex'
|
||||
| 'foundry' = 'firstParty',
|
||||
) {
|
||||
mock.restore()
|
||||
mock.module('src/utils/model/providers.js', () => ({
|
||||
getAPIProvider: () => provider,
|
||||
getAPIProviderForStatsig: () => provider,
|
||||
}))
|
||||
return import(`./withRetry.js?ts=${Date.now()}-${Math.random()}`)
|
||||
}
|
||||
|
||||
// --- parseOpenAIDuration ---
|
||||
describe('parseOpenAIDuration', () => {
|
||||
test('parses seconds: "1s" → 1000', () => {
|
||||
test('parses seconds: "1s" → 1000', async () => {
|
||||
const { parseOpenAIDuration } = await importFreshWithRetryModule()
|
||||
expect(parseOpenAIDuration('1s')).toBe(1000)
|
||||
})
|
||||
|
||||
test('parses minutes+seconds: "6m0s" → 360000', () => {
|
||||
test('parses minutes+seconds: "6m0s" → 360000', async () => {
|
||||
const { parseOpenAIDuration } = await importFreshWithRetryModule()
|
||||
expect(parseOpenAIDuration('6m0s')).toBe(360000)
|
||||
})
|
||||
|
||||
test('parses hours+minutes+seconds: "1h30m0s" → 5400000', () => {
|
||||
test('parses hours+minutes+seconds: "1h30m0s" → 5400000', async () => {
|
||||
const { parseOpenAIDuration } = await importFreshWithRetryModule()
|
||||
expect(parseOpenAIDuration('1h30m0s')).toBe(5400000)
|
||||
})
|
||||
|
||||
test('parses milliseconds: "500ms" → 500', () => {
|
||||
test('parses milliseconds: "500ms" → 500', async () => {
|
||||
const { parseOpenAIDuration } = await importFreshWithRetryModule()
|
||||
expect(parseOpenAIDuration('500ms')).toBe(500)
|
||||
})
|
||||
|
||||
test('parses minutes only: "2m" → 120000', () => {
|
||||
test('parses minutes only: "2m" → 120000', async () => {
|
||||
const { parseOpenAIDuration } = await importFreshWithRetryModule()
|
||||
expect(parseOpenAIDuration('2m')).toBe(120000)
|
||||
})
|
||||
|
||||
test('returns null for empty string', () => {
|
||||
test('returns null for empty string', async () => {
|
||||
const { parseOpenAIDuration } = await importFreshWithRetryModule()
|
||||
expect(parseOpenAIDuration('')).toBeNull()
|
||||
})
|
||||
|
||||
test('returns null for unrecognized format', () => {
|
||||
test('returns null for unrecognized format', async () => {
|
||||
const { parseOpenAIDuration } = await importFreshWithRetryModule()
|
||||
expect(parseOpenAIDuration('invalid')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
// --- getRateLimitResetDelayMs ---
|
||||
describe('getRateLimitResetDelayMs - Anthropic (firstParty)', () => {
|
||||
test('reads anthropic-ratelimit-unified-reset Unix timestamp', () => {
|
||||
test('reads anthropic-ratelimit-unified-reset Unix timestamp', async () => {
|
||||
const { getRateLimitResetDelayMs } =
|
||||
await importFreshWithRetryModule('firstParty')
|
||||
const futureUnixSec = Math.floor(Date.now() / 1000) + 60
|
||||
const error = makeError({
|
||||
'anthropic-ratelimit-unified-reset': String(futureUnixSec),
|
||||
@@ -74,12 +102,16 @@ describe('getRateLimitResetDelayMs - Anthropic (firstParty)', () => {
|
||||
expect(delay!).toBeLessThanOrEqual(60_000)
|
||||
})
|
||||
|
||||
test('returns null when header absent', () => {
|
||||
test('returns null when header absent', async () => {
|
||||
const { getRateLimitResetDelayMs } =
|
||||
await importFreshWithRetryModule('firstParty')
|
||||
const error = makeError({})
|
||||
expect(getRateLimitResetDelayMs(error)).toBeNull()
|
||||
})
|
||||
|
||||
test('returns null when reset is in the past', () => {
|
||||
test('returns null when reset is in the past', async () => {
|
||||
const { getRateLimitResetDelayMs } =
|
||||
await importFreshWithRetryModule('firstParty')
|
||||
const pastUnixSec = Math.floor(Date.now() / 1000) - 10
|
||||
const error = makeError({
|
||||
'anthropic-ratelimit-unified-reset': String(pastUnixSec),
|
||||
@@ -89,15 +121,19 @@ describe('getRateLimitResetDelayMs - Anthropic (firstParty)', () => {
|
||||
})
|
||||
|
||||
describe('getRateLimitResetDelayMs - OpenAI provider', () => {
|
||||
test('reads x-ratelimit-reset-requests duration string', () => {
|
||||
test('reads x-ratelimit-reset-requests duration string', async () => {
|
||||
process.env.CLAUDE_CODE_USE_OPENAI = '1'
|
||||
const { getRateLimitResetDelayMs } =
|
||||
await importFreshWithRetryModule('openai')
|
||||
const error = makeError({ 'x-ratelimit-reset-requests': '30s' })
|
||||
const delay = getRateLimitResetDelayMs(error)
|
||||
expect(delay).toBe(30_000)
|
||||
})
|
||||
|
||||
test('reads x-ratelimit-reset-tokens and picks the larger delay', () => {
|
||||
test('reads x-ratelimit-reset-tokens and picks the larger delay', async () => {
|
||||
process.env.CLAUDE_CODE_USE_OPENAI = '1'
|
||||
const { getRateLimitResetDelayMs } =
|
||||
await importFreshWithRetryModule('openai')
|
||||
const error = makeError({
|
||||
'x-ratelimit-reset-requests': '10s',
|
||||
'x-ratelimit-reset-tokens': '1m0s',
|
||||
@@ -107,29 +143,37 @@ describe('getRateLimitResetDelayMs - OpenAI provider', () => {
|
||||
expect(delay).toBe(60_000)
|
||||
})
|
||||
|
||||
test('returns null when no openai rate limit headers present', () => {
|
||||
test('returns null when no openai rate limit headers present', async () => {
|
||||
process.env.CLAUDE_CODE_USE_OPENAI = '1'
|
||||
const { getRateLimitResetDelayMs } =
|
||||
await importFreshWithRetryModule('openai')
|
||||
const error = makeError({})
|
||||
expect(getRateLimitResetDelayMs(error)).toBeNull()
|
||||
})
|
||||
|
||||
test('works for github provider too', () => {
|
||||
test('works for github provider too', async () => {
|
||||
process.env.CLAUDE_CODE_USE_GITHUB = '1'
|
||||
const { getRateLimitResetDelayMs } =
|
||||
await importFreshWithRetryModule('github')
|
||||
const error = makeError({ 'x-ratelimit-reset-requests': '5s' })
|
||||
expect(getRateLimitResetDelayMs(error)).toBe(5_000)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getRateLimitResetDelayMs - providers without reset headers', () => {
|
||||
test('returns null for bedrock', () => {
|
||||
test('returns null for bedrock', async () => {
|
||||
process.env.CLAUDE_CODE_USE_BEDROCK = '1'
|
||||
const { getRateLimitResetDelayMs } =
|
||||
await importFreshWithRetryModule('bedrock')
|
||||
const error = makeError({ 'anthropic-ratelimit-unified-reset': String(Math.floor(Date.now() / 1000) + 60) })
|
||||
// Bedrock doesn't use this header — should still return null
|
||||
expect(getRateLimitResetDelayMs(error)).toBeNull()
|
||||
})
|
||||
|
||||
test('returns null for vertex', () => {
|
||||
test('returns null for vertex', async () => {
|
||||
process.env.CLAUDE_CODE_USE_VERTEX = '1'
|
||||
const { getRateLimitResetDelayMs } =
|
||||
await importFreshWithRetryModule('vertex')
|
||||
const error = makeError({})
|
||||
expect(getRateLimitResetDelayMs(error)).toBeNull()
|
||||
})
|
||||
|
||||
@@ -4,6 +4,7 @@ import axios from 'axios'
|
||||
const originalEnv = { ...process.env }
|
||||
|
||||
async function importFreshModule() {
|
||||
mock.restore()
|
||||
return import(`./officialRegistry.ts?ts=${Date.now()}-${Math.random()}`)
|
||||
}
|
||||
|
||||
@@ -13,11 +14,15 @@ beforeEach(() => {
|
||||
|
||||
afterEach(() => {
|
||||
process.env = { ...originalEnv }
|
||||
mock.restore()
|
||||
})
|
||||
|
||||
describe('prefetchOfficialMcpUrls', () => {
|
||||
test('does not fetch registry when using OpenAI mode', async () => {
|
||||
process.env.CLAUDE_CODE_USE_OPENAI = '1'
|
||||
mock.module('../../utils/model/providers.js', () => ({
|
||||
getAPIProvider: () => 'openai',
|
||||
}))
|
||||
const getSpy = mock(() => Promise.resolve({ data: { servers: [] } }))
|
||||
axios.get = getSpy as typeof axios.get
|
||||
|
||||
@@ -29,6 +34,9 @@ describe('prefetchOfficialMcpUrls', () => {
|
||||
|
||||
test('does not fetch registry when using Gemini mode', async () => {
|
||||
process.env.CLAUDE_CODE_USE_GEMINI = '1'
|
||||
mock.module('../../utils/model/providers.js', () => ({
|
||||
getAPIProvider: () => 'gemini',
|
||||
}))
|
||||
const getSpy = mock(() => Promise.resolve({ data: { servers: [] } }))
|
||||
axios.get = getSpy as typeof axios.get
|
||||
|
||||
@@ -43,6 +51,9 @@ describe('prefetchOfficialMcpUrls', () => {
|
||||
delete process.env.CLAUDE_CODE_USE_GEMINI
|
||||
delete process.env.CLAUDE_CODE_USE_GITHUB
|
||||
|
||||
mock.module('../../utils/model/providers.js', () => ({
|
||||
getAPIProvider: () => 'firstParty',
|
||||
}))
|
||||
const getSpy = mock(() =>
|
||||
Promise.resolve({
|
||||
data: {
|
||||
|
||||
@@ -4,6 +4,7 @@ import axios from 'axios'
|
||||
const originalEnv = { ...process.env }
|
||||
|
||||
async function importFreshModule() {
|
||||
mock.restore()
|
||||
return import(`./utils.ts?ts=${Date.now()}-${Math.random()}`)
|
||||
}
|
||||
|
||||
@@ -13,11 +14,15 @@ beforeEach(() => {
|
||||
|
||||
afterEach(() => {
|
||||
process.env = { ...originalEnv }
|
||||
mock.restore()
|
||||
})
|
||||
|
||||
describe('checkDomainBlocklist', () => {
|
||||
test('returns allowed without API call in OpenAI mode', async () => {
|
||||
process.env.CLAUDE_CODE_USE_OPENAI = '1'
|
||||
mock.module('../../utils/model/providers.js', () => ({
|
||||
getAPIProvider: () => 'openai',
|
||||
}))
|
||||
const getSpy = mock(() =>
|
||||
Promise.resolve({ status: 200, data: { can_fetch: true } }),
|
||||
)
|
||||
@@ -32,6 +37,9 @@ describe('checkDomainBlocklist', () => {
|
||||
|
||||
test('returns allowed without API call in Gemini mode', async () => {
|
||||
process.env.CLAUDE_CODE_USE_GEMINI = '1'
|
||||
mock.module('../../utils/model/providers.js', () => ({
|
||||
getAPIProvider: () => 'gemini',
|
||||
}))
|
||||
const getSpy = mock(() =>
|
||||
Promise.resolve({ status: 200, data: { can_fetch: true } }),
|
||||
)
|
||||
@@ -49,6 +57,9 @@ describe('checkDomainBlocklist', () => {
|
||||
delete process.env.CLAUDE_CODE_USE_GEMINI
|
||||
delete process.env.CLAUDE_CODE_USE_GITHUB
|
||||
|
||||
mock.module('../../utils/model/providers.js', () => ({
|
||||
getAPIProvider: () => 'firstParty',
|
||||
}))
|
||||
const getSpy = mock(() =>
|
||||
Promise.resolve({ status: 200, data: { can_fetch: true } }),
|
||||
)
|
||||
|
||||
@@ -4,6 +4,7 @@ const originalEnv = { ...process.env }
|
||||
const originalFetch = globalThis.fetch
|
||||
|
||||
async function importFreshModule() {
|
||||
mock.restore()
|
||||
return import(`./apiPreconnect.ts?ts=${Date.now()}-${Math.random()}`)
|
||||
}
|
||||
|
||||
@@ -14,11 +15,15 @@ beforeEach(() => {
|
||||
afterEach(() => {
|
||||
process.env = { ...originalEnv }
|
||||
globalThis.fetch = originalFetch
|
||||
mock.restore()
|
||||
})
|
||||
|
||||
describe('preconnectAnthropicApi', () => {
|
||||
test('does not fetch when OpenAI mode is enabled', async () => {
|
||||
process.env.CLAUDE_CODE_USE_OPENAI = '1'
|
||||
mock.module('./model/providers.js', () => ({
|
||||
getAPIProvider: () => 'openai',
|
||||
}))
|
||||
const fetchMock = mock(() => Promise.resolve(new Response(null, { status: 200 })))
|
||||
globalThis.fetch = fetchMock as typeof globalThis.fetch
|
||||
|
||||
@@ -30,6 +35,9 @@ describe('preconnectAnthropicApi', () => {
|
||||
|
||||
test('does not fetch when Gemini mode is enabled', async () => {
|
||||
process.env.CLAUDE_CODE_USE_GEMINI = '1'
|
||||
mock.module('./model/providers.js', () => ({
|
||||
getAPIProvider: () => 'gemini',
|
||||
}))
|
||||
const fetchMock = mock(() => Promise.resolve(new Response(null, { status: 200 })))
|
||||
globalThis.fetch = fetchMock as typeof globalThis.fetch
|
||||
|
||||
@@ -41,6 +49,9 @@ describe('preconnectAnthropicApi', () => {
|
||||
|
||||
test('does not fetch when GitHub mode is enabled', async () => {
|
||||
process.env.CLAUDE_CODE_USE_GITHUB = '1'
|
||||
mock.module('./model/providers.js', () => ({
|
||||
getAPIProvider: () => 'github',
|
||||
}))
|
||||
const fetchMock = mock(() => Promise.resolve(new Response(null, { status: 200 })))
|
||||
globalThis.fetch = fetchMock as typeof globalThis.fetch
|
||||
|
||||
@@ -58,6 +69,9 @@ describe('preconnectAnthropicApi', () => {
|
||||
delete process.env.CLAUDE_CODE_USE_VERTEX
|
||||
delete process.env.CLAUDE_CODE_USE_FOUNDRY
|
||||
|
||||
mock.module('./model/providers.js', () => ({
|
||||
getAPIProvider: () => 'firstParty',
|
||||
}))
|
||||
const fetchMock = mock(() => Promise.resolve(new Response(null, { status: 200 })))
|
||||
globalThis.fetch = fetchMock as typeof globalThis.fetch
|
||||
|
||||
|
||||
@@ -1,31 +1,60 @@
|
||||
import { afterEach, expect, test } from 'bun:test'
|
||||
import { afterEach, beforeEach, expect, mock, test } from 'bun:test'
|
||||
|
||||
import {
|
||||
clearGeminiAccessToken,
|
||||
readGeminiAccessToken,
|
||||
saveGeminiAccessToken,
|
||||
} from './geminiCredentials.ts'
|
||||
type MockStorageData = Record<string, unknown>
|
||||
|
||||
const originalToken = process.env.GEMINI_ACCESS_TOKEN
|
||||
const originalEnv = { ...process.env }
|
||||
let storageState: MockStorageData = {}
|
||||
|
||||
afterEach(() => {
|
||||
if (originalToken === undefined) {
|
||||
delete process.env.GEMINI_ACCESS_TOKEN
|
||||
} else {
|
||||
process.env.GEMINI_ACCESS_TOKEN = originalToken
|
||||
}
|
||||
clearGeminiAccessToken()
|
||||
async function importFreshModule() {
|
||||
mock.module('./secureStorage/index.js', () => ({
|
||||
getSecureStorage: () => ({
|
||||
name: 'mock-secure-storage',
|
||||
read: () => storageState,
|
||||
readAsync: async () => storageState,
|
||||
update: (next: MockStorageData) => {
|
||||
storageState = next
|
||||
return { success: true }
|
||||
},
|
||||
delete: () => {
|
||||
storageState = {}
|
||||
return true
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
return import(`./geminiCredentials.ts?ts=${Date.now()}-${Math.random()}`)
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
process.env = { ...originalEnv }
|
||||
storageState = {}
|
||||
})
|
||||
|
||||
test('saveGeminiAccessToken stores and reads back the token', () => {
|
||||
afterEach(() => {
|
||||
process.env = { ...originalEnv }
|
||||
storageState = {}
|
||||
mock.restore()
|
||||
})
|
||||
|
||||
test('saveGeminiAccessToken stores and reads back the token', async () => {
|
||||
const {
|
||||
readGeminiAccessToken,
|
||||
saveGeminiAccessToken,
|
||||
} = await importFreshModule()
|
||||
|
||||
const result = saveGeminiAccessToken('token-123')
|
||||
expect(result.success).toBe(true)
|
||||
expect(readGeminiAccessToken()).toBe('token-123')
|
||||
})
|
||||
|
||||
test('clearGeminiAccessToken removes the stored token', () => {
|
||||
test('clearGeminiAccessToken removes the stored token', async () => {
|
||||
const {
|
||||
clearGeminiAccessToken,
|
||||
readGeminiAccessToken,
|
||||
saveGeminiAccessToken,
|
||||
} = await importFreshModule()
|
||||
|
||||
expect(saveGeminiAccessToken('token-123').success).toBe(true)
|
||||
expect(clearGeminiAccessToken().success).toBe(true)
|
||||
expect(readGeminiAccessToken()).toBeUndefined()
|
||||
})
|
||||
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import { afterEach, expect, test } from 'bun:test'
|
||||
|
||||
import {
|
||||
getAPIProvider,
|
||||
usesAnthropicAccountFlow,
|
||||
} from './providers.js'
|
||||
|
||||
const originalEnv = {
|
||||
CLAUDE_CODE_USE_GEMINI: process.env.CLAUDE_CODE_USE_GEMINI,
|
||||
CLAUDE_CODE_USE_GITHUB: process.env.CLAUDE_CODE_USE_GITHUB,
|
||||
@@ -23,6 +18,10 @@ afterEach(() => {
|
||||
process.env.CLAUDE_CODE_USE_FOUNDRY = originalEnv.CLAUDE_CODE_USE_FOUNDRY
|
||||
})
|
||||
|
||||
async function importFreshProvidersModule() {
|
||||
return import(`./providers.js?ts=${Date.now()}-${Math.random()}`)
|
||||
}
|
||||
|
||||
function clearProviderEnv(): void {
|
||||
delete process.env.CLAUDE_CODE_USE_GEMINI
|
||||
delete process.env.CLAUDE_CODE_USE_GITHUB
|
||||
@@ -34,9 +33,12 @@ function clearProviderEnv(): void {
|
||||
|
||||
test('first-party provider keeps Anthropic account setup flow enabled', () => {
|
||||
clearProviderEnv()
|
||||
|
||||
expect(getAPIProvider()).toBe('firstParty')
|
||||
expect(usesAnthropicAccountFlow()).toBe(true)
|
||||
return importFreshProvidersModule().then(
|
||||
({ getAPIProvider, usesAnthropicAccountFlow }) => {
|
||||
expect(getAPIProvider()).toBe('firstParty')
|
||||
expect(usesAnthropicAccountFlow()).toBe(true)
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
test.each([
|
||||
@@ -48,19 +50,22 @@ test.each([
|
||||
['CLAUDE_CODE_USE_FOUNDRY', 'foundry'],
|
||||
] as const)(
|
||||
'%s disables Anthropic account setup flow',
|
||||
(envKey, provider) => {
|
||||
async (envKey, provider) => {
|
||||
clearProviderEnv()
|
||||
process.env[envKey] = '1'
|
||||
const { getAPIProvider, usesAnthropicAccountFlow } =
|
||||
await importFreshProvidersModule()
|
||||
|
||||
expect(getAPIProvider()).toBe(provider)
|
||||
expect(usesAnthropicAccountFlow()).toBe(false)
|
||||
},
|
||||
)
|
||||
|
||||
test('GEMINI takes precedence over GitHub when both are set', () => {
|
||||
test('GEMINI takes precedence over GitHub when both are set', async () => {
|
||||
clearProviderEnv()
|
||||
process.env.CLAUDE_CODE_USE_GEMINI = '1'
|
||||
process.env.CLAUDE_CODE_USE_GITHUB = '1'
|
||||
const { getAPIProvider } = await importFreshProvidersModule()
|
||||
|
||||
expect(getAPIProvider()).toBe('gemini')
|
||||
})
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
import { afterEach, describe, expect, test } from 'bun:test'
|
||||
import { afterEach, describe, expect, mock, test } from 'bun:test'
|
||||
|
||||
import { saveGlobalConfig, type ProviderProfile } from './config.js'
|
||||
import { getAPIProvider } from './model/providers.js'
|
||||
import {
|
||||
applyActiveProviderProfileFromConfig,
|
||||
applyProviderProfileToProcessEnv,
|
||||
deleteProviderProfile,
|
||||
getProviderPresetDefaults,
|
||||
} from './providerProfiles.js'
|
||||
import type { ProviderProfile } from './config.js'
|
||||
|
||||
const originalEnv = { ...process.env }
|
||||
|
||||
@@ -29,6 +22,7 @@ const RESTORED_KEYS = [
|
||||
] as const
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore()
|
||||
for (const key of RESTORED_KEYS) {
|
||||
if (originalEnv[key] === undefined) {
|
||||
delete process.env[key]
|
||||
@@ -36,14 +30,6 @@ afterEach(() => {
|
||||
process.env[key] = originalEnv[key]
|
||||
}
|
||||
}
|
||||
|
||||
saveGlobalConfig(current => ({
|
||||
...current,
|
||||
providerProfiles: [],
|
||||
activeProviderProfileId: undefined,
|
||||
openaiAdditionalModelOptionsCache: [],
|
||||
openaiAdditionalModelOptionsCacheByProfile: {},
|
||||
}))
|
||||
})
|
||||
|
||||
function buildProfile(overrides: Partial<ProviderProfile> = {}): ProviderProfile {
|
||||
@@ -57,10 +43,43 @@ function buildProfile(overrides: Partial<ProviderProfile> = {}): ProviderProfile
|
||||
}
|
||||
}
|
||||
|
||||
async function importFreshProviderModules() {
|
||||
mock.restore()
|
||||
let configState = {
|
||||
providerProfiles: [] as ProviderProfile[],
|
||||
activeProviderProfileId: undefined as string | undefined,
|
||||
openaiAdditionalModelOptionsCache: [] as any[],
|
||||
openaiAdditionalModelOptionsCacheByProfile: {} as Record<string, any[]>,
|
||||
}
|
||||
|
||||
mock.module('./config.js', () => ({
|
||||
getGlobalConfig: () => configState,
|
||||
saveGlobalConfig: (
|
||||
updater: (current: typeof configState) => typeof configState,
|
||||
) => {
|
||||
configState = updater(configState)
|
||||
},
|
||||
}))
|
||||
|
||||
const providerProfiles = await import(
|
||||
`./providerProfiles.js?ts=${Date.now()}-${Math.random()}`
|
||||
)
|
||||
const providers = await import(
|
||||
`./model/providers.js?ts=${Date.now()}-${Math.random()}`
|
||||
)
|
||||
|
||||
return {
|
||||
...providerProfiles,
|
||||
...providers,
|
||||
}
|
||||
}
|
||||
|
||||
describe('applyProviderProfileToProcessEnv', () => {
|
||||
test('openai profile clears competing gemini/github flags', () => {
|
||||
test('openai profile clears competing gemini/github flags', async () => {
|
||||
process.env.CLAUDE_CODE_USE_GEMINI = '1'
|
||||
process.env.CLAUDE_CODE_USE_GITHUB = '1'
|
||||
const { applyProviderProfileToProcessEnv, getAPIProvider } =
|
||||
await importFreshProviderModules()
|
||||
|
||||
applyProviderProfileToProcessEnv(buildProfile())
|
||||
|
||||
@@ -70,9 +89,11 @@ describe('applyProviderProfileToProcessEnv', () => {
|
||||
expect(getAPIProvider()).toBe('openai')
|
||||
})
|
||||
|
||||
test('anthropic profile clears competing gemini/github flags', () => {
|
||||
test('anthropic profile clears competing gemini/github flags', async () => {
|
||||
process.env.CLAUDE_CODE_USE_GEMINI = '1'
|
||||
process.env.CLAUDE_CODE_USE_GITHUB = '1'
|
||||
const { applyProviderProfileToProcessEnv, getAPIProvider } =
|
||||
await importFreshProviderModules()
|
||||
|
||||
applyProviderProfileToProcessEnv(
|
||||
buildProfile({
|
||||
@@ -90,10 +111,12 @@ describe('applyProviderProfileToProcessEnv', () => {
|
||||
})
|
||||
|
||||
describe('applyActiveProviderProfileFromConfig', () => {
|
||||
test('does not override explicit startup provider selection', () => {
|
||||
test('does not override explicit startup provider selection', async () => {
|
||||
process.env.CLAUDE_CODE_USE_OPENAI = '1'
|
||||
process.env.OPENAI_BASE_URL = 'http://localhost:11434/v1'
|
||||
process.env.OPENAI_MODEL = 'qwen2.5:3b'
|
||||
const { applyActiveProviderProfileFromConfig } =
|
||||
await importFreshProviderModules()
|
||||
|
||||
const applied = applyActiveProviderProfileFromConfig({
|
||||
providerProfiles: [
|
||||
@@ -111,11 +134,13 @@ describe('applyActiveProviderProfileFromConfig', () => {
|
||||
expect(process.env.OPENAI_MODEL).toBe('qwen2.5:3b')
|
||||
})
|
||||
|
||||
test('does not override explicit startup selection when profile marker is stale', () => {
|
||||
test('does not override explicit startup selection when profile marker is stale', async () => {
|
||||
process.env.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED = '1'
|
||||
process.env.CLAUDE_CODE_USE_OPENAI = '1'
|
||||
process.env.OPENAI_BASE_URL = 'http://localhost:11434/v1'
|
||||
process.env.OPENAI_MODEL = 'qwen2.5:3b'
|
||||
const { applyActiveProviderProfileFromConfig } =
|
||||
await importFreshProviderModules()
|
||||
|
||||
const applied = applyActiveProviderProfileFromConfig({
|
||||
providerProfiles: [
|
||||
@@ -134,7 +159,7 @@ describe('applyActiveProviderProfileFromConfig', () => {
|
||||
expect(process.env.OPENAI_MODEL).toBe('qwen2.5:3b')
|
||||
})
|
||||
|
||||
test('applies active profile when no explicit provider is selected', () => {
|
||||
test('applies active profile when no explicit provider is selected', async () => {
|
||||
delete process.env.CLAUDE_CODE_USE_OPENAI
|
||||
delete process.env.CLAUDE_CODE_USE_GEMINI
|
||||
delete process.env.CLAUDE_CODE_USE_GITHUB
|
||||
@@ -144,6 +169,8 @@ describe('applyActiveProviderProfileFromConfig', () => {
|
||||
|
||||
process.env.OPENAI_BASE_URL = 'http://localhost:11434/v1'
|
||||
process.env.OPENAI_MODEL = 'qwen2.5:3b'
|
||||
const { applyActiveProviderProfileFromConfig } =
|
||||
await importFreshProviderModules()
|
||||
|
||||
const applied = applyActiveProviderProfileFromConfig({
|
||||
providerProfiles: [
|
||||
@@ -164,8 +191,9 @@ describe('applyActiveProviderProfileFromConfig', () => {
|
||||
})
|
||||
|
||||
describe('getProviderPresetDefaults', () => {
|
||||
test('ollama preset defaults to a local Ollama model', () => {
|
||||
test('ollama preset defaults to a local Ollama model', async () => {
|
||||
delete process.env.OPENAI_MODEL
|
||||
const { getProviderPresetDefaults } = await importFreshProviderModules()
|
||||
|
||||
const defaults = getProviderPresetDefaults('ollama')
|
||||
|
||||
@@ -175,23 +203,23 @@ describe('getProviderPresetDefaults', () => {
|
||||
})
|
||||
|
||||
describe('deleteProviderProfile', () => {
|
||||
test('deleting final profile clears provider env when active profile applied it', () => {
|
||||
applyProviderProfileToProcessEnv(
|
||||
buildProfile({
|
||||
id: 'only_profile',
|
||||
baseUrl: 'https://api.openai.com/v1',
|
||||
model: 'gpt-4o',
|
||||
apiKey: 'sk-test',
|
||||
}),
|
||||
)
|
||||
test('deleting final profile clears provider env when active profile applied it', async () => {
|
||||
const {
|
||||
addProviderProfile,
|
||||
deleteProviderProfile,
|
||||
} =
|
||||
await importFreshProviderModules()
|
||||
const profile = addProviderProfile({
|
||||
name: 'Only Profile',
|
||||
provider: 'openai',
|
||||
baseUrl: 'https://api.openai.com/v1',
|
||||
model: 'gpt-4o',
|
||||
apiKey: 'sk-test',
|
||||
})
|
||||
|
||||
saveGlobalConfig(current => ({
|
||||
...current,
|
||||
providerProfiles: [buildProfile({ id: 'only_profile' })],
|
||||
activeProviderProfileId: 'only_profile',
|
||||
}))
|
||||
expect(profile).not.toBeNull()
|
||||
|
||||
const result = deleteProviderProfile('only_profile')
|
||||
const result = deleteProviderProfile(profile!.id)
|
||||
|
||||
expect(result.removed).toBe(true)
|
||||
expect(result.activeProfileId).toBeUndefined()
|
||||
@@ -215,18 +243,25 @@ describe('deleteProviderProfile', () => {
|
||||
expect(process.env.ANTHROPIC_API_KEY).toBeUndefined()
|
||||
})
|
||||
|
||||
test('deleting final profile preserves explicit startup provider env', () => {
|
||||
test('deleting final profile preserves explicit startup provider env', async () => {
|
||||
const { addProviderProfile, deleteProviderProfile } =
|
||||
await importFreshProviderModules()
|
||||
const profile = addProviderProfile({
|
||||
name: 'Only Profile',
|
||||
provider: 'openai',
|
||||
baseUrl: 'https://api.openai.com/v1',
|
||||
model: 'gpt-4o',
|
||||
})
|
||||
|
||||
expect(profile).not.toBeNull()
|
||||
|
||||
process.env.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED = undefined
|
||||
delete process.env.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED
|
||||
process.env.CLAUDE_CODE_USE_OPENAI = '1'
|
||||
process.env.OPENAI_BASE_URL = 'http://localhost:11434/v1'
|
||||
process.env.OPENAI_MODEL = 'qwen2.5:3b'
|
||||
|
||||
saveGlobalConfig(current => ({
|
||||
...current,
|
||||
providerProfiles: [buildProfile({ id: 'only_profile' })],
|
||||
activeProviderProfileId: 'only_profile',
|
||||
}))
|
||||
|
||||
const result = deleteProviderProfile('only_profile')
|
||||
const result = deleteProviderProfile(profile!.id)
|
||||
|
||||
expect(result.removed).toBe(true)
|
||||
expect(result.activeProfileId).toBeUndefined()
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
|
||||
import { expect, test, mock, describe, beforeEach, afterEach } from "bun:test";
|
||||
import { getSecureStorage } from "./index.js";
|
||||
import { linuxSecretStorage } from "./linuxSecretStorage.js";
|
||||
import { windowsCredentialStorage } from "./windowsCredentialStorage.js";
|
||||
import { getSecureStorageServiceName, CREDENTIALS_SERVICE_SUFFIX } from "./macOsKeychainHelpers.js";
|
||||
@@ -133,24 +132,31 @@ describe("Secure Storage Platform Implementations", () => {
|
||||
describe("Platform Selection", () => {
|
||||
const originalPlatform = process.platform;
|
||||
|
||||
async function importFreshSecureStorage() {
|
||||
return import(`./index.js?ts=${Date.now()}-${Math.random()}`);
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
Object.defineProperty(process, 'platform', { value: originalPlatform });
|
||||
});
|
||||
|
||||
test("darwin returns keychain with fallback", () => {
|
||||
test("darwin returns keychain with fallback", async () => {
|
||||
Object.defineProperty(process, 'platform', { value: 'darwin' });
|
||||
const { getSecureStorage } = await importFreshSecureStorage();
|
||||
const storage = getSecureStorage();
|
||||
expect(storage.name).toContain("keychain");
|
||||
});
|
||||
|
||||
test("linux returns libsecret with fallback", () => {
|
||||
test("linux returns libsecret with fallback", async () => {
|
||||
Object.defineProperty(process, 'platform', { value: 'linux' });
|
||||
const { getSecureStorage } = await importFreshSecureStorage();
|
||||
const storage = getSecureStorage();
|
||||
expect(storage.name).toContain("libsecret");
|
||||
});
|
||||
|
||||
test("win32 returns credential-locker with fallback", () => {
|
||||
test("win32 returns credential-locker with fallback", async () => {
|
||||
Object.defineProperty(process, 'platform', { value: 'win32' });
|
||||
const { getSecureStorage } = await importFreshSecureStorage();
|
||||
const storage = getSecureStorage();
|
||||
expect(storage.name).toContain("credential-locker");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user