文件
OmniCard/backend/test/modules/ai/deepseek.test.ts
wikiboss 62e0ae4435 feat: 同步管理端、用户端与后端业务,并接通线上 Web 接口
补齐平台订阅、外部登录、CRM 同步与用户端页面;Nginx 转发用户端接口,登录页使用 Google 与 Apple 标志。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-23 18:01:47 +08:00

378 行
14 KiB
TypeScript

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

import { DeepSeekService } from '../../../src/modules/ai/service/deepseek';
import { AiDeepSeekConfigEntity } from '../../../src/modules/ai/entity/deepseek-config';
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'); // 保留默认值
});
it('1.4 无 repository 时 setTenantConfig 仅写内存 Map 兜底', async () => {
expect((service as any).deepSeekConfigEntity).toBeUndefined();
await service.setTenantConfig(42, {
apiKey: 'sk-map-only-fallback-key',
model: 'deepseek-reasoner',
baseUrl: 'https://api.deepseek.com',
});
expect(service.isConfigured(42)).toBe(true);
expect(service.getTenantConfig(42).apiKey).toBe('sk-map-only-fallback-key');
expect(service.getTenantConfig(42).model).toBe('deepseek-reasoner');
// 不污染平台公共池
expect(service.isConfigured()).toBe(false);
});
it('1.5 有 mock repository 时 setTenantConfig 落库且 resolve 优先读库', async () => {
const store: AiDeepSeekConfigEntity[] = [];
const mockRepo = {
find: jest.fn(async () => [...store]),
findOne: jest.fn(async ({ where }: { where: { tenantId: number } }) => {
return store.find((r) => r.tenantId === where.tenantId) || null;
}),
save: jest.fn(async (row: AiDeepSeekConfigEntity) => {
const idx = store.findIndex((r) => r.tenantId === row.tenantId);
if (idx >= 0) {
store[idx] = row;
} else {
store.push(row);
}
return row;
}),
};
(service as any).deepSeekConfigEntity = mockRepo;
await service.setTenantConfig(1001, {
apiKey: 'sk-persisted-tenant-key',
model: 'deepseek-chat',
baseUrl: 'https://api.deepseek.com',
});
expect(mockRepo.save).toHaveBeenCalled();
expect(store).toHaveLength(1);
expect(store[0].tenantId).toBe(1001);
expect(store[0].apiKey).toBe('sk-persisted-tenant-key');
expect(store[0].enabled).toBe(true);
// 清空内存 Map,验证 resolveTenantConfig 从 DB 读回
(service as any).tenantConfigs.clear();
const fromDb = await service.resolveTenantConfig(1001);
expect(fromDb.apiKey).toBe('sk-persisted-tenant-key');
expect(service.getTenantConfig(1001).apiKey).toBe('sk-persisted-tenant-key');
});
it('1.6 DB 抛错时静默回退内存 Map,不中断业务', async () => {
const mockRepo = {
find: jest.fn(async () => {
throw new Error('db down');
}),
findOne: jest.fn(async () => {
throw new Error('db down');
}),
save: jest.fn(async () => {
throw new Error('db down');
}),
};
(service as any).deepSeekConfigEntity = mockRepo;
await expect(
service.setTenantConfig(9, { apiKey: 'sk-survive-db-outage' })
).resolves.toMatchObject({ apiKey: 'sk-survive-db-outage' });
expect(service.getTenantConfig(9).apiKey).toBe('sk-survive-db-outage');
});
});
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');
});
});
describe('5. Token 用量仅进程内存,不得冒充账单/落库 (R29)', () => {
/** 内存用量标记:storage:'memory' 或 persisted:false 均可 */
function assertMemoryOnlyUsage(result: {
storage?: string;
persisted?: boolean;
totalTokens: number;
promptTokens: number;
completionTokens: number;
callCount: number;
}) {
const markedMemory =
result.storage === 'memory' || result.persisted === false;
expect(markedMemory).toBe(true);
// 不得伪装成已落库账单用量
expect(result.storage).not.toBe('db');
expect(result.storage).not.toBe('persisted');
expect(result.persisted).not.toBe(true);
}
it('5.1 无任何记录时 getTenantTokenUsage 返回全 0,并标明内存存储', () => {
const usage = service.getTenantTokenUsage(9001);
expect(usage.totalTokens).toBe(0);
expect(usage.promptTokens).toBe(0);
expect(usage.completionTokens).toBe(0);
expect(usage.callCount).toBe(0);
assertMemoryOnlyUsage(usage);
// 禁止造示例/占位用量
expect(usage.totalTokens).not.toBeGreaterThan(0);
});
it('5.2 recordTokenUsage 返回值须带 storage:memory(或 persisted:false)', () => {
const recorded = service.recordTokenUsage(9002, {
promptTokens: 10,
completionTokens: 5,
totalTokens: 15,
});
expect(recorded.totalTokens).toBe(15);
expect(recorded.promptTokens).toBe(10);
expect(recorded.completionTokens).toBe(5);
expect(recorded.callCount).toBe(1);
assertMemoryOnlyUsage(recorded);
});
it('5.3 累计后 getTenantTokenUsage 仍标明内存,数值仅反映进程内账本', () => {
service.recordTokenUsage(9003, { totalTokens: 100 });
service.recordTokenUsage(9003, {
promptTokens: 20,
completionTokens: 30,
totalTokens: 50,
});
const usage = service.getTenantTokenUsage(9003);
expect(usage.totalTokens).toBe(150);
expect(usage.promptTokens).toBe(20);
expect(usage.completionTokens).toBe(30);
expect(usage.callCount).toBe(2);
assertMemoryOnlyUsage(usage);
});
it('5.4 不同租户用量隔离;未写入租户仍为 0 且标明内存', () => {
service.recordTokenUsage(9004, { totalTokens: 77 });
const other = service.getTenantTokenUsage(9005);
expect(other.totalTokens).toBe(0);
expect(other.callCount).toBe(0);
assertMemoryOnlyUsage(other);
assertMemoryOnlyUsage(service.getTenantTokenUsage(9004));
});
});
});