130 lines
3.8 KiB
TypeScript
130 lines
3.8 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { parseSsePayload, reduceAiSseMessage } from './provider';
|
|
|
|
describe('AI chat SSE message reducer', () => {
|
|
it('separates reasoning and answer deltas', () => {
|
|
let message = reduceAiSseMessage(undefined, {
|
|
event: 'reasoning.delta',
|
|
data: JSON.stringify({ messageId: 8, delta: '分析' }),
|
|
});
|
|
message = reduceAiSseMessage(message, {
|
|
event: 'content.delta',
|
|
data: JSON.stringify({ delta: '**答案**' }),
|
|
});
|
|
|
|
expect(message.reasoningContent).toBe('分析');
|
|
expect(message.content).toBe('**答案**');
|
|
});
|
|
|
|
it('tracks tool lifecycle without exposing raw payloads', () => {
|
|
let message = reduceAiSseMessage(undefined, {
|
|
event: 'tool.started',
|
|
data: JSON.stringify({
|
|
toolCallId: 'call-1',
|
|
toolName: 'search_students',
|
|
summary: '姓名条件',
|
|
}),
|
|
});
|
|
message = reduceAiSseMessage(message, {
|
|
event: 'tool.completed',
|
|
data: JSON.stringify({
|
|
toolCallId: 'call-1',
|
|
toolName: 'search_students',
|
|
status: 'success',
|
|
summary: '找到 1 条记录',
|
|
}),
|
|
});
|
|
|
|
expect(message.toolRuns).toHaveLength(1);
|
|
expect(message.toolRuns[0]).toMatchObject({ status: 'success', summary: '找到 1 条记录' });
|
|
});
|
|
|
|
it('tracks processed attachments and final feedback state', () => {
|
|
let message = reduceAiSseMessage(undefined, {
|
|
event: 'attachment.processed',
|
|
data: JSON.stringify({
|
|
attachment: {
|
|
id: 4,
|
|
name: '名单.xlsx',
|
|
mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
size: 1200,
|
|
status: 'ready',
|
|
url: '/api/ai/chat/attachments/4',
|
|
createdAt: '2026-07-24T00:00:00.000Z',
|
|
},
|
|
}),
|
|
});
|
|
message = reduceAiSseMessage(message, {
|
|
event: 'message.completed',
|
|
data: JSON.stringify({
|
|
message: {
|
|
id: 12,
|
|
content: '完成',
|
|
reasoningContent: null,
|
|
feedback: 'like',
|
|
attachments: message.attachments,
|
|
},
|
|
}),
|
|
});
|
|
expect(message.attachments).toHaveLength(1);
|
|
expect(message.feedback).toBe('like');
|
|
});
|
|
|
|
it('uses final content and records cancellation and errors', () => {
|
|
let message = reduceAiSseMessage(undefined, {
|
|
event: 'message.completed',
|
|
data: JSON.stringify({
|
|
message: {
|
|
id: 12,
|
|
content: '最终回答',
|
|
reasoningContent: '完成',
|
|
status: 'completed',
|
|
toolRuns: [
|
|
{
|
|
toolCallId: 'nested-tool',
|
|
toolName: 'search_rooms',
|
|
status: 'success',
|
|
resultSummary: '共 4 间',
|
|
},
|
|
],
|
|
},
|
|
}),
|
|
});
|
|
message = reduceAiSseMessage(message, {
|
|
event: 'error',
|
|
data: JSON.stringify({ message: '上游服务不可用' }),
|
|
});
|
|
message = reduceAiSseMessage(message, {
|
|
event: 'message.cancelled',
|
|
data: JSON.stringify({ messageId: 12 }),
|
|
});
|
|
|
|
expect(message).toMatchObject({
|
|
id: 12,
|
|
content: '最终回答',
|
|
reasoningContent: '完成',
|
|
error: '上游服务不可用',
|
|
cancelled: true,
|
|
});
|
|
expect(message.toolRuns[0]).toMatchObject({ toolCallId: 'nested-tool', status: 'success' });
|
|
});
|
|
|
|
it('reads nested assistant message from message.created', () => {
|
|
const message = reduceAiSseMessage(undefined, {
|
|
event: 'message.created',
|
|
data: JSON.stringify({
|
|
message: { id: 9, content: '', reasoningContent: null, status: 'pending' },
|
|
}),
|
|
});
|
|
|
|
expect(message.id).toBe(9);
|
|
});
|
|
|
|
it('tolerates non-JSON event data', () => {
|
|
expect(parseSsePayload({ event: 'content.delta', data: 'plain text' })).toEqual({
|
|
event: 'content.delta',
|
|
payload: { delta: 'plain text' },
|
|
});
|
|
});
|
|
});
|