@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.
260 lines (216 loc) • 8.46 kB
text/typescript
// @vitest-environment node
import { describe, expect, it, vi } from 'vitest';
import { MessageModel } from '@/database/models/message';
import { TopicModel } from '@/database/models/topic';
import { AiChatService } from '@/server/services/aiChat';
import { aiChatRouter } from '../aiChat';
vi.mock('@/database/models/message');
vi.mock('@/database/models/topic');
vi.mock('@/server/services/aiChat');
vi.mock('@/server/services/file', () => ({
FileService: vi.fn(),
}));
vi.mock('@/utils/server', () => ({
getXorPayload: vi.fn(),
}));
vi.mock('@/server/modules/ModelRuntime', () => ({
initModelRuntimeWithUserPayload: vi.fn(),
}));
describe('aiChatRouter', () => {
const mockCtx = { userId: 'u1' };
it('should create topic optionally, create user/assistant messages, and return payload', async () => {
const mockCreateTopic = vi.fn().mockResolvedValue({ id: 't1' });
const mockCreateMessage = vi
.fn()
.mockResolvedValueOnce({ id: 'm-user' })
.mockResolvedValueOnce({ id: 'm-assistant' });
const mockGet = vi
.fn()
.mockResolvedValue({ messages: [{ id: 'm-user' }, { id: 'm-assistant' }], topics: [{}] });
vi.mocked(TopicModel).mockImplementation(() => ({ create: mockCreateTopic }) as any);
vi.mocked(MessageModel).mockImplementation(() => ({ create: mockCreateMessage }) as any);
vi.mocked(AiChatService).mockImplementation(() => ({ getMessagesAndTopics: mockGet }) as any);
const caller = aiChatRouter.createCaller(mockCtx as any);
const input = {
newAssistantMessage: { model: 'gpt-4o', provider: 'openai' },
newTopic: { title: 'T', topicMessageIds: ['a', 'b'] },
newUserMessage: { content: 'hi', files: ['f1'] },
sessionId: 's1',
} as any;
const res = await caller.sendMessageInServer(input);
expect(mockCreateTopic).toHaveBeenCalledWith({
messages: ['a', 'b'],
sessionId: 's1',
title: 'T',
});
expect(mockCreateMessage).toHaveBeenNthCalledWith(1, {
content: 'hi',
files: ['f1'],
role: 'user',
sessionId: 's1',
topicId: 't1',
});
expect(mockCreateMessage).toHaveBeenNthCalledWith(
2,
expect.objectContaining({
content: expect.any(String),
fromModel: 'gpt-4o',
parentId: 'm-user',
role: 'assistant',
sessionId: 's1',
topicId: 't1',
}),
);
expect(mockGet).toHaveBeenCalledWith({ includeTopic: true, sessionId: 's1', topicId: 't1' });
expect(res.assistantMessageId).toBe('m-assistant');
expect(res.userMessageId).toBe('m-user');
expect(res.isCreateNewTopic).toBe(true);
expect(res.topicId).toBe('t1');
expect(res.messages?.length).toBe(2);
expect(res.topics?.length).toBe(1);
});
it('should reuse existing topic when topicId provided', async () => {
const mockCreateMessage = vi
.fn()
.mockResolvedValueOnce({ id: 'm-user' })
.mockResolvedValueOnce({ id: 'm-assistant' });
const mockGet = vi.fn().mockResolvedValue({ messages: [], topics: undefined });
vi.mocked(MessageModel).mockImplementation(() => ({ create: mockCreateMessage }) as any);
vi.mocked(AiChatService).mockImplementation(() => ({ getMessagesAndTopics: mockGet }) as any);
const caller = aiChatRouter.createCaller(mockCtx as any);
const res = await caller.sendMessageInServer({
newAssistantMessage: { model: 'gpt-4o', provider: 'openai' },
newUserMessage: { content: 'hi' },
sessionId: 's1',
topicId: 't-exist',
} as any);
expect(mockCreateMessage).toHaveBeenCalled();
expect(mockGet).toHaveBeenCalledWith({
includeTopic: false,
sessionId: 's1',
topicId: 't-exist',
});
expect(res.isCreateNewTopic).toBe(false);
expect(res.topicId).toBe('t-exist');
});
it('should pass threadId to both user and assistant messages when provided', async () => {
const mockCreateMessage = vi
.fn()
.mockResolvedValueOnce({ id: 'm-user' })
.mockResolvedValueOnce({ id: 'm-assistant' });
const mockGet = vi.fn().mockResolvedValue({ messages: [], topics: undefined });
vi.mocked(MessageModel).mockImplementation(() => ({ create: mockCreateMessage }) as any);
vi.mocked(AiChatService).mockImplementation(() => ({ getMessagesAndTopics: mockGet }) as any);
const caller = aiChatRouter.createCaller(mockCtx as any);
await caller.sendMessageInServer({
newAssistantMessage: { model: 'gpt-4o', provider: 'openai' },
newUserMessage: { content: 'hi' },
sessionId: 's1',
threadId: 'thread-123',
topicId: 't1',
} as any);
expect(mockCreateMessage).toHaveBeenNthCalledWith(1, {
content: 'hi',
role: 'user',
sessionId: 's1',
threadId: 'thread-123',
topicId: 't1',
});
expect(mockCreateMessage).toHaveBeenNthCalledWith(
2,
expect.objectContaining({
parentId: 'm-user',
role: 'assistant',
sessionId: 's1',
threadId: 'thread-123',
topicId: 't1',
}),
);
});
describe('outputJSON', () => {
it('should successfully generate structured output', async () => {
const { getXorPayload } = await import('@/utils/server');
const { initModelRuntimeWithUserPayload } = await import('@/server/modules/ModelRuntime');
const mockPayload = { apiKey: 'test-key' };
const mockResult = { object: { name: 'John', age: 30 } };
const mockGenerateObject = vi.fn().mockResolvedValue(mockResult);
vi.mocked(getXorPayload).mockReturnValue(mockPayload);
vi.mocked(initModelRuntimeWithUserPayload).mockReturnValue({
generateObject: mockGenerateObject,
} as any);
const caller = aiChatRouter.createCaller(mockCtx as any);
const input = {
keyVaultsPayload: 'encrypted-payload',
messages: [{ content: 'test', role: 'user' }],
model: 'gpt-4o',
provider: 'openai',
schema: {
name: 'Person',
schema: {
type: 'object' as const,
properties: { name: { type: 'string' }, age: { type: 'number' } },
},
},
};
const result = await caller.outputJSON(input);
expect(getXorPayload).toHaveBeenCalledWith('encrypted-payload');
expect(initModelRuntimeWithUserPayload).toHaveBeenCalledWith('openai', mockPayload);
expect(mockGenerateObject).toHaveBeenCalledWith({
messages: input.messages,
model: 'gpt-4o',
schema: input.schema,
tools: undefined,
});
expect(result).toEqual(mockResult);
});
it('should throw error when keyVaultsPayload is invalid', async () => {
const { getXorPayload } = await import('@/utils/server');
vi.mocked(getXorPayload).mockReturnValue(undefined as any);
const caller = aiChatRouter.createCaller(mockCtx as any);
const input = {
keyVaultsPayload: 'invalid-payload',
messages: [],
model: 'gpt-4o',
provider: 'openai',
};
await expect(caller.outputJSON(input)).rejects.toThrow('keyVaultsPayload is not correct');
});
it('should handle tools parameter when provided', async () => {
const { getXorPayload } = await import('@/utils/server');
const { initModelRuntimeWithUserPayload } = await import('@/server/modules/ModelRuntime');
const mockPayload = { apiKey: 'test-key' };
const mockTools = [
{
type: 'function' as const,
function: {
name: 'test',
parameters: {
type: 'object' as const,
properties: { input: { type: 'string' } },
},
},
},
];
const mockGenerateObject = vi.fn().mockResolvedValue({ object: {} });
vi.mocked(getXorPayload).mockReturnValue(mockPayload);
vi.mocked(initModelRuntimeWithUserPayload).mockReturnValue({
generateObject: mockGenerateObject,
} as any);
const caller = aiChatRouter.createCaller(mockCtx as any);
const input = {
keyVaultsPayload: 'encrypted-payload',
messages: [],
model: 'gpt-4o',
provider: 'openai',
tools: mockTools,
};
await caller.outputJSON(input);
expect(mockGenerateObject).toHaveBeenCalledWith({
messages: [],
model: 'gpt-4o',
schema: undefined,
tools: mockTools,
});
});
});
});