@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.
506 lines (425 loc) • 15.7 kB
text/typescript
import { ChatMessage, ChatToolPayload } from '@/types/message';
import { MessageDispatch, messagesReducer } from './reducer';
describe('messagesReducer', () => {
let initialState: ChatMessage[];
beforeEach(() => {
initialState = [
{
id: 'message1',
content: 'Hello World',
createdAt: 1629264000000,
updatedAt: 1629264000000,
role: 'user',
meta: {},
},
{
id: 'message2',
content: 'How are you?',
createdAt: 1629264000000,
updatedAt: 1629264000000,
role: 'assistant',
meta: {},
tools: [
{ identifier: 'tool1', apiName: 'calculator', id: 'abc', type: 'default', arguments: '' },
],
},
];
});
describe('updateMessage', () => {
it('should update the specified message with the provided value', () => {
const payload: MessageDispatch = {
type: 'updateMessage',
id: 'message1',
value: { content: 'Updated Message' },
};
const newState = messagesReducer(initialState, payload);
const newMessage = newState.find((i) => i.id === 'message1');
expect(newMessage).not.toBeUndefined();
expect(newMessage?.content).toBe('Updated Message');
expect(newMessage?.updatedAt).toBeGreaterThan(initialState[0].updatedAt);
});
it('should not modify the state if the specified message does not exist', () => {
const payload: MessageDispatch = {
type: 'updateMessage',
id: 'nonexistentMessage',
value: { content: 'Updated Message' },
};
const newState = messagesReducer(initialState, payload);
expect(newState).toEqual(initialState);
});
it('should not modify the state if the specified message does not exist', () => {
const payload: MessageDispatch = {
type: 'updateMessage',
id: 'nonexistentMessage',
value: { content: 'Updated Message' },
};
const newState = messagesReducer(initialState, payload);
expect(newState).toEqual(initialState);
});
});
describe('unimplemented type', () => {
it('should throw an error when an unimplemented type is provided', () => {
// @ts-ignore
const payload: MessageDispatch = { type: 'unimplementedType' };
expect(() => messagesReducer(initialState, payload)).toThrowError(
'暂未实现的 type,请检查 reducer',
);
});
});
describe('updateMessageExtra', () => {
it('should update the extra field of a message', () => {
const payload: MessageDispatch = {
type: 'updateMessageExtra',
id: 'message1',
key: 'testKey',
value: 'testValue',
};
const newState = messagesReducer(initialState, payload);
const updatedMessage = newState.find((m) => m.id === 'message1');
expect(updatedMessage?.extra).toEqual({ testKey: 'testValue' });
expect(updatedMessage?.updatedAt).toBeGreaterThan(initialState[0].updatedAt);
});
it('should update the extra field of a message if extra exist', () => {
const payload: MessageDispatch = {
type: 'updateMessageExtra',
id: 'data',
key: 'abc',
value: '2',
};
const newState = messagesReducer(
[
{
id: 'data',
content: 'Hello World',
createdAt: 1629264000000,
updatedAt: 1629264000000,
role: 'user',
meta: {},
extra: { abc: '1' },
} as ChatMessage,
...initialState,
],
payload,
);
const updatedMessage = newState.find((m) => m.id === 'data');
expect(updatedMessage?.extra).toEqual({ abc: '2' });
expect(updatedMessage?.updatedAt).toBeGreaterThan(initialState[0].updatedAt);
});
it('should not modify state if message is not found', () => {
const payload: MessageDispatch = {
type: 'updateMessageExtra',
id: 'nonexistent',
key: 'testKey',
value: 'testValue',
};
const newState = messagesReducer(initialState, payload);
expect(newState).toEqual(initialState);
});
});
describe('updatePluginState', () => {
it('should update the plugin state of a message', () => {
const payload: MessageDispatch = {
type: 'updatePluginState',
id: 'message1',
key: 'pluginKey',
value: 'pluginValue',
};
const newState = messagesReducer(initialState, payload);
const updatedMessage = newState.find((m) => m.id === 'message1');
expect(updatedMessage?.pluginState).toEqual({ pluginKey: 'pluginValue' });
expect(updatedMessage?.updatedAt).toBeGreaterThan(initialState[0].updatedAt);
});
it('should not modify state if message is not found', () => {
const payload: MessageDispatch = {
type: 'updatePluginState',
id: 'nonexistent',
key: 'pluginKey',
value: 'pluginValue',
};
const newState = messagesReducer(initialState, payload);
expect(newState).toEqual(initialState);
});
it('should not modify state if new plugin state is equal to old state', () => {
const state = [
{
...initialState[0],
pluginState: { existingKey: 'existingValue' },
},
];
const payload: MessageDispatch = {
type: 'updatePluginState',
id: 'message1',
key: 'existingKey',
value: 'existingValue',
};
const newState = messagesReducer(state, payload);
expect(newState).toEqual(state);
});
});
describe('updateMessagePlugin', () => {
it('should update the plugin of a tool message', () => {
const toolMessage: ChatMessage = {
id: 'toolMessage',
role: 'tool',
content: 'Tool content',
createdAt: 1629264000000,
updatedAt: 1629264000000,
meta: {},
plugin: {
identifier: 'tool1',
apiName: 'calculator',
type: 'default',
arguments: '',
},
tool_call_id: 'abc',
};
const state = [...initialState, toolMessage];
const payload: MessageDispatch = {
type: 'updateMessagePlugin',
id: 'toolMessage',
value: { identifier: 'newPlugin' },
};
const newState = messagesReducer(state, payload);
const updatedMessage = newState.find((m) => m.id === 'toolMessage');
expect(updatedMessage?.plugin).toEqual({
apiName: 'calculator',
arguments: '',
identifier: 'newPlugin',
type: 'default',
});
expect(updatedMessage?.updatedAt).toBeGreaterThan(toolMessage.updatedAt);
});
it('should not modify state if message is not found or not a tool message', () => {
const payload: MessageDispatch = {
type: 'updateMessagePlugin',
id: 'message1', // This is a user message, not a tool message
value: { identifier: 'newPlugin' },
};
const newState = messagesReducer(initialState, payload);
expect(newState).toEqual(initialState);
});
});
describe('updateMessageTools', () => {
it('should update a specific tool in an assistant message', () => {
const payload: MessageDispatch = {
type: 'updateMessageTools',
id: 'message2',
tool_call_id: 'abc',
value: { apiName: 'updatedCalculator' },
};
const newState = messagesReducer(initialState, payload);
const updatedMessage = newState.find((m) => m.id === 'message2');
expect(updatedMessage?.tools?.[0]).toEqual({
apiName: 'updatedCalculator',
arguments: '',
id: 'abc',
identifier: 'tool1',
type: 'default',
});
expect(updatedMessage?.updatedAt).toBeGreaterThan(initialState[1].updatedAt);
});
it('should not modify state if message is not found or not an assistant message', () => {
const payload: MessageDispatch = {
type: 'updateMessageTools',
id: 'message1', // This is a user message, not an assistant message
tool_call_id: 'tool1',
value: { identifier: 'updatedCalculator' },
};
const newState = messagesReducer(initialState, payload);
expect(newState).toEqual(initialState);
});
it('should not modify state if tool is not found in the message', () => {
const payload: MessageDispatch = {
type: 'updateMessageTools',
id: 'message2',
tool_call_id: 'nonexistentTool',
value: { identifier: 'updatedCalculator' },
};
const newState = messagesReducer(initialState, payload);
expect(newState).toEqual(initialState);
});
});
describe('addMessageTool', () => {
it('should add a tool to the specified assistant message if it dont have tools', () => {
const messageId = '1';
const toolPayload: ChatToolPayload = {
id: 'tc_1',
type: 'default',
identifier: 'tool1',
apiName: 'testFunction',
arguments: '{"arg1": "value1"}',
};
const payload: MessageDispatch = {
type: 'addMessageTool',
id: messageId,
value: toolPayload,
};
const newState = messagesReducer(
[...initialState, { id: messageId, role: 'assistant', content: '' } as ChatMessage],
payload,
);
const updatedMessage = newState.find((m) => m.id === messageId);
expect(updatedMessage).not.toBeUndefined();
expect(updatedMessage?.tools).toHaveLength(1);
expect(updatedMessage?.tools?.[0]).toEqual(toolPayload);
expect(updatedMessage?.updatedAt).toBeGreaterThan(initialState[0].updatedAt);
});
it('should add a tool to the specified assistant message', () => {
const messageId = 'message2';
const toolPayload: ChatToolPayload = {
id: 'tc_1',
type: 'default',
identifier: 'tool1',
apiName: 'testFunction',
arguments: '{"arg1": "value1"}',
};
const payload: MessageDispatch = {
type: 'addMessageTool',
id: messageId,
value: toolPayload,
};
const newState = messagesReducer(
[...initialState, { id: messageId, role: 'assistant', content: '' } as ChatMessage],
payload,
);
const updatedMessage = newState.find((m) => m.id === messageId);
expect(updatedMessage).not.toBeUndefined();
expect(updatedMessage?.tools).toHaveLength(2);
expect(updatedMessage?.tools?.[1]).toEqual(toolPayload);
});
it('should not modify the state if the message is not found', () => {
const toolPayload: ChatToolPayload = {
id: 'tc_1',
type: 'default',
identifier: 'tool1',
apiName: 'testFunction',
arguments: '{"arg1": "value1"}',
};
const payload: MessageDispatch = {
type: 'addMessageTool',
id: 'nonexistentMessage',
value: toolPayload,
};
const newState = messagesReducer(initialState, payload);
expect(newState).toEqual(initialState);
});
it('should not add a tool if the message is not an assistant message', () => {
const toolPayload: ChatToolPayload = {
id: 'tc_1',
type: 'default',
identifier: 'tool1',
apiName: 'testFunction',
arguments: '{"arg1": "value1"}',
};
const payload: MessageDispatch = {
type: 'addMessageTool',
id: 'message1', // This is a user message
value: toolPayload,
};
const newState = messagesReducer(initialState, payload);
expect(newState).toEqual(initialState);
});
});
describe('deleteMessageTool', () => {
it('should delete the specified tool from the message', () => {
const payload: MessageDispatch = {
type: 'deleteMessageTool',
id: 'message2',
tool_call_id: 'abc',
};
const newState = messagesReducer(initialState, payload);
const updatedMessage = newState.find((m) => m.id === 'message2');
expect(updatedMessage).not.toBeUndefined();
expect(updatedMessage?.tools).toHaveLength(0);
expect(updatedMessage?.updatedAt).toBeGreaterThan(initialState[0].updatedAt);
});
it('should not modify the state if the message is not found', () => {
const payload: MessageDispatch = {
type: 'deleteMessageTool',
id: 'nonexistentMessage',
tool_call_id: 'tool1',
};
const newState = messagesReducer(initialState, payload);
expect(newState).toEqual(initialState);
});
it('should not modify the state if the tool is not found', () => {
const payload: MessageDispatch = {
type: 'deleteMessageTool',
id: 'message1',
tool_call_id: 'nonexistentTool',
};
const newState = messagesReducer(initialState, payload);
expect(newState).toEqual(initialState);
});
it('should not delete a tool if the message is not an assistant message', () => {
const payload: MessageDispatch = {
type: 'deleteMessageTool',
id: 'message1', // This is a user message
tool_call_id: 'tool1',
};
const newState = messagesReducer(initialState, payload);
expect(newState).toEqual(initialState);
});
});
describe('createMessage', () => {
it('should add a new message to the state', () => {
const payload: MessageDispatch = {
type: 'createMessage',
id: 'newMessage',
value: {
content: 'New message content',
role: 'user',
sessionId: 'inbox',
},
};
const newState = messagesReducer(initialState, payload);
expect(newState.length).toBe(initialState.length + 1);
const newMessage = newState.find((m) => m.id === 'newMessage');
expect(newMessage).toBeDefined();
expect(newMessage?.content).toBe('New message content');
expect(newMessage?.role).toBe('user');
expect(newMessage?.createdAt).toBeDefined();
expect(newMessage?.updatedAt).toBeDefined();
expect(newMessage?.meta).toEqual({});
});
});
describe('deleteMessage', () => {
it('should remove a message from the state', () => {
const payload: MessageDispatch = {
type: 'deleteMessage',
id: 'message1',
};
const newState = messagesReducer(initialState, payload);
expect(newState.length).toBe(initialState.length - 1);
expect(newState.find((m) => m.id === 'message1')).toBeUndefined();
});
it('should not modify state if message to delete is not found', () => {
const payload: MessageDispatch = {
type: 'deleteMessage',
id: 'nonexistentMessage',
};
const newState = messagesReducer(initialState, payload);
expect(newState).toEqual(initialState);
});
});
describe('deleteMessages', () => {
it('should remove 2 messages from the state', () => {
const payload: MessageDispatch = {
type: 'deleteMessages',
ids: ['message1', 'message2'],
};
const newState = messagesReducer(initialState, payload);
expect(newState.length).toBe(0);
expect(newState.find((m) => m.id === 'message1')).toBeUndefined();
expect(newState.find((m) => m.id === 'message2')).toBeUndefined();
});
it('should not modify state if message to delete is not found', () => {
const payload: MessageDispatch = {
type: 'deleteMessage',
id: 'nonexistentMessage',
};
const newState = messagesReducer(initialState, payload);
expect(newState).toEqual(initialState);
});
});
});