@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.
446 lines (404 loc) • 13.3 kB
text/typescript
import { act } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
import { DEFAULT_INBOX_AVATAR } from '@/const/meta';
import { INBOX_SESSION_ID } from '@/const/session';
import { useAgentStore } from '@/store/agent';
import { ChatStore } from '@/store/chat';
import { initialState } from '@/store/chat/initialState';
import { messageMapKey } from '@/store/chat/utils/messageMapKey';
import { createServerConfigStore } from '@/store/serverConfig/store';
import { LobeAgentConfig } from '@/types/agent';
import { ChatMessage } from '@/types/message';
import { MetaData } from '@/types/meta';
import { merge } from '@/utils/merge';
import { chatSelectors } from './selectors';
vi.mock('i18next', () => ({
t: vi.fn((key) => key), // Simplified mock return value
}));
const initialStore = initialState as ChatStore;
const mockMessages = [
{
id: 'msg1',
content: 'Hello World',
role: 'user',
},
{
id: 'msg2',
content: 'Goodbye World',
role: 'user',
},
{
id: 'msg3',
content: 'Function Message',
role: 'tool',
tools: [
{
arguments: ['arg1', 'arg2'],
identifier: 'func1',
apiName: 'ttt',
type: 'pluginType',
id: 'abc',
},
],
},
] as ChatMessage[];
const mockReasoningMessages = [
{
id: 'msg1',
content: 'Hello World',
role: 'user',
},
{
id: 'msg2',
content: 'Goodbye World',
role: 'user',
},
{
id: 'msg3',
content: 'Content Message',
role: 'assistant',
reasoning: {
content: 'Reasoning Content',
},
},
] as ChatMessage[];
const mockedChats = [
{
id: 'msg1',
content: 'Hello World',
role: 'user',
meta: {
avatar: '😀',
},
},
{
id: 'msg2',
content: 'Goodbye World',
role: 'user',
meta: {
avatar: '😀',
},
},
{
id: 'msg3',
content: 'Function Message',
role: 'tool',
meta: {
avatar: '🤯',
backgroundColor: 'rgba(0,0,0,0)',
description: 'inbox.desc',
title: 'inbox.title',
},
tools: [
{
arguments: ['arg1', 'arg2'],
identifier: 'func1',
apiName: 'ttt',
type: 'pluginType',
id: 'abc',
},
],
},
] as ChatMessage[];
const mockChatStore = {
messagesMap: {
[messageMapKey('abc')]: mockMessages,
},
activeId: 'abc',
} as ChatStore;
beforeAll(() => {
createServerConfigStore();
});
afterEach(() => {
createServerConfigStore().setState({ featureFlags: { edit_agent: true } });
});
describe('chatSelectors', () => {
describe('getMessageById', () => {
it('should return undefined if the message with the given id does not exist', () => {
const message = chatSelectors.getMessageById('non-existent-id')(initialStore);
expect(message).toBeUndefined();
});
it('should return the message object with the matching id', () => {
const state = merge(initialStore, {
messagesMap: {
[messageMapKey('abc')]: mockMessages,
},
activeId: 'abc',
});
const message = chatSelectors.getMessageById('msg1')(state);
expect(message).toEqual(mockedChats[0]);
});
it('should return the message with the matching id', () => {
const message = chatSelectors.getMessageById('msg1')(mockChatStore);
expect(message).toEqual(mockedChats[0]);
});
it('should return undefined if no message matches the id', () => {
const message = chatSelectors.getMessageById('nonexistent')(mockChatStore);
expect(message).toBeUndefined();
});
});
describe('getMessageByToolCallId', () => {
it('should return undefined if the message with the given id does not exist', () => {
const message = chatSelectors.getMessageByToolCallId('non-existent-id')(initialStore);
expect(message).toBeUndefined();
});
it('should return the message object with the matching tool_call_id', () => {
const toolMessage = {
id: 'msg3',
content: 'Function Message',
role: 'tool',
tool_call_id: 'ttt',
plugin: {
arguments: 'arg1',
identifier: 'func1',
apiName: 'ttt',
type: 'default',
},
} as ChatMessage;
const state = merge(initialStore, {
messagesMap: {
[messageMapKey('abc')]: [...mockMessages, toolMessage],
},
activeId: 'abc',
});
const message = chatSelectors.getMessageByToolCallId('ttt')(state);
expect(message).toMatchObject(toolMessage);
});
});
describe('currentChatsWithHistoryConfig', () => {
it('should slice the messages according to the current agent config', () => {
const state = merge(initialStore, {
messagesMap: {
[messageMapKey('abc')]: mockMessages,
},
activeId: 'abc',
});
const chats = chatSelectors.mainAIChatsWithHistoryConfig(state);
expect(chats).toHaveLength(3);
expect(chats).toEqual(mockedChats);
});
it('should slice the messages according to config, assuming historyCount is mocked to 2', async () => {
const state = merge(initialStore, {
messagesMap: {
[messageMapKey('abc')]: mockMessages,
},
activeId: 'abc',
});
act(() => {
useAgentStore.setState({
activeId: 'inbox',
agentMap: {
inbox: {
chatConfig: {
historyCount: 2,
enableHistoryCount: true,
},
model: 'abc',
} as LobeAgentConfig,
},
});
});
const chats = chatSelectors.mainAIChatsWithHistoryConfig(state);
expect(chats).toHaveLength(2);
expect(chats).toEqual([
{
id: 'msg2',
content: 'Goodbye World',
role: 'user',
meta: {
avatar: '😀',
},
},
{
id: 'msg3',
content: 'Function Message',
role: 'tool',
meta: {
avatar: '🤯',
backgroundColor: 'rgba(0,0,0,0)',
description: 'inbox.desc',
title: 'inbox.title',
},
tools: [
{
apiName: 'ttt',
arguments: ['arg1', 'arg2'],
identifier: 'func1',
id: 'abc',
type: 'pluginType',
},
],
},
]);
});
});
describe('mainDisplayChats', () => {
it('should return existing messages except tool message', () => {
const state = merge(initialStore, {
messagesMap: {
[messageMapKey('someActiveId')]: mockMessages,
},
activeId: 'someActiveId',
});
const chats = chatSelectors.mainDisplayChats(state);
expect(chats).toEqual(mockedChats.slice(0, 2));
});
});
describe('chatsMessageString', () => {
it('should concatenate the contents of all messages returned by currentChatsWithHistoryConfig', () => {
// Prepare a state with a few messages
const state = merge(initialStore, {
messagesMap: {
[messageMapKey('active-session')]: mockMessages,
},
activeId: 'active-session',
});
// Assume that the currentChatsWithHistoryConfig will return the last two messages
const expectedString = mockMessages
.slice(-2)
.map((m) => m.content)
.join('');
// Call the selector and verify the result
const concatenatedString = chatSelectors.mainAIChatsMessageString(state);
expect(concatenatedString).toBe(expectedString);
// Restore the mocks after the test
vi.restoreAllMocks();
});
});
describe('latestMessageReasoningContent', () => {
it('should return the reasoning content of the latest message', () => {
// Prepare a state with a few messages
const state = merge(initialStore, {
messagesMap: {
[messageMapKey('active-session')]: mockReasoningMessages,
},
activeId: 'active-session',
});
const expectedString = mockReasoningMessages.at(-1)?.reasoning?.content;
// Call the selector and verify the result
const reasoningContent = chatSelectors.mainAILatestMessageReasoningContent(state);
expect(reasoningContent).toBe(expectedString);
// Restore the mocks after the test
vi.restoreAllMocks();
});
});
describe('showInboxWelcome', () => {
it('should return false if the active session is not the inbox session', () => {
const state = merge(initialStore, { activeId: 'someActiveId' });
const result = chatSelectors.showInboxWelcome(state);
expect(result).toBe(false);
});
it('should return false if there are existing messages in the inbox session', () => {
const state = merge(initialStore, {
activeId: INBOX_SESSION_ID,
messagesMap: {
[messageMapKey('inbox')]: mockMessages,
},
});
const result = chatSelectors.showInboxWelcome(state);
expect(result).toBe(false);
});
it('should return true if the active session is the inbox session and there are no existing messages', () => {
const state = merge(initialStore, {
activeId: INBOX_SESSION_ID,
messages: [],
});
const result = chatSelectors.showInboxWelcome(state);
expect(result).toBe(true);
});
});
describe('currentToolMessages', () => {
it('should return only tool messages', () => {
const messages = [
{ id: '1', role: 'user', content: 'Hello' },
{ id: '2', role: 'assistant', content: 'Hi' },
{ id: '3', role: 'tool', content: 'Tool message 1' },
{ id: '4', role: 'user', content: 'Query' },
{ id: '5', role: 'tool', tools: [] },
] as ChatMessage[];
const state: Partial<ChatStore> = {
activeId: 'test-id',
messagesMap: {
[messageMapKey('test-id')]: messages,
},
};
const result = chatSelectors.currentToolMessages(state as ChatStore);
expect(result).toHaveLength(2);
expect(result.every((msg) => msg.role === 'tool')).toBe(true);
});
it('should return an empty array when no tool messages exist', () => {
const messages = [
{ id: '1', role: 'user', content: 'Hello' },
{ id: '2', role: 'assistant', content: 'Hi' },
] as ChatMessage[];
const state: Partial<ChatStore> = {
activeId: 'test-id',
messagesMap: {
[messageMapKey('test-id')]: messages,
},
};
const result = chatSelectors.currentToolMessages(state as ChatStore);
expect(result).toHaveLength(0);
});
});
describe('currentChatKey', () => {
it('should generate correct key with activeId only', () => {
const state: Partial<ChatStore> = {
activeId: 'testId',
activeTopicId: undefined,
};
const result = chatSelectors.currentChatKey(state as ChatStore);
expect(result).toBe(messageMapKey('testId', undefined));
});
it('should generate correct key with both activeId and activeTopicId', () => {
const state: Partial<ChatStore> = {
activeId: 'testId',
activeTopicId: 'topicId',
};
const result = chatSelectors.currentChatKey(state as ChatStore);
expect(result).toBe(messageMapKey('testId', 'topicId'));
});
it('should generate key with undefined activeId', () => {
const state: Partial<ChatStore> = {
activeId: undefined,
activeTopicId: 'topicId',
};
const result = chatSelectors.currentChatKey(state as ChatStore);
expect(result).toBe(messageMapKey(undefined as any, 'topicId'));
});
it('should generate key with empty string activeId', () => {
const state: Partial<ChatStore> = {
activeId: '',
activeTopicId: undefined,
};
const result = chatSelectors.currentChatKey(state as ChatStore);
expect(result).toBe(messageMapKey('', undefined));
});
});
describe('isToolCallStreaming', () => {
it('should return true when tool call is streaming for given message and index', () => {
const state: Partial<ChatStore> = {
toolCallingStreamIds: {
'msg-1': [true, false, true],
},
};
expect(chatSelectors.isToolCallStreaming('msg-1', 0)(state as ChatStore)).toBe(true);
expect(chatSelectors.isToolCallStreaming('msg-1', 2)(state as ChatStore)).toBe(true);
});
it('should return false when tool call is not streaming for given message and index', () => {
const state: Partial<ChatStore> = {
toolCallingStreamIds: {
'msg-1': [true, false, true],
},
};
expect(chatSelectors.isToolCallStreaming('msg-1', 1)(state as ChatStore)).toBe(false);
expect(chatSelectors.isToolCallStreaming('msg-2', 0)(state as ChatStore)).toBe(false);
});
it('should return false when no streaming data exists for the message', () => {
const state: Partial<ChatStore> = {
toolCallingStreamIds: {},
};
expect(chatSelectors.isToolCallStreaming('msg-1', 0)(state as ChatStore)).toBe(false);
});
});
});