@lobehub/chat
Version:
Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.
523 lines (472 loc) • 16.3 kB
text/typescript
import { describe, expect, it } from 'vitest';
import { PipelineContext } from '../../types';
import { GroupMessageFlattenProcessor } from '../GroupMessageFlatten';
describe('GroupMessageFlattenProcessor', () => {
const createContext = (messages: any[]): PipelineContext => ({
initialState: { messages: [] },
isAborted: false,
messages,
metadata: {},
});
describe('Basic Scenarios', () => {
it('should flatten group message with single child and single tool result', async () => {
const processor = new GroupMessageFlattenProcessor();
const input: any[] = [
{
id: 'msg-group-1',
role: 'group',
content: '',
createdAt: '2025-10-27T10:00:00.000Z',
updatedAt: '2025-10-27T10:00:10.000Z',
meta: { title: 'Test Agent' },
children: [
{
id: 'msg-1',
content: 'Checking weather',
tools: [
{
id: 'tool-1',
type: 'builtin',
apiName: 'search',
arguments: '{"query":"weather"}',
identifier: 'web-browsing',
result: {
id: 'msg-tool-1',
content: 'Weather is sunny, 25°C',
error: null,
state: { cached: true },
},
},
],
usage: { totalTokens: 100 },
performance: { tps: 20 },
},
],
},
];
const context = createContext(input);
const result = await processor.process(context);
// Should create 2 messages: 1 assistant + 1 tool
expect(result.messages).toHaveLength(2);
// Check assistant message
const assistantMsg = result.messages[0];
expect(assistantMsg.role).toBe('assistant');
expect(assistantMsg.id).toBe('msg-1');
expect(assistantMsg.content).toBe('Checking weather');
expect(assistantMsg.tools).toHaveLength(1);
expect(assistantMsg.tools[0]).toEqual({
id: 'tool-1',
type: 'builtin',
apiName: 'search',
arguments: '{"query":"weather"}',
identifier: 'web-browsing',
});
expect(assistantMsg.createdAt).toBe('2025-10-27T10:00:00.000Z');
expect(assistantMsg.meta).toEqual({ title: 'Test Agent' });
// Check tool message
const toolMsg = result.messages[1];
expect(toolMsg.role).toBe('tool');
expect(toolMsg.id).toBe('msg-tool-1');
expect(toolMsg.content).toBe('Weather is sunny, 25°C');
expect(toolMsg.tool_call_id).toBe('tool-1');
expect(toolMsg.plugin).toEqual({
id: 'tool-1',
type: 'builtin',
apiName: 'search',
arguments: '{"query":"weather"}',
identifier: 'web-browsing',
});
expect(toolMsg.pluginState).toEqual({ cached: true });
// Check metadata
expect(result.metadata.groupMessagesFlattened).toBe(1);
expect(result.metadata.assistantMessagesCreated).toBe(1);
expect(result.metadata.toolMessagesCreated).toBe(1);
});
it('should flatten group message with multiple children', async () => {
const processor = new GroupMessageFlattenProcessor();
const input: any[] = [
{
id: 'msg-group-1',
role: 'group',
content: '',
children: [
{
id: 'msg-1',
content: 'First response',
tools: [
{
id: 'tool-1',
type: 'builtin',
apiName: 'search',
arguments: '{}',
identifier: 'web-browsing',
result: {
id: 'msg-tool-1',
content: 'Result 1',
error: null,
state: {},
},
},
],
},
{
id: 'msg-2',
content: 'Follow-up response',
tools: [
{
id: 'tool-2',
type: 'builtin',
apiName: 'search',
arguments: '{}',
identifier: 'web-browsing',
result: {
id: 'msg-tool-2',
content: 'Result 2',
error: null,
state: {},
},
},
],
},
],
},
];
const context = createContext(input);
const result = await processor.process(context);
// Should create 4 messages: 2 assistants + 2 tools
expect(result.messages).toHaveLength(4);
expect(result.messages[0].role).toBe('assistant');
expect(result.messages[0].id).toBe('msg-1');
expect(result.messages[1].role).toBe('tool');
expect(result.messages[1].id).toBe('msg-tool-1');
expect(result.messages[2].role).toBe('assistant');
expect(result.messages[2].id).toBe('msg-2');
expect(result.messages[3].role).toBe('tool');
expect(result.messages[3].id).toBe('msg-tool-2');
expect(result.metadata.groupMessagesFlattened).toBe(1);
expect(result.metadata.assistantMessagesCreated).toBe(2);
expect(result.metadata.toolMessagesCreated).toBe(2);
});
it('should handle child without tool result (still executing)', async () => {
const processor = new GroupMessageFlattenProcessor();
const input: any[] = [
{
id: 'msg-group-1',
role: 'group',
content: '',
children: [
{
id: 'msg-1',
content: 'Checking weather',
tools: [
{
id: 'tool-1',
type: 'builtin',
apiName: 'search',
arguments: '{}',
identifier: 'web-browsing',
// No result - tool is still executing
},
],
},
],
},
];
const context = createContext(input);
const result = await processor.process(context);
// Should only create 1 assistant message (no tool message)
expect(result.messages).toHaveLength(1);
expect(result.messages[0].role).toBe('assistant');
expect(result.messages[0].tools).toHaveLength(1);
expect(result.metadata.assistantMessagesCreated).toBe(1);
expect(result.metadata.toolMessagesCreated).toBe(0);
});
it('should handle tool result with error', async () => {
const processor = new GroupMessageFlattenProcessor();
const input: any[] = [
{
id: 'msg-group-1',
role: 'group',
content: '',
children: [
{
id: 'msg-1',
content: '',
tools: [
{
id: 'tool-1',
type: 'builtin',
apiName: 'search',
arguments: '{}',
identifier: 'web-browsing',
result: {
id: 'msg-tool-1',
content: '',
error: { message: 'Network timeout' },
state: {},
},
},
],
},
],
},
];
const context = createContext(input);
const result = await processor.process(context);
expect(result.messages).toHaveLength(2);
const toolMsg = result.messages[1];
expect(toolMsg.role).toBe('tool');
expect(toolMsg.pluginError).toEqual({ message: 'Network timeout' });
});
});
describe('Mixed Messages', () => {
it('should preserve non-group messages and flatten group messages', async () => {
const processor = new GroupMessageFlattenProcessor();
const input: any[] = [
{
id: 'msg-user-1',
role: 'user',
content: 'What is the weather?',
},
{
id: 'msg-group-1',
role: 'group',
content: '',
children: [
{
id: 'msg-1',
content: 'Checking...',
tools: [
{
id: 'tool-1',
type: 'builtin',
apiName: 'search',
arguments: '{}',
identifier: 'web-browsing',
result: {
id: 'msg-tool-1',
content: 'Sunny',
error: null,
state: {},
},
},
],
},
],
},
{
id: 'msg-user-2',
role: 'user',
content: 'Thanks!',
},
];
const context = createContext(input);
const result = await processor.process(context);
// 1 user + (1 assistant + 1 tool) + 1 user = 4 messages
expect(result.messages).toHaveLength(4);
expect(result.messages[0].role).toBe('user');
expect(result.messages[0].id).toBe('msg-user-1');
expect(result.messages[1].role).toBe('assistant');
expect(result.messages[2].role).toBe('tool');
expect(result.messages[3].role).toBe('user');
expect(result.messages[3].id).toBe('msg-user-2');
});
});
describe('Edge Cases', () => {
it('should handle group message with empty children array', async () => {
const processor = new GroupMessageFlattenProcessor();
const input: any[] = [
{
id: 'msg-group-1',
role: 'group',
content: '',
children: [],
},
];
const context = createContext(input);
const result = await processor.process(context);
// Empty children means no messages created
expect(result.messages).toHaveLength(0);
});
it('should handle group message without children field', async () => {
const processor = new GroupMessageFlattenProcessor();
const input: any[] = [
{
id: 'msg-group-1',
role: 'group',
content: '',
// No children field
},
];
const context = createContext(input);
const result = await processor.process(context);
// Should keep the message as-is (though this is invalid data)
expect(result.messages).toHaveLength(1);
expect(result.messages[0].id).toBe('msg-group-1');
});
it('should preserve reasoning field from group message', async () => {
const processor = new GroupMessageFlattenProcessor();
const input: any[] = [
{
id: 'msg-group-1',
role: 'group',
content: '',
reasoning: {
content: 'Thinking about the query...',
signature: 'sig-123',
},
children: [
{
id: 'msg-1',
content: 'Result',
tools: [],
},
],
},
];
const context = createContext(input);
const result = await processor.process(context);
expect(result.messages).toHaveLength(1);
expect(result.messages[0].reasoning).toEqual({
content: 'Thinking about the query...',
signature: 'sig-123',
});
});
it('should preserve parent/thread/group/topic IDs', async () => {
const processor = new GroupMessageFlattenProcessor();
const input: any[] = [
{
id: 'msg-group-1',
role: 'group',
content: '',
parentId: 'parent-1',
threadId: 'thread-1',
groupId: 'group-1',
topicId: 'topic-1',
children: [
{
id: 'msg-1',
content: '',
tools: [
{
id: 'tool-1',
type: 'builtin',
apiName: 'search',
arguments: '{}',
identifier: 'web-browsing',
result: {
id: 'msg-tool-1',
content: 'Result',
error: null,
state: {},
},
},
],
},
],
},
];
const context = createContext(input);
const result = await processor.process(context);
const assistantMsg = result.messages[0];
expect(assistantMsg.parentId).toBe('parent-1');
expect(assistantMsg.threadId).toBe('thread-1');
expect(assistantMsg.groupId).toBe('group-1');
expect(assistantMsg.topicId).toBe('topic-1');
const toolMsg = result.messages[1];
expect(toolMsg.parentId).toBe('parent-1');
expect(toolMsg.threadId).toBe('thread-1');
expect(toolMsg.groupId).toBe('group-1');
expect(toolMsg.topicId).toBe('topic-1');
});
});
describe('Real-world Test Case', () => {
it('should flatten the provided real-world group message', async () => {
const processor = new GroupMessageFlattenProcessor();
// Using the real-world test data provided
const input: any[] = [
{
id: 'msg_LnIlOyMUnX1ylf',
role: 'group',
content: '',
reasoning: {
content:
'**Checking Hangzhou weather**\n\nIt seems the user is asking to check the weather in Hangzhou...',
},
createdAt: '2025-10-27T10:47:59.475Z',
updatedAt: '2025-10-27T10:48:10.768Z',
topicId: 'tpc_WQ1wRvxdDpLw',
parentId: 'msg_ekwWzxAKueHkd6',
meta: {
avatar: '🤯',
title: '随便聊聊',
},
children: [
{
content: '',
id: 'msg_LnIlOyMUnX1ylf',
performance: {
tps: 29.336734693877553,
ttft: 3844,
duration: 3920,
latency: 7764,
},
tools: [
{
id: 'call_kYZG2daTTfnkgNiN6oIR25YK',
type: 'builtin',
apiName: 'search',
arguments:
'{"query":"杭州 天气","searchCategories":["general"],"searchEngines":["google","bing"],"searchTimeRange":"day"}',
identifier: 'lobe-web-browsing',
result: {
content: '<searchResults>...</searchResults>',
error: null,
id: 'msg_DS234ZZMju1NNO',
state: {
query: '杭州 天气',
costTime: 1752,
resultNumbers: 600,
},
},
},
],
usage: {
inputCacheMissTokens: 2404,
totalTokens: 2519,
cost: 0.000831,
},
},
],
usage: {
totalTokens: 2519,
cost: 0.000831,
},
},
];
const context = createContext(input);
const result = await processor.process(context);
// Should create 2 messages
expect(result.messages).toHaveLength(2);
// Check assistant message
const assistantMsg = result.messages[0];
expect(assistantMsg.role).toBe('assistant');
expect(assistantMsg.id).toBe('msg_LnIlOyMUnX1ylf');
expect(assistantMsg.tools).toHaveLength(1);
expect(assistantMsg.tools[0].identifier).toBe('lobe-web-browsing');
expect(assistantMsg.tools[0].apiName).toBe('search');
expect(assistantMsg.reasoning).toBeDefined();
expect(assistantMsg.topicId).toBe('tpc_WQ1wRvxdDpLw');
expect(assistantMsg.parentId).toBe('msg_ekwWzxAKueHkd6');
// Check tool message
const toolMsg = result.messages[1];
expect(toolMsg.role).toBe('tool');
expect(toolMsg.id).toBe('msg_DS234ZZMju1NNO');
expect(toolMsg.tool_call_id).toBe('call_kYZG2daTTfnkgNiN6oIR25YK');
expect(toolMsg.plugin).toBeDefined();
expect(toolMsg.plugin.identifier).toBe('lobe-web-browsing');
expect(toolMsg.pluginState).toBeDefined();
expect(toolMsg.pluginState.query).toBe('杭州 天气');
});
});
});