coze-studio/frontend/packages/common/chat-area/chat-area/__tests__/messages.test.ts

487 lines
13 KiB
TypeScript

/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable -- test */
vi.mock(
'../../chat-area-utils/src/parse-markdown/parse-markdown-to-text.ts',
() => ({
parseMarkdown: vi.fn(),
}),
);
import Mock from 'mockjs';
import { ContentType, Scene, type Message } from '@coze-common/chat-core';
import { describe, expect, it, vi } from 'vitest';
import { type MessagesStore, createMessagesStore } from '../src/store/messages';
import {
type MessageGroup,
type TextMessage,
type MessageIdStruct,
} from '../src/store/types';
import { createSectionIdStore } from '../src/store/section-id';
import { subscribeMessageToUpdateMessageGroup } from '../src/store/messages';
import { SystemLifeCycleService } from '../src/plugin/life-cycle';
import { renderHook } from '@testing-library/react-hooks';
import { useCreatePluginStoreSet } from '../src/hooks/context/use-create-plugin-store';
import { createPluginStore } from '../src/store/plugins';
vi.mock('@coze-common/chat-core', () => ({
ContentType: vi.fn(),
VerboseMsgType: vi.fn(),
Scene: {
CozeHome: 3,
},
messageSource: vi.fn(),
}));
vi.mock('@coze-arch/coze-design', () => ({
UIToast: {
error: vi.fn(),
},
}));
vi.mock('@coze-arch/bot-api/memory', () => ({
BotTableRWMode: {
LimitedReadWrite: 1,
ReadOnly: 2,
CozeHome: 3,
},
messageSource: vi.fn(),
}));
vi.mock('../src/utils/message', () => ({
getMessageUniqueKey: vi.fn(
(message: Message<ContentType>) =>
message?.message_id || message?.extra_info?.local_message_id,
),
findMessageById: vi.fn((messages: Message<ContentType>[], id: string) =>
messages.find(
m => m.message_id === id || m.extra_info?.local_message_id === id,
),
),
findMessageByIdStruct: vi.fn(
(messages: Message<ContentType>[], idStruct: MessageIdStruct) =>
messages.find(
m =>
m.message_id === idStruct.message_id ||
m.extra_info?.local_message_id ===
idStruct.extra_info?.local_message_id,
),
),
findMessageIndexById: vi.fn((messages: Message<ContentType>[], id: string) =>
messages.findIndex(
m => m.message_id === id || m.extra_info?.local_message_id === id,
),
),
findMessageIndexByIdStruct: vi.fn(
(messages: Message<ContentType>[], idStruct: MessageIdStruct) =>
messages.findIndex(
m =>
m.message_id === idStruct.message_id ||
m.extra_info?.local_message_id ===
idStruct.extra_info?.local_message_id,
),
),
getIsValidMessage: vi.fn(() => true),
getIsTriggerMessage: vi.fn(() => false),
}));
vi.stubGlobal('IS_DEV_MODE', false);
let useMessagesStore: MessagesStore;
const testMessageId = '321321';
const testMessageLocalId = '123123';
const testUserMessageLocalId = '6345634523';
const testReplyId = '666666666';
const testSectionId = '9999';
const testMessage: Message<ContentType.Text> = {
message_id: testMessageId,
extra_info: {
local_message_id: testMessageLocalId,
bot_state: '',
execute_display_name: '',
input_tokens: '',
output_tokens: '',
plugin: '',
plugin_request: '',
plugin_status: '',
time_cost: '',
token: '',
tool_name: '',
workflow_tokens: '',
},
role: 'assistant',
content: '',
content_obj: '',
type: 'answer',
is_finish: true,
broken_pos: 9999999,
reply_id: testReplyId,
section_id: '',
sender_id: '',
mention_list: [],
content_type: ContentType.Text,
};
const testUserMessage: TextMessage = {
role: 'user',
type: 'ack',
content: '',
content_type: ContentType.Text,
message_id: testReplyId,
reply_id: testReplyId,
section_id: '',
extra_info: {
local_message_id: testUserMessageLocalId,
input_tokens: '',
output_tokens: '',
token: '',
plugin_status: '',
time_cost: '',
workflow_tokens: '',
bot_state: '',
plugin_request: '',
tool_name: '',
plugin: '',
execute_display_name: '',
},
/** Normal, interrupted state, used when pulling the message list, this field is not available when chat is running. */
/** interrupt position */
broken_pos: 9999999,
sender_id: '',
mention_list: [],
content_obj: '',
is_finish: true,
};
Mock.Random.extend({
random_type() {
const type = [
'answer',
'function_call',
'tool_response',
'follow_up',
'ack',
'question',
];
return this.pick?.(type);
},
random_content_type() {
const contentType = [
'text',
'link',
'music',
'video',
'card',
'image',
'file',
'tako',
'custom',
];
return this.pick?.(contentType);
},
random_message_status() {
const status = ['available', 'broken'];
return this.pick?.(status);
},
});
const randomTestMessageList: Message<ContentType>[] = Mock.mock({
'array|20-60': [
{
role: 'assistant',
type: '@RANDOM_TYPE',
content: '@string',
content_type: '@RANDOM_CONTENT_TYPE',
message_id: '@string',
reply_id: '9999999',
section_id: '8888888',
extra_info: {
local_message_id: '@string',
input_tokens: '@string',
output_tokens: '@string',
token: '@string',
plugin_status: '@string',
time_cost: '@string',
workflow_tokens: '@string',
bot_state: '@string',
plugin_request: '@string',
tool_name: '@string',
plugin: '@string',
},
/** Normal, interrupted state, used when pulling the message list, this field is not available when chat is running. */
/** interrupt position */
broken_pos: 9999999,
sender_id: '77777',
mention_list: [],
content_obj: '',
is_finish: true,
},
],
}).array;
beforeEach(() => {
vi.useFakeTimers();
const sectionIdStore = createSectionIdStore('unit-test');
const newUseMessagesStore = createMessagesStore('unit-test');
const { result } = renderHook(() =>
useCreatePluginStoreSet({
mark: 'test',
scene: Scene.CozeHome,
}),
);
const pluginStore = createPluginStore('unit-test');
const lifeCycleService = new SystemLifeCycleService({
usePluginStore: pluginStore,
});
useMessagesStore = newUseMessagesStore;
sectionIdStore.getState().setLatestSectionId(testSectionId);
subscribeMessageToUpdateMessageGroup(
{
useMessagesStore: newUseMessagesStore,
useSectionIdStore: sectionIdStore,
},
{},
lifeCycleService,
);
});
describe('useMessagesStore', () => {
beforeEach(() => {
useMessagesStore = createMessagesStore('unit-test');
});
it('findMessage', () => {
const { findMessage } = useMessagesStore.getState();
const undefinedResult = findMessage('not-exist');
expect(undefinedResult).toBeUndefined();
useMessagesStore.getState().addMessage(testMessage);
const messageByLocalId = findMessage(testMessageLocalId);
expect(messageByLocalId).toStrictEqual(testMessage);
const messageById = findMessage(testMessageId);
expect(messageById).toStrictEqual(testMessage);
});
it('hasMessage', () => {
const { hasMessage } = useMessagesStore.getState();
const falsy = hasMessage('not-exist');
expect(falsy).toBeFalsy();
useMessagesStore.getState().addMessage(testMessage);
const localIdTruthy = hasMessage(testMessageLocalId);
expect(localIdTruthy).toBeTruthy();
const idTruthy = hasMessage(testMessageId);
expect(idTruthy).toBeTruthy();
});
it('updateMessage', () => {
const { updateMessage } = useMessagesStore.getState();
useMessagesStore.getState().addMessage(testMessage);
const newMessage: Message<ContentType.Text> = {
...testMessage,
content: 'new content',
};
updateMessage(testMessageId, newMessage);
const message = useMessagesStore.getState().findMessage(testMessageId);
expect(message?.content).toBe('new content');
});
it('addMessage', () => {
const { addMessage } = useMessagesStore.getState();
addMessage(testMessage);
const message = useMessagesStore.getState().findMessage(testMessageId);
expect(message).toStrictEqual(testMessage);
});
it('addMessages', () => {
const { addMessages } = useMessagesStore.getState();
addMessages([testMessage]);
const message = useMessagesStore.getState().findMessage(testMessageId);
expect(message).toStrictEqual(testMessage);
});
it('deleteMessageByIdStruct', () => {
const { deleteMessageByIdStruct } = useMessagesStore.getState();
useMessagesStore.getState().addMessage(testMessage);
const errorIdStruct: MessageIdStruct = {
message_id: 'not-exist',
extra_info: { local_message_id: 'not-exist' },
};
const correctIdStruct: MessageIdStruct = {
message_id: testMessageId,
extra_info: { local_message_id: testMessageLocalId },
};
deleteMessageByIdStruct(errorIdStruct);
expect(useMessagesStore.getState().messages).toStrictEqual([testMessage]);
deleteMessageByIdStruct(correctIdStruct);
expect(useMessagesStore.getState().messages).toStrictEqual([]);
});
it('deleteMessageById', () => {
const { deleteMessageById } = useMessagesStore.getState();
useMessagesStore.getState().addMessage(testMessage);
const errorId = 'not-exist';
deleteMessageById(errorId);
expect(useMessagesStore.getState().messages).toStrictEqual([testMessage]);
deleteMessageById(testMessageId);
expect(useMessagesStore.getState().messages).toStrictEqual([]);
});
it('deleteMessageByIdList', () => {
const { deleteMessageByIdList } = useMessagesStore.getState();
useMessagesStore.getState().addMessage(testMessage);
const errorIdList = ['not-exist'];
deleteMessageByIdList(errorIdList);
expect(useMessagesStore.getState().messages).toStrictEqual([testMessage]);
deleteMessageByIdList([testMessageId]);
expect(useMessagesStore.getState().messages).toStrictEqual([]);
});
it('setGroupMessageList', () => {
const { setGroupMessageList } = useMessagesStore.getState();
useMessagesStore.getState().addMessage(testMessage);
const messageGroup = {
groupId: testReplyId,
memberSet: {
userMessageId: '',
llmAnswerMessageIdList: [testMessageId],
functionCallMessageIdList: [],
followUpMessageIdList: [],
},
sectionId: '',
showContextDivider: 'with-onboarding' as const,
showSuggestions: false,
isLatest: true,
};
setGroupMessageList([messageGroup]);
expect(useMessagesStore.getState().messageGroupList).toStrictEqual([
messageGroup,
]);
});
it('getMessageGroupById', () => {
const { getMessageGroupById } = useMessagesStore.getState();
useMessagesStore.getState().addMessage(testMessage);
const messageGroup = {
groupId: testReplyId,
memberSet: {
userMessageId: '',
llmAnswerMessageIdList: [testMessageId],
functionCallMessageIdList: [],
followUpMessageIdList: [],
},
sectionId: '',
showContextDivider: 'with-onboarding' as const,
showSuggestions: false,
isLatest: true,
};
useMessagesStore.getState().setGroupMessageList([messageGroup]);
expect(getMessageGroupById(testReplyId)).toStrictEqual(messageGroup);
});
it('getMessageGroupByUserMessageId', () => {
const { getMessageGroupByUserMessageId } = useMessagesStore.getState();
useMessagesStore.getState().addMessage(testMessage);
const messageGroup = {
groupId: testReplyId,
memberSet: {
userMessageId: testReplyId,
llmAnswerMessageIdList: [testMessageId],
functionCallMessageIdList: [],
followUpMessageIdList: [],
},
sectionId: '',
showContextDivider: 'with-onboarding' as const,
showSuggestions: false,
isLatest: true,
};
useMessagesStore.getState().setGroupMessageList([messageGroup]);
expect(getMessageGroupByUserMessageId(testReplyId)).toStrictEqual(
messageGroup,
);
});
it('isLastMessageGroup', () => {
const { isLastMessageGroup } = useMessagesStore.getState();
useMessagesStore.getState().addMessage(testMessage);
const messageGroup = {
groupId: testReplyId,
memberSet: {
userMessageId: '',
llmAnswerMessageIdList: [testMessageId],
functionCallMessageIdList: [],
followUpMessageIdList: [],
},
sectionId: '',
showContextDivider: 'with-onboarding' as const,
showSuggestions: false,
isLatest: true,
};
useMessagesStore.getState().setGroupMessageList([messageGroup]);
expect(isLastMessageGroup(testReplyId)).toBeTruthy();
});
it('clearMessage', () => {
const { clearMessage } = useMessagesStore.getState();
useMessagesStore.getState().addMessage(testMessage);
clearMessage();
expect(useMessagesStore.getState().messages).toStrictEqual([]);
});
});