@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.
524 lines (429 loc) • 18.9 kB
text/typescript
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { DBModel } from '@/database/_deprecated/core/types/db';
import { CreateMessageParams, MessageModel } from '@/database/_deprecated/models/message';
import { DB_Message } from '@/database/_deprecated/schemas/message';
import { DB_Topic } from '@/database/_deprecated/schemas/topic';
import { nanoid } from '@/utils/uuid';
import * as uuidUtils from '@/utils/uuid';
import { CreateTopicParams, QueryTopicParams, TopicModel } from '../topic';
describe('TopicModel', () => {
let topicData: CreateTopicParams;
const currentSessionId = 'session1';
beforeEach(() => {
// Set up topic data with the correct structure
topicData = {
sessionId: currentSessionId,
title: 'Test Topic',
favorite: false,
};
});
afterEach(async () => {
// Clean up the database after each test
await TopicModel.clearTable();
});
describe('create', () => {
it('should create a topic record', async () => {
const result = await TopicModel.create(topicData);
expect(result).toHaveProperty('id');
// Verify that the topic has been added to the database
const topicInDb = await TopicModel.findById(result.id);
expect(topicInDb).toEqual(
expect.objectContaining({
title: topicData.title,
favorite: topicData.favorite ? 1 : 0,
sessionId: topicData.sessionId,
}),
);
});
it('should create a topic with favorite set to true', async () => {
const favoriteTopicData: CreateTopicParams = {
...topicData,
favorite: true,
};
const result = await TopicModel.create(favoriteTopicData);
expect(result).toHaveProperty('id');
const topicInDb = await TopicModel.findById(result.id);
expect(topicInDb).toEqual(
expect.objectContaining({
title: favoriteTopicData.title,
favorite: 1,
sessionId: favoriteTopicData.sessionId,
}),
);
});
it('should update messages with the new topic id when messages are provided', async () => {
const messagesToUpdate = [nanoid(), nanoid()];
// 假设这些消息存在于数据库中
for (const messageId of messagesToUpdate) {
await MessageModel.table.add({ id: messageId, text: 'Sample message', topicId: null });
}
const topicDataWithMessages = {
...topicData,
messages: messagesToUpdate,
};
const topic = await TopicModel.create(topicDataWithMessages);
expect(topic).toHaveProperty('id');
// 验证数据库中的消息是否已更新
const updatedMessages: DB_Message[] = await MessageModel.table
.where('id')
.anyOf(messagesToUpdate)
.toArray();
expect(updatedMessages).toHaveLength(messagesToUpdate.length);
for (const message of updatedMessages) {
expect(message.topicId).toEqual(topic.id);
}
});
it('should create a topic with a unique id when no id is provided', async () => {
const spy = vi.spyOn(uuidUtils, 'nanoid'); // 使用 Vitest 的 spy 功能来监视 nanoid 调用
const result = await TopicModel.create(topicData);
expect(spy).toHaveBeenCalled(); // 验证 nanoid 被调用来生成 id
expect(result).toHaveProperty('id');
expect(typeof result.id).toBe('string');
spy.mockRestore(); // 测试结束后恢复原始行为
});
});
describe('batch create', () => {
it('should batch create topic records', async () => {
const topicsToCreate = [topicData, topicData];
const results = await TopicModel.batchCreate(topicsToCreate);
expect(results.ids).toHaveLength(topicsToCreate.length);
// Verify that the topics have been added to the database
for (const result of results.ids!) {
const topicInDb = await TopicModel.findById(result);
expect(topicInDb).toEqual(
expect.objectContaining({
title: topicData.title,
favorite: topicData.favorite ? 1 : 0,
sessionId: topicData.sessionId,
}),
);
}
});
it('should batch create topics with mixed favorite values', async () => {
const mixedTopicsData: CreateTopicParams[] = [
{ ...topicData, favorite: true },
{ ...topicData, favorite: false },
];
const results = await TopicModel.batchCreate(mixedTopicsData);
expect(results.ids).toHaveLength(mixedTopicsData.length);
for (const id of results.ids!) {
const topicInDb = await TopicModel.findById(id);
expect(topicInDb).toBeDefined();
expect(topicInDb.favorite).toBeGreaterThanOrEqual(0);
expect(topicInDb.favorite).toBeLessThanOrEqual(1);
}
});
});
it('should query topics with pagination', async () => {
// Create multiple topics to test the query method
await TopicModel.batchCreate([topicData, topicData]);
const queryParams: QueryTopicParams = { pageSize: 1, current: 0, sessionId: 'session1' };
const queriedTopics = await TopicModel.query(queryParams);
expect(queriedTopics).toHaveLength(1);
});
it('should find topics by session id', async () => {
// Create multiple topics to test the findBySessionId method
await TopicModel.batchCreate([topicData, topicData]);
const topicsBySessionId = await TopicModel.findBySessionId(topicData.sessionId);
expect(topicsBySessionId).toHaveLength(2);
expect(topicsBySessionId.every((i) => i.sessionId === topicData.sessionId)).toBeTruthy();
});
it('should delete a topic and its associated messages', async () => {
const createdTopic = await TopicModel.create(topicData);
await TopicModel.delete(createdTopic.id);
// Verify the topic and its related messages are deleted
const topicInDb = await TopicModel.findById(createdTopic.id);
expect(topicInDb).toBeUndefined();
// You need to verify that messages related to the topic are also deleted
// This would require additional setup to create messages associated with the topic
// and then assertions to check that they're deleted after the topic itself is deleted
});
it('should batch delete topics by session id', async () => {
// Create multiple topics to test the batchDeleteBySessionId method
await TopicModel.batchCreate([topicData, topicData]);
await TopicModel.batchDeleteBySessionId(topicData.sessionId);
// Verify that all topics with the given session id are deleted
const topicsInDb = await TopicModel.findBySessionId(topicData.sessionId);
expect(topicsInDb).toHaveLength(0);
});
it('should update a topic', async () => {
const createdTopic = await TopicModel.create(topicData);
const updateData = { title: 'New Title' };
await TopicModel.update(createdTopic.id, updateData);
const updatedTopic = await TopicModel.findById(createdTopic.id);
expect(updatedTopic).toHaveProperty('title', 'New Title');
});
describe('toggleFavorite', () => {
it('should toggle favorite status of a topic', async () => {
const createdTopic = await TopicModel.create(topicData);
const newState = await TopicModel.toggleFavorite(createdTopic.id);
expect(newState).toBe(true);
const topicInDb = await TopicModel.findById(createdTopic.id);
expect(topicInDb).toHaveProperty('favorite', 1);
});
it('should handle toggleFavorite when topic does not exist', async () => {
const nonExistentTopicId = 'non-existent-id';
await expect(TopicModel.toggleFavorite(nonExistentTopicId)).rejects.toThrow(
`Topic with id ${nonExistentTopicId} not found`,
);
});
it('should set favorite to specific state using toggleFavorite', async () => {
const createdTopic = await TopicModel.create(topicData);
// Set favorite to true regardless of current state
await TopicModel.toggleFavorite(createdTopic.id, true);
let topicInDb = await TopicModel.findById(createdTopic.id);
expect(topicInDb.favorite).toBe(1);
// Set favorite to false regardless of current state
await TopicModel.toggleFavorite(createdTopic.id, false);
topicInDb = await TopicModel.findById(createdTopic.id);
expect(topicInDb.favorite).toBe(0);
});
});
it('should delete a topic and its associated messages', async () => {
// 创建话题和相关联的消息
const createdTopic = await TopicModel.create(topicData);
const messageData: CreateMessageParams = {
content: 'Test Message',
topicId: createdTopic.id,
sessionId: topicData.sessionId,
role: 'user',
};
await MessageModel.create(messageData);
// 删除话题
await TopicModel.delete(createdTopic.id);
// 验证话题是否被删除
const topicInDb = await TopicModel.findById(createdTopic.id);
expect(topicInDb).toBeUndefined();
// 验证与话题关联的消息是否也被删除
const messagesInDb = await MessageModel.query({
sessionId: topicData.sessionId,
topicId: createdTopic.id,
});
expect(messagesInDb).toHaveLength(0);
});
it('should batch delete topics and their associated messages', async () => {
// 创建多个话题和相关联的消息
const createdTopic1 = await TopicModel.create(topicData);
const createdTopic2 = await TopicModel.create(topicData);
const messageData1: CreateMessageParams = {
content: 'Test Message 1',
topicId: createdTopic1.id,
sessionId: topicData.sessionId,
role: 'user',
};
const messageData2: CreateMessageParams = {
content: 'Test Message 2',
topicId: createdTopic2.id,
sessionId: topicData.sessionId,
role: 'user',
};
await MessageModel.create(messageData1);
await MessageModel.create(messageData2);
// 执行批量删除
await TopicModel.batchDelete([createdTopic1.id, createdTopic2.id]);
// 验证话题是否被删除
const topicInDb1 = await TopicModel.findById(createdTopic1.id);
const topicInDb2 = await TopicModel.findById(createdTopic2.id);
expect(topicInDb1).toBeUndefined();
expect(topicInDb2).toBeUndefined();
// 验证与话题关联的消息是否也被删除
const messagesInDb1 = await MessageModel.query({
sessionId: topicData.sessionId,
topicId: createdTopic1.id,
});
const messagesInDb2 = await MessageModel.query({
sessionId: topicData.sessionId,
topicId: createdTopic2.id,
});
expect(messagesInDb1).toHaveLength(0);
expect(messagesInDb2).toHaveLength(0);
});
describe('duplicateTopic', () => {
let originalTopic: DBModel<DB_Topic>;
let originalMessages: any[];
beforeEach(async () => {
// 创建一个原始主题
const { id } = await TopicModel.create({
title: 'Original Topic',
sessionId: 'session1',
favorite: false,
});
originalTopic = await TopicModel.findById(id);
// 创建一些关联到原始主题的消息
originalMessages = await Promise.all(
['Message 1', 'Message 2'].map((text) =>
MessageModel.create({
content: text,
topicId: originalTopic.id,
sessionId: originalTopic.sessionId!,
role: 'user',
}),
),
);
});
afterEach(async () => {
// 清理数据库中的所有主题和消息
await TopicModel.clearTable();
await MessageModel.clearTable();
});
it('should duplicate a topic with all associated messages', async () => {
// 执行复制操作
await TopicModel.duplicateTopic(originalTopic.id);
// 验证复制后的主题是否存在
const duplicatedTopic = await TopicModel.findBySessionId(originalTopic.sessionId!);
expect(duplicatedTopic).toHaveLength(2);
// 验证复制后的消息是否存在
const duplicatedMessages = await MessageModel.query({
sessionId: originalTopic.sessionId!,
topicId: duplicatedTopic[1].id, // 假设复制的主题是第二个
});
expect(duplicatedMessages).toHaveLength(originalMessages.length);
});
it('should throw an error if the topic does not exist', async () => {
// 尝试复制一个不存在的主题
const nonExistentTopicId = nanoid();
await expect(TopicModel.duplicateTopic(nonExistentTopicId)).rejects.toThrow(
`Topic with id ${nonExistentTopicId} not found`,
);
});
it('should preserve the properties of the duplicated topic', async () => {
// 执行复制操作
await TopicModel.duplicateTopic(originalTopic.id);
// 获取复制的主题
const topics = await TopicModel.findBySessionId(originalTopic.sessionId!);
const duplicatedTopic = topics.find((topic) => topic.id !== originalTopic.id);
// 验证复制的主题是否保留了原始主题的属性
expect(duplicatedTopic).toBeDefined();
expect(duplicatedTopic).toMatchObject({
title: originalTopic.title,
favorite: originalTopic.favorite,
sessionId: originalTopic.sessionId,
});
// 确保生成了新的 ID
expect(duplicatedTopic.id).not.toBe(originalTopic.id);
});
it('should properly handle the messages hierarchy when duplicating', async () => {
// 创建一个子消息关联到其中一个原始消息
const { id } = await MessageModel.create({
content: 'Child Message',
topicId: originalTopic.id,
parentId: originalMessages[0].id,
sessionId: originalTopic.sessionId!,
role: 'user',
});
const childMessage = await MessageModel.findById(id);
// 执行复制操作
await TopicModel.duplicateTopic(originalTopic.id);
// 获取复制的消息
const duplicatedMessages = await MessageModel.queryBySessionId(originalTopic.sessionId!);
// 验证复制的子消息是否存在并且 parentId 已更新
const duplicatedChildMessage = duplicatedMessages.find(
(message) => message.content === childMessage.content && message.id !== childMessage.id,
);
expect(duplicatedChildMessage).toBeDefined();
expect(duplicatedChildMessage.parentId).not.toBe(childMessage.parentId);
expect(duplicatedChildMessage.parentId).toBeDefined();
});
it('should fail if the database transaction fails', async () => {
// 强制数据库事务失败,例如通过在复制过程中抛出异常
const dbTransactionFailedError = new Error('DB transaction failed');
const spyOn = vi.spyOn(TopicModel['db'], 'transaction').mockImplementation((async () => {
throw dbTransactionFailedError;
}) as any);
// 尝试复制主题并捕捉期望的错误
await expect(TopicModel.duplicateTopic(originalTopic.id)).rejects.toThrow(
dbTransactionFailedError,
);
spyOn.mockRestore();
});
it('should not create partial duplicates if the process fails at some point', async () => {
// 假设复制消息的过程中发生了错误
vi.spyOn(MessageModel, 'duplicateMessages').mockImplementation(async () => {
throw new Error('Failed to duplicate messages');
});
// 尝试复制主题,期望会抛出错误
await expect(TopicModel.duplicateTopic(originalTopic.id)).rejects.toThrow();
// 确保没有创建任何副本
const topics = await TopicModel.findBySessionId(originalTopic.sessionId!);
expect(topics).toHaveLength(1); // 只有原始主题
const messages = await MessageModel.queryBySessionId(originalTopic.sessionId!);
expect(messages).toHaveLength(originalMessages.length); // 只有原始消息
});
});
describe('clearTable', () => {
it('should clear the table', async () => {
// Create a topic to ensure the table is not empty
await TopicModel.create(topicData);
// Clear the table
await TopicModel.clearTable();
// Verify the table is empty
const topics = await TopicModel.queryAll();
expect(topics).toHaveLength(0);
});
});
describe('update', () => {
it('should update a topic', async () => {
// Create a topic
const createdTopic = await TopicModel.create(topicData);
// Update the topic
const newTitle = 'Updated Title';
await TopicModel.update(createdTopic.id, { title: newTitle });
// Verify the topic is updated
const updatedTopic = await TopicModel.findById(createdTopic.id);
expect(updatedTopic.title).toBe(newTitle);
});
});
describe('batchDelete', () => {
it('should batch delete topics', async () => {
// Create multiple topics
const topic1 = await TopicModel.create(topicData);
const topic2 = await TopicModel.create(topicData);
await TopicModel.create(topicData);
const ids = [topic1.id, topic2.id];
// Batch delete the topics
await TopicModel.batchDelete(ids);
expect(await TopicModel.table.count()).toEqual(1);
});
});
describe('queryAll', () => {
it('should query all topics', async () => {
// Create multiple topics
await TopicModel.batchCreate([topicData, topicData]);
// Query all topics
const topics = await TopicModel.queryAll();
// Verify all topics are queried
expect(topics).toHaveLength(2);
});
});
describe('queryByKeyword', () => {
it('should query global topics by keyword', async () => {
// Create a topic with a unique title
const uniqueTitle = 'Unique Title';
await TopicModel.create({ ...topicData, title: uniqueTitle });
// Query topics by the unique title
const topics = await TopicModel.queryByKeyword(uniqueTitle);
// Verify the correct topic is queried
expect(topics).toHaveLength(1);
expect(topics[0].title).toBe(uniqueTitle);
});
it('should query topics in current session by keyword', async () => {
// Create a topic with a unique title
const uniqueTitle = 'Unique Title';
await TopicModel.create({ ...topicData, title: uniqueTitle });
// Query topics by the unique title
const topics = await TopicModel.queryByKeyword(uniqueTitle, currentSessionId);
// Verify the correct topic is queried
expect(topics).toHaveLength(1);
expect(topics[0].title).toBe(uniqueTitle);
});
it('should not query any topic in other session by keyword', async () => {
// Create a topic with a unique title
const uniqueTitle = 'Unique Title';
await TopicModel.create({ ...topicData, title: uniqueTitle });
// Query topics by the unique title
const topics = await TopicModel.queryByKeyword(uniqueTitle, 'session-id-2');
// Verify the correct topic is queried
expect(topics).toHaveLength(0);
});
});
});