Files
gongxue-base/apps/server/src/ai-chat/ai-model-stream.service.spec.ts

69 lines
2.3 KiB
TypeScript

import { AiModelStreamService } from './ai-model-stream.service';
import type { AiRuntimeConfig } from '../ai-config/dto/ai-config.dto';
const config: AiRuntimeConfig = {
provider: 'DEEPSEEK' as AiRuntimeConfig['provider'],
baseUrl: 'https://example.test/v1',
apiKey: 'secret',
defaultModel: 'deepseek-reasoner',
timeoutMs: 1000,
enabled: true,
};
describe('AiModelStreamService', () => {
afterEach(() => jest.restoreAllMocks());
it('分离思考、正文并拼接分片工具调用,且处理无尾随空行的最后事件', async () => {
const chunks = [
'data: {"choices":[{"delta":{"reasoning_content":"思考"}}]}\n\n',
'data: {"choices":[{"delta":{"content":"答案","tool_calls":[{"index":0,"id":"call_","function":{"name":"search_","arguments":"{\\"q\\":"}}]}}]}\n\n',
'data: {"choices":[{"delta":{"tool_calls":[{"index":0,"id":"1","function":{"name":"students","arguments":"\\"张三\\"}"}}]}}]}',
];
async function* body() {
for (const chunk of chunks) yield Buffer.from(chunk);
}
const service = new AiModelStreamService();
jest.spyOn(service as never, 'pinnedPost' as never).mockResolvedValue({
status: 200,
contentType: 'text/event-stream',
body: body(),
} as never);
const events = [];
for await (const event of service.stream(
config,
[{ role: 'user', content: '查询' }],
[],
new AbortController().signal,
)) events.push(event);
expect(events).toEqual([
{ type: 'reasoning', delta: '思考' },
{ type: 'content', delta: '答案' },
{
type: 'complete',
toolCalls: [{ id: 'call_1', name: 'search_students', arguments: '{"q":"张三"}' }],
},
]);
});
it('不向调用方暴露上游非 JSON 错误正文', async () => {
async function* body() { yield Buffer.from('proxy internal detail'); }
const service = new AiModelStreamService();
jest.spyOn(service as never, 'pinnedPost' as never).mockResolvedValue({
status: 502,
contentType: 'text/plain',
body: body(),
} as never);
const consume = async () => {
for await (const _ of service.stream(
config,
[{ role: 'user', content: '查询' }],
[],
new AbortController().signal,
)) void _;
};
await expect(consume()).rejects.toThrow('AI 服务暂时不可用');
});
});