Files
2026-07-13 13:20:22 +08:00

211 lines
7.7 KiB
TypeScript

/**
* @license
* Copyright 2025 AionUi (aionui.com)
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { ClientFactory, normalizeNewApiBaseUrl } from '@/common/api/ClientFactory';
import { OpenAIRotatingClient } from '@/common/api/OpenAIRotatingClient';
import { GeminiRotatingClient } from '@/common/api/GeminiRotatingClient';
import { AnthropicRotatingClient } from '@/common/api/AnthropicRotatingClient';
import { AuthType } from '@office-ai/aioncli-core';
// Mock rotating clients
vi.mock('@/common/api/OpenAIRotatingClient');
vi.mock('@/common/api/GeminiRotatingClient');
vi.mock('@/common/api/AnthropicRotatingClient');
// Mock utility functions
vi.mock('@/common/utils/platformAuthType', () => ({
getProviderAuthType: vi.fn((provider) => provider.auth_type || provider.authType || AuthType.USE_OPENAI),
}));
vi.mock('@/common/utils/platformConstants', () => ({
isNewApiPlatform: vi.fn((platform) => platform === 'new-api'),
}));
describe('ClientFactory', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('normalizeNewApiBaseUrl', () => {
it('adds /v1 suffix for OpenAI', () => {
const result = normalizeNewApiBaseUrl('https://api.example.com', AuthType.USE_OPENAI);
expect(result).toBe('https://api.example.com/v1');
});
it('strips existing /v1 and re-adds for OpenAI', () => {
const result = normalizeNewApiBaseUrl('https://api.example.com/v1', AuthType.USE_OPENAI);
expect(result).toBe('https://api.example.com/v1');
});
it('strips /v1beta and adds /v1 for OpenAI', () => {
const result = normalizeNewApiBaseUrl('https://api.example.com/v1beta', AuthType.USE_OPENAI);
expect(result).toBe('https://api.example.com/v1');
});
it('returns root URL for Anthropic', () => {
const result = normalizeNewApiBaseUrl('https://api.example.com/v1', AuthType.USE_ANTHROPIC);
expect(result).toBe('https://api.example.com');
});
it('returns root URL for Gemini', () => {
const result = normalizeNewApiBaseUrl('https://api.example.com/v1', AuthType.USE_GEMINI);
expect(result).toBe('https://api.example.com');
});
it('removes trailing slashes', () => {
const result = normalizeNewApiBaseUrl('https://api.example.com///', AuthType.USE_OPENAI);
expect(result).toBe('https://api.example.com/v1');
});
it('handles empty base URL', () => {
const result = normalizeNewApiBaseUrl('', AuthType.USE_OPENAI);
expect(result).toBe('');
});
});
describe('createRotatingClient', () => {
const mockProvider = {
id: 'test-provider',
platform: 'openai',
api_key: 'sk-test-key',
base_url: 'https://api.openai.com',
use_model: 'gpt-4',
authType: AuthType.USE_OPENAI,
};
it('creates OpenAIRotatingClient for USE_OPENAI', async () => {
const client = await ClientFactory.createRotatingClient(mockProvider);
expect(OpenAIRotatingClient).toHaveBeenCalled();
expect(client).toBeInstanceOf(OpenAIRotatingClient);
});
it('creates AnthropicRotatingClient for USE_ANTHROPIC', async () => {
const anthropicProvider = {
...mockProvider,
authType: AuthType.USE_ANTHROPIC,
};
const client = await ClientFactory.createRotatingClient(anthropicProvider);
expect(AnthropicRotatingClient).toHaveBeenCalled();
expect(client).toBeInstanceOf(AnthropicRotatingClient);
});
it('creates GeminiRotatingClient for USE_GEMINI', async () => {
const geminiProvider = {
...mockProvider,
platform: 'gemini',
authType: AuthType.USE_GEMINI,
base_url: 'https://generativelanguage.googleapis.com',
use_model: 'gemini-3-pro-image-preview',
};
const client = await ClientFactory.createRotatingClient(geminiProvider);
expect(GeminiRotatingClient).toHaveBeenCalled();
expect(client).toBeInstanceOf(GeminiRotatingClient);
const calls = (GeminiRotatingClient as any).mock.calls;
expect(calls[0][1]).toMatchObject({
model: 'gemini-3-pro-image-preview',
baseURL: 'https://generativelanguage.googleapis.com',
});
expect(calls[0][3]).toBe(AuthType.USE_GEMINI);
});
it('normalizes base URL for new-api platform', async () => {
const newApiProvider = {
...mockProvider,
platform: 'new-api',
base_url: 'https://gateway.example.com/v1',
};
await ClientFactory.createRotatingClient(newApiProvider);
const calls = (OpenAIRotatingClient as any).mock.calls;
expect(calls.length).toBeGreaterThan(0);
// Client should receive normalized URL
const config = calls[0][1];
expect(config.baseURL).toBe('https://gateway.example.com/v1');
});
it('does not normalize base URL for non-new-api platform', async () => {
const standardProvider = {
...mockProvider,
platform: 'openai',
base_url: 'https://api.openai.com/v1beta',
};
await ClientFactory.createRotatingClient(standardProvider);
const calls = (OpenAIRotatingClient as any).mock.calls;
const config = calls[0][1];
expect(config.baseURL).toBe('https://api.openai.com/v1beta');
});
it('passes timeout option to client', async () => {
await ClientFactory.createRotatingClient(mockProvider, { timeout: 30000 });
const calls = (OpenAIRotatingClient as any).mock.calls;
const config = calls[0][1];
expect(config.timeout).toBe(30000);
});
it('passes rotating options to client', async () => {
await ClientFactory.createRotatingClient(mockProvider, {
rotatingOptions: { maxRetries: 5, retryDelay: 2000 },
});
const calls = (OpenAIRotatingClient as any).mock.calls;
const rotatingOpts = calls[0][2];
expect(rotatingOpts.maxRetries).toBe(5);
expect(rotatingOpts.retryDelay).toBe(2000);
});
it('adds default HTTP-Referer and X-Title headers for OpenAI', async () => {
await ClientFactory.createRotatingClient(mockProvider);
const calls = (OpenAIRotatingClient as any).mock.calls;
const config = calls[0][1];
expect(config.defaultHeaders).toEqual({
'HTTP-Referer': 'https://aionui.com',
'X-Title': 'AionUi',
});
});
it('handles proxy option for OpenAI', async () => {
await ClientFactory.createRotatingClient(mockProvider, { proxy: 'http://proxy.example.com:8080' });
const calls = (OpenAIRotatingClient as any).mock.calls;
const config = calls[0][1];
expect(config.httpAgent).toBeDefined();
});
it('passes model to Anthropic config', async () => {
const anthropicProvider = {
...mockProvider,
authType: AuthType.USE_ANTHROPIC,
use_model: 'claude-sonnet-4-20250514',
};
await ClientFactory.createRotatingClient(anthropicProvider);
const calls = (AnthropicRotatingClient as any).mock.calls;
const config = calls[0][1];
expect(config.model).toBe('claude-sonnet-4-20250514');
});
it('passes model to Gemini config', async () => {
const geminiProvider = {
...mockProvider,
authType: AuthType.USE_GEMINI,
use_model: 'gemini-3-pro-image-preview',
};
await ClientFactory.createRotatingClient(geminiProvider);
const calls = (GeminiRotatingClient as any).mock.calls;
const config = calls[0][1];
expect(config.model).toBe('gemini-3-pro-image-preview');
});
it('defaults to OpenAI client for unknown auth type', async () => {
const unknownProvider = {
...mockProvider,
authType: 'UNKNOWN' as AuthType,
};
await ClientFactory.createRotatingClient(unknownProvider);
expect(OpenAIRotatingClient).toHaveBeenCalled();
});
});
});