310 lines
8.9 KiB
TypeScript
310 lines
8.9 KiB
TypeScript
// @vitest-environment node
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
const getCachedShellEnvMock = vi.fn<() => NodeJS.ProcessEnv | null>();
|
|
|
|
vi.mock('@main/utils/shellEnv', () => ({
|
|
getCachedShellEnv: () => getCachedShellEnvMock(),
|
|
}));
|
|
|
|
describe('ProviderConnectionService', () => {
|
|
const originalOpenAiApiKey = process.env.OPENAI_API_KEY;
|
|
const originalCodexApiKey = process.env.CODEX_API_KEY;
|
|
|
|
function createConfig(authMode: 'auto' | 'oauth' | 'api_key' = 'auto') {
|
|
return {
|
|
providerConnections: {
|
|
anthropic: {
|
|
authMode,
|
|
},
|
|
codex: {
|
|
apiKeyBetaEnabled: false,
|
|
authMode: 'oauth' as const,
|
|
},
|
|
},
|
|
runtime: {
|
|
providerBackends: {
|
|
gemini: 'auto' as const,
|
|
codex: 'codex-native' as const,
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
beforeEach(() => {
|
|
vi.resetModules();
|
|
vi.clearAllMocks();
|
|
getCachedShellEnvMock.mockReturnValue({});
|
|
delete process.env.OPENAI_API_KEY;
|
|
delete process.env.CODEX_API_KEY;
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (originalOpenAiApiKey === undefined) {
|
|
delete process.env.OPENAI_API_KEY;
|
|
} else {
|
|
process.env.OPENAI_API_KEY = originalOpenAiApiKey;
|
|
}
|
|
|
|
if (originalCodexApiKey === undefined) {
|
|
delete process.env.CODEX_API_KEY;
|
|
} else {
|
|
process.env.CODEX_API_KEY = originalCodexApiKey;
|
|
}
|
|
});
|
|
|
|
it('removes Anthropic environment credentials when OAuth mode is selected', async () => {
|
|
const { ProviderConnectionService } =
|
|
await import('@main/services/runtime/ProviderConnectionService');
|
|
|
|
const service = new ProviderConnectionService(
|
|
{
|
|
lookupPreferred: vi.fn().mockResolvedValue(null),
|
|
} as never,
|
|
{
|
|
getConfig: () => createConfig('oauth'),
|
|
} as never
|
|
);
|
|
|
|
const result = await service.applyConfiguredConnectionEnv(
|
|
{
|
|
ANTHROPIC_API_KEY: 'direct-key',
|
|
ANTHROPIC_AUTH_TOKEN: 'proxy-token',
|
|
},
|
|
'anthropic'
|
|
);
|
|
|
|
expect(result.ANTHROPIC_API_KEY).toBeUndefined();
|
|
expect(result.ANTHROPIC_AUTH_TOKEN).toBeUndefined();
|
|
});
|
|
|
|
it('injects the stored Anthropic API key when api_key mode is selected', async () => {
|
|
const lookupPreferred = vi.fn().mockResolvedValue({
|
|
envVarName: 'ANTHROPIC_API_KEY',
|
|
value: 'stored-key',
|
|
});
|
|
const { ProviderConnectionService } =
|
|
await import('@main/services/runtime/ProviderConnectionService');
|
|
|
|
const service = new ProviderConnectionService(
|
|
{
|
|
lookupPreferred,
|
|
} as never,
|
|
{
|
|
getConfig: () => createConfig('api_key'),
|
|
} as never
|
|
);
|
|
|
|
const result = await service.applyConfiguredConnectionEnv(
|
|
{
|
|
ANTHROPIC_API_KEY: undefined,
|
|
ANTHROPIC_AUTH_TOKEN: 'proxy-token',
|
|
},
|
|
'anthropic'
|
|
);
|
|
|
|
expect(lookupPreferred).toHaveBeenCalledWith('ANTHROPIC_API_KEY');
|
|
expect(result.ANTHROPIC_API_KEY).toBe('stored-key');
|
|
expect(result.ANTHROPIC_AUTH_TOKEN).toBeUndefined();
|
|
});
|
|
|
|
it('reports a missing Anthropic API key when api_key mode is selected', async () => {
|
|
const { ProviderConnectionService } =
|
|
await import('@main/services/runtime/ProviderConnectionService');
|
|
|
|
const service = new ProviderConnectionService(
|
|
{
|
|
lookupPreferred: vi.fn().mockResolvedValue(null),
|
|
} as never,
|
|
{
|
|
getConfig: () => createConfig('api_key'),
|
|
} as never
|
|
);
|
|
|
|
const issue = await service.getConfiguredConnectionIssue({}, 'anthropic');
|
|
|
|
expect(issue).toContain('Anthropic API key mode is enabled');
|
|
expect(issue).toContain('ANTHROPIC_API_KEY');
|
|
});
|
|
|
|
it('prefers stored API key status over environment detection for Anthropic', async () => {
|
|
getCachedShellEnvMock.mockReturnValue({
|
|
ANTHROPIC_API_KEY: 'shell-key',
|
|
});
|
|
|
|
const { ProviderConnectionService } =
|
|
await import('@main/services/runtime/ProviderConnectionService');
|
|
|
|
const service = new ProviderConnectionService(
|
|
{
|
|
lookupPreferred: vi.fn().mockResolvedValue({
|
|
envVarName: 'ANTHROPIC_API_KEY',
|
|
value: 'stored-key',
|
|
}),
|
|
} as never,
|
|
{
|
|
getConfig: () => createConfig('auto'),
|
|
} as never
|
|
);
|
|
|
|
const info = await service.getConnectionInfo('anthropic');
|
|
|
|
expect(info).toMatchObject({
|
|
supportsOAuth: true,
|
|
supportsApiKey: true,
|
|
configuredAuthMode: 'auto',
|
|
apiKeyConfigured: true,
|
|
apiKeySource: 'stored',
|
|
apiKeySourceLabel: 'Stored in app',
|
|
});
|
|
});
|
|
|
|
it('exposes Codex as native-only API-key runtime', async () => {
|
|
const { ProviderConnectionService } =
|
|
await import('@main/services/runtime/ProviderConnectionService');
|
|
|
|
const service = new ProviderConnectionService(
|
|
{
|
|
lookupPreferred: vi.fn().mockResolvedValue(null),
|
|
} as never,
|
|
{
|
|
getConfig: () => createConfig('auto'),
|
|
} as never
|
|
);
|
|
|
|
const info = await service.getConnectionInfo('codex');
|
|
|
|
expect(info).toMatchObject({
|
|
supportsOAuth: false,
|
|
supportsApiKey: true,
|
|
configurableAuthModes: [],
|
|
configuredAuthMode: null,
|
|
apiKeyConfigured: false,
|
|
apiKeySource: null,
|
|
apiKeySourceLabel: null,
|
|
});
|
|
expect(info.apiKeyBetaAvailable).toBeUndefined();
|
|
expect(info.apiKeyBetaEnabled).toBeUndefined();
|
|
});
|
|
|
|
it('mirrors a stored OpenAI key into CODEX_API_KEY for native Codex launches', async () => {
|
|
const lookupPreferred = vi.fn().mockResolvedValue({
|
|
envVarName: 'OPENAI_API_KEY',
|
|
value: 'openai-stored-key',
|
|
});
|
|
const { ProviderConnectionService } =
|
|
await import('@main/services/runtime/ProviderConnectionService');
|
|
|
|
const service = new ProviderConnectionService(
|
|
{
|
|
lookupPreferred,
|
|
} as never,
|
|
{
|
|
getConfig: () => createConfig('auto'),
|
|
} as never
|
|
);
|
|
|
|
const result = await service.applyConfiguredConnectionEnv({}, 'codex');
|
|
|
|
expect(lookupPreferred).toHaveBeenCalledWith('OPENAI_API_KEY');
|
|
expect(result.OPENAI_API_KEY).toBe('openai-stored-key');
|
|
expect(result.CODEX_API_KEY).toBe('openai-stored-key');
|
|
});
|
|
|
|
it('keeps ambient OpenAI credentials for native Codex launches', async () => {
|
|
const { ProviderConnectionService } =
|
|
await import('@main/services/runtime/ProviderConnectionService');
|
|
|
|
const service = new ProviderConnectionService(
|
|
{
|
|
lookupPreferred: vi.fn().mockResolvedValue(null),
|
|
} as never,
|
|
{
|
|
getConfig: () => createConfig('auto'),
|
|
} as never
|
|
);
|
|
|
|
const result = await service.applyConfiguredConnectionEnv(
|
|
{
|
|
OPENAI_API_KEY: 'shell-openai-key',
|
|
},
|
|
'codex'
|
|
);
|
|
|
|
expect(result.OPENAI_API_KEY).toBe('shell-openai-key');
|
|
expect(result.CODEX_API_KEY).toBe('shell-openai-key');
|
|
});
|
|
|
|
it('accepts CODEX_API_KEY as the native external credential source for Codex', async () => {
|
|
getCachedShellEnvMock.mockReturnValue({
|
|
CODEX_API_KEY: 'native-key',
|
|
});
|
|
|
|
const { ProviderConnectionService } =
|
|
await import('@main/services/runtime/ProviderConnectionService');
|
|
|
|
const service = new ProviderConnectionService(
|
|
{
|
|
lookupPreferred: vi.fn().mockResolvedValue(null),
|
|
} as never,
|
|
{
|
|
getConfig: () => createConfig('auto'),
|
|
} as never
|
|
);
|
|
|
|
const info = await service.getConnectionInfo('codex');
|
|
const issue = await service.getConfiguredConnectionIssue(
|
|
{
|
|
CODEX_API_KEY: 'native-key',
|
|
},
|
|
'codex'
|
|
);
|
|
|
|
expect(info.apiKeyConfigured).toBe(true);
|
|
expect(info.apiKeySource).toBe('environment');
|
|
expect(info.apiKeySourceLabel).toBe('Detected from CODEX_API_KEY');
|
|
expect(issue).toBeNull();
|
|
});
|
|
|
|
it('reports a missing native Codex credential when neither OPENAI_API_KEY nor CODEX_API_KEY exist', async () => {
|
|
const { ProviderConnectionService } =
|
|
await import('@main/services/runtime/ProviderConnectionService');
|
|
|
|
const service = new ProviderConnectionService(
|
|
{
|
|
lookupPreferred: vi.fn().mockResolvedValue(null),
|
|
} as never,
|
|
{
|
|
getConfig: () => createConfig('auto'),
|
|
} as never
|
|
);
|
|
|
|
const issue = await service.getConfiguredConnectionIssue({}, 'codex');
|
|
|
|
expect(issue).toContain('Codex native requires OPENAI_API_KEY or CODEX_API_KEY');
|
|
});
|
|
|
|
it('augments PTY env for native Codex without dropping existing OpenAI credentials', async () => {
|
|
const { ProviderConnectionService } =
|
|
await import('@main/services/runtime/ProviderConnectionService');
|
|
|
|
const service = new ProviderConnectionService(
|
|
{
|
|
lookupPreferred: vi.fn().mockResolvedValue(null),
|
|
} as never,
|
|
{
|
|
getConfig: () => createConfig('auto'),
|
|
} as never
|
|
);
|
|
|
|
const result = await service.augmentConfiguredConnectionEnv(
|
|
{
|
|
OPENAI_API_KEY: 'shell-key',
|
|
},
|
|
'codex'
|
|
);
|
|
|
|
expect(result.OPENAI_API_KEY).toBe('shell-key');
|
|
expect(result.CODEX_API_KEY).toBe('shell-key');
|
|
});
|
|
});
|