234 行
8.3 KiB
TypeScript
234 行
8.3 KiB
TypeScript
import { DeepSeekService } from '../../../src/modules/ai/service/deepseek';
|
|
import { DeepSeekConfig } from '../../../src/comm/types/omni';
|
|
|
|
describe('T31 DeepSeek 大模型集成与适配服务 (deepseek.ts)', () => {
|
|
let service: DeepSeekService;
|
|
|
|
beforeEach(() => {
|
|
// 先清理环境变量,防止跨测试用例污染
|
|
delete process.env.DEEPSEEK_API_KEY;
|
|
delete process.env.DEEPSEEK_BASE_URL;
|
|
delete process.env.DEEPSEEK_MODEL;
|
|
service = new DeepSeekService();
|
|
});
|
|
|
|
describe('1. 配置管理与状态检测 (Config & Readiness)', () => {
|
|
it('1.1 初始状态默认配置验证', () => {
|
|
const config = service.getConfig();
|
|
expect(config.baseUrl).toBe('https://api.deepseek.com');
|
|
expect(config.model).toBe('deepseek-chat');
|
|
expect(config.temperature).toBe(0.7);
|
|
expect(config.timeoutMs).toBe(30000);
|
|
expect(service.isConfigured()).toBe(false);
|
|
});
|
|
|
|
it('1.2 优先从环境变量读取 DEEPSEEK_API_KEY', () => {
|
|
process.env.DEEPSEEK_API_KEY = 'sk-env-test-key-123';
|
|
process.env.DEEPSEEK_BASE_URL = 'https://custom.deepseek.com';
|
|
process.env.DEEPSEEK_MODEL = 'deepseek-reasoner';
|
|
|
|
const envService = new DeepSeekService();
|
|
expect(envService.isConfigured()).toBe(true);
|
|
expect(envService.getConfig().apiKey).toBe('sk-env-test-key-123');
|
|
expect(envService.getConfig().baseUrl).toBe('https://custom.deepseek.com');
|
|
expect(envService.getConfig().model).toBe('deepseek-reasoner');
|
|
});
|
|
|
|
it('1.3 updateConfig 动态更新与合并配置', () => {
|
|
service.updateConfig({
|
|
apiKey: 'sk-dynamic-key-456',
|
|
model: 'deepseek-reasoner',
|
|
temperature: 0.2,
|
|
});
|
|
|
|
expect(service.isConfigured()).toBe(true);
|
|
const cfg = service.getConfig();
|
|
expect(cfg.apiKey).toBe('sk-dynamic-key-456');
|
|
expect(cfg.model).toBe('deepseek-reasoner');
|
|
expect(cfg.temperature).toBe(0.2);
|
|
expect(cfg.baseUrl).toBe('https://api.deepseek.com'); // 保留默认值
|
|
});
|
|
});
|
|
|
|
describe('2. 提示词动态拼装 (Prompt Builders)', () => {
|
|
it('2.1 buildSystemPrompt 组装名片主、人设与知识库切片', () => {
|
|
const card = {
|
|
name: '张总',
|
|
company: '华强出海科技',
|
|
title: '海外商务总监',
|
|
};
|
|
const concierge = {
|
|
personaType: 'trade_expert' as const,
|
|
toneStyle: 'professional' as const,
|
|
promptTemplate: '你是一名资深出海贸易专家,精通中东与东南亚市场。',
|
|
};
|
|
const chunks = [
|
|
{ index: 1, content: '支持信用证结算与 FOB 迪拜港条款。' },
|
|
{ index: 2, content: '最小起订量 (MOQ) 为 500 台。' },
|
|
];
|
|
|
|
const systemPrompt = service.buildSystemPrompt(card, concierge, chunks);
|
|
|
|
expect(systemPrompt).toContain('张总');
|
|
expect(systemPrompt).toContain('华强出海科技');
|
|
expect(systemPrompt).toContain('海外商务总监');
|
|
expect(systemPrompt).toContain('你是一名资深出海贸易专家');
|
|
expect(systemPrompt).toContain('支持信用证结算与 FOB 迪拜港条款。');
|
|
expect(systemPrompt).toContain('最小起订量 (MOQ) 为 500 台。');
|
|
});
|
|
|
|
it('2.2 buildLocalizePrompt 组装多语言润色与术语库指令', () => {
|
|
const prompt = service.buildLocalizePrompt(
|
|
'欢迎参观我们位于迪拜的展位',
|
|
'en-US',
|
|
'professional',
|
|
['展位 -> Booth', '迪拜 -> Dubai']
|
|
);
|
|
|
|
expect(prompt).toContain('欢迎参观我们位于迪拜的展位');
|
|
expect(prompt).toContain('en-US');
|
|
expect(prompt).toContain('professional');
|
|
expect(prompt).toContain('Booth');
|
|
expect(prompt).toContain('Dubai');
|
|
});
|
|
|
|
it('2.3 buildCopilotPrompt 组装销售破冰首语生成指令', () => {
|
|
const lead = {
|
|
name: 'Ahmed',
|
|
company: 'Gulf Energy',
|
|
phone: '+971501234567',
|
|
demand: '储能集装箱报价',
|
|
};
|
|
const tracks = [
|
|
{ module: 'case_energy', duration: 180 },
|
|
{ module: 'price_table', duration: 90 },
|
|
];
|
|
|
|
const prompt = service.buildCopilotPrompt(lead, tracks);
|
|
|
|
expect(prompt).toContain('Ahmed');
|
|
expect(prompt).toContain('Gulf Energy');
|
|
expect(prompt).toContain('储能集装箱报价');
|
|
expect(prompt).toContain('case_energy');
|
|
});
|
|
});
|
|
|
|
describe('3. 文本补全调用与高可用降级 (chatCompletion & Fallback)', () => {
|
|
it('3.1 未配置 API Key 时,提供 fallbackReply 则安全返回兜底内容', async () => {
|
|
expect(service.isConfigured()).toBe(false);
|
|
|
|
const result = await service.chatCompletion(
|
|
{
|
|
messages: [{ role: 'user', content: '你们的产品价格是多少?' }],
|
|
},
|
|
'这是本地规则引擎的兜底回复'
|
|
);
|
|
|
|
expect(result).toBe('这是本地规则引擎的兜底回复');
|
|
});
|
|
|
|
it('3.2 未配置 API Key 且未提供 fallbackReply 时安全降级或抛出清晰提示', async () => {
|
|
await expect(
|
|
service.chatCompletion({
|
|
messages: [{ role: 'user', content: '你好' }],
|
|
})
|
|
).rejects.toThrow('DeepSeek API Key 未配置');
|
|
});
|
|
|
|
it('3.3 模拟正常调用 DeepSeek 官方接口返回生成文本', async () => {
|
|
service.updateConfig({ apiKey: 'sk-mock-valid-key' });
|
|
|
|
// 模拟底层 HTTP client
|
|
const mockPost = jest.fn().mockResolvedValue({
|
|
status: 200,
|
|
data: {
|
|
id: 'chatcmpl-test-123',
|
|
model: 'deepseek-chat',
|
|
choices: [
|
|
{
|
|
index: 0,
|
|
message: {
|
|
role: 'assistant',
|
|
content: '您好!我是华强科技的 AI 顾问,很高兴为您服务。',
|
|
},
|
|
finish_reason: 'stop',
|
|
},
|
|
],
|
|
},
|
|
});
|
|
(service as any).httpClient = { post: mockPost };
|
|
|
|
const reply = await service.chatCompletion({
|
|
messages: [{ role: 'user', content: '您好' }],
|
|
});
|
|
|
|
expect(reply).toBe('您好!我是华强科技的 AI 顾问,很高兴为您服务。');
|
|
expect(mockPost).toHaveBeenCalledWith(
|
|
'https://api.deepseek.com/chat/completions',
|
|
expect.objectContaining({
|
|
model: 'deepseek-chat',
|
|
messages: [{ role: 'user', content: '您好' }],
|
|
}),
|
|
expect.objectContaining({
|
|
headers: expect.objectContaining({
|
|
Authorization: 'Bearer sk-mock-valid-key',
|
|
}),
|
|
})
|
|
);
|
|
});
|
|
|
|
it('3.4 API 调用异常且提供 fallbackReply 时平滑降级', async () => {
|
|
service.updateConfig({ apiKey: 'sk-mock-valid-key' });
|
|
const mockPost = jest.fn().mockRejectedValue(new Error('Network Timeout'));
|
|
(service as any).httpClient = { post: mockPost };
|
|
|
|
const reply = await service.chatCompletion(
|
|
{ messages: [{ role: 'user', content: '报价' }] },
|
|
'网络异常时的平滑降级回复'
|
|
);
|
|
|
|
expect(reply).toBe('网络异常时的平滑降级回复');
|
|
});
|
|
});
|
|
|
|
describe('4. 连通性测试 (testConnection)', () => {
|
|
it('4.1 未配置 API Key 且未传入临时 Key 时测试失败', async () => {
|
|
const res = await service.testConnection();
|
|
expect(res.success).toBe(false);
|
|
expect(res.message).toContain('API Key 未提供');
|
|
});
|
|
|
|
it('4.2 传入测试 Key 成功握手并统计延迟', async () => {
|
|
const mockPost = jest.fn().mockResolvedValue({
|
|
status: 200,
|
|
data: {
|
|
choices: [
|
|
{ message: { content: 'pong' }, finish_reason: 'stop' },
|
|
],
|
|
},
|
|
});
|
|
(service as any).httpClient = { post: mockPost };
|
|
|
|
const res = await service.testConnection('sk-test-tmp-key');
|
|
expect(res.success).toBe(true);
|
|
expect(typeof res.latencyMs).toBe('number');
|
|
expect(res.latencyMs).toBeGreaterThanOrEqual(0);
|
|
expect(res.model).toBe('deepseek-chat');
|
|
});
|
|
|
|
it('4.3 官方 API 返回 401 鉴权失败捕获', async () => {
|
|
const mockPost = jest.fn().mockRejectedValue({
|
|
response: {
|
|
status: 401,
|
|
data: { error: { message: 'Authentication Fails (invalid api key)' } },
|
|
},
|
|
});
|
|
(service as any).httpClient = { post: mockPost };
|
|
|
|
const res = await service.testConnection('sk-invalid-key');
|
|
expect(res.success).toBe(false);
|
|
expect(res.message).toContain('Authentication Fails');
|
|
});
|
|
});
|
|
});
|