| | const { Constants } = require('librechat-data-provider'); |
| | const { initializeFakeClient } = require('./FakeClient'); |
| |
|
| | jest.mock('~/db/connect'); |
| | jest.mock('~/server/services/Config', () => ({ |
| | getAppConfig: jest.fn().mockResolvedValue({ |
| | |
| | paths: { uploads: '/tmp' }, |
| | fileStrategy: 'local', |
| | memory: { disabled: false }, |
| | }), |
| | })); |
| | jest.mock('~/models', () => ({ |
| | User: jest.fn(), |
| | Key: jest.fn(), |
| | Session: jest.fn(), |
| | Balance: jest.fn(), |
| | Transaction: jest.fn(), |
| | getMessages: jest.fn().mockResolvedValue([]), |
| | saveMessage: jest.fn(), |
| | updateMessage: jest.fn(), |
| | deleteMessagesSince: jest.fn(), |
| | deleteMessages: jest.fn(), |
| | getConvoTitle: jest.fn(), |
| | getConvo: jest.fn(), |
| | saveConvo: jest.fn(), |
| | deleteConvos: jest.fn(), |
| | getPreset: jest.fn(), |
| | getPresets: jest.fn(), |
| | savePreset: jest.fn(), |
| | deletePresets: jest.fn(), |
| | findFileById: jest.fn(), |
| | createFile: jest.fn(), |
| | updateFile: jest.fn(), |
| | deleteFile: jest.fn(), |
| | deleteFiles: jest.fn(), |
| | getFiles: jest.fn(), |
| | updateFileUsage: jest.fn(), |
| | })); |
| |
|
| | const { getConvo, saveConvo } = require('~/models'); |
| |
|
| | jest.mock('@librechat/agents', () => { |
| | const { Providers } = jest.requireActual('@librechat/agents'); |
| | return { |
| | Providers, |
| | ChatOpenAI: jest.fn().mockImplementation(() => { |
| | return {}; |
| | }), |
| | }; |
| | }); |
| |
|
| | let parentMessageId; |
| | let conversationId; |
| | const fakeMessages = []; |
| | const userMessage = 'Hello, ChatGPT!'; |
| | const apiKey = 'fake-api-key'; |
| |
|
| | const messageHistory = [ |
| | { role: 'user', isCreatedByUser: true, text: 'Hello', messageId: '1' }, |
| | { role: 'assistant', isCreatedByUser: false, text: 'Hi', messageId: '2', parentMessageId: '1' }, |
| | { |
| | role: 'user', |
| | isCreatedByUser: true, |
| | text: "What's up", |
| | messageId: '3', |
| | parentMessageId: '2', |
| | }, |
| | ]; |
| |
|
| | describe('BaseClient', () => { |
| | let TestClient; |
| | const options = { |
| | |
| | modelOptions: { |
| | model: 'gpt-4o-mini', |
| | temperature: 0, |
| | }, |
| | }; |
| |
|
| | beforeEach(() => { |
| | TestClient = initializeFakeClient(apiKey, options, fakeMessages); |
| | TestClient.summarizeMessages = jest.fn().mockResolvedValue({ |
| | summaryMessage: { |
| | role: 'system', |
| | content: 'Refined answer', |
| | }, |
| | summaryTokenCount: 5, |
| | }); |
| | }); |
| |
|
| | test('returns the input messages without instructions when addInstructions() is called with empty instructions', () => { |
| | const messages = [{ content: 'Hello' }, { content: 'How are you?' }, { content: 'Goodbye' }]; |
| | const instructions = ''; |
| | const result = TestClient.addInstructions(messages, instructions); |
| | expect(result).toEqual(messages); |
| | }); |
| |
|
| | test('returns the input messages with instructions properly added when addInstructions() is called with non-empty instructions', () => { |
| | const messages = [{ content: 'Hello' }, { content: 'How are you?' }, { content: 'Goodbye' }]; |
| | const instructions = { content: 'Please respond to the question.' }; |
| | const result = TestClient.addInstructions(messages, instructions); |
| | const expected = [ |
| | { content: 'Please respond to the question.' }, |
| | { content: 'Hello' }, |
| | { content: 'How are you?' }, |
| | { content: 'Goodbye' }, |
| | ]; |
| | expect(result).toEqual(expected); |
| | }); |
| |
|
| | test('returns the input messages with instructions properly added when addInstructions() with legacy flag', () => { |
| | const messages = [{ content: 'Hello' }, { content: 'How are you?' }, { content: 'Goodbye' }]; |
| | const instructions = { content: 'Please respond to the question.' }; |
| | const result = TestClient.addInstructions(messages, instructions, true); |
| | const expected = [ |
| | { content: 'Hello' }, |
| | { content: 'How are you?' }, |
| | { content: 'Please respond to the question.' }, |
| | { content: 'Goodbye' }, |
| | ]; |
| | expect(result).toEqual(expected); |
| | }); |
| |
|
| | test('concats messages correctly in concatenateMessages()', () => { |
| | const messages = [ |
| | { name: 'User', content: 'Hello' }, |
| | { name: 'Assistant', content: 'How can I help you?' }, |
| | { name: 'User', content: 'I have a question.' }, |
| | ]; |
| | const result = TestClient.concatenateMessages(messages); |
| | const expected = |
| | 'User:\nHello\n\nAssistant:\nHow can I help you?\n\nUser:\nI have a question.\n\n'; |
| | expect(result).toBe(expected); |
| | }); |
| |
|
| | test('refines messages correctly in summarizeMessages()', async () => { |
| | const messagesToRefine = [ |
| | { role: 'user', content: 'Hello', tokenCount: 10 }, |
| | { role: 'assistant', content: 'How can I help you?', tokenCount: 20 }, |
| | ]; |
| | const remainingContextTokens = 100; |
| | const expectedRefinedMessage = { |
| | role: 'system', |
| | content: 'Refined answer', |
| | }; |
| |
|
| | const result = await TestClient.summarizeMessages({ messagesToRefine, remainingContextTokens }); |
| | expect(result.summaryMessage).toEqual(expectedRefinedMessage); |
| | }); |
| |
|
| | test('gets messages within token limit (under limit) correctly in getMessagesWithinTokenLimit()', async () => { |
| | TestClient.maxContextTokens = 100; |
| | TestClient.shouldSummarize = true; |
| |
|
| | const messages = [ |
| | { role: 'user', content: 'Hello', tokenCount: 5 }, |
| | { role: 'assistant', content: 'How can I help you?', tokenCount: 19 }, |
| | { role: 'user', content: 'I have a question.', tokenCount: 18 }, |
| | ]; |
| | const expectedContext = [ |
| | { role: 'user', content: 'Hello', tokenCount: 5 }, |
| | { role: 'assistant', content: 'How can I help you?', tokenCount: 19 }, |
| | { role: 'user', content: 'I have a question.', tokenCount: 18 }, |
| | ]; |
| | |
| | const expectedRemainingContextTokens = 58 - 3; |
| | const expectedMessagesToRefine = []; |
| |
|
| | const lastExpectedMessage = |
| | expectedMessagesToRefine?.[expectedMessagesToRefine.length - 1] ?? {}; |
| | const expectedIndex = messages.findIndex((msg) => msg.content === lastExpectedMessage?.content); |
| |
|
| | const result = await TestClient.getMessagesWithinTokenLimit({ messages }); |
| |
|
| | expect(result.context).toEqual(expectedContext); |
| | expect(result.messagesToRefine.length - 1).toEqual(expectedIndex); |
| | expect(result.remainingContextTokens).toBe(expectedRemainingContextTokens); |
| | expect(result.messagesToRefine).toEqual(expectedMessagesToRefine); |
| | }); |
| |
|
| | test('gets result over token limit correctly in getMessagesWithinTokenLimit()', async () => { |
| | TestClient.maxContextTokens = 50; |
| | TestClient.shouldSummarize = true; |
| |
|
| | const messages = [ |
| | { role: 'user', content: 'Hello', tokenCount: 30 }, |
| | { role: 'assistant', content: 'How can I help you?', tokenCount: 30 }, |
| | { role: 'user', content: 'I have a question.', tokenCount: 5 }, |
| | { role: 'user', content: 'I need a coffee, stat!', tokenCount: 19 }, |
| | { role: 'assistant', content: 'Sure, I can help with that.', tokenCount: 18 }, |
| | ]; |
| |
|
| | |
| | const expectedRemainingContextTokens = 5; |
| | const expectedMessagesToRefine = [ |
| | { role: 'user', content: 'Hello', tokenCount: 30 }, |
| | { role: 'assistant', content: 'How can I help you?', tokenCount: 30 }, |
| | ]; |
| | const expectedContext = [ |
| | { role: 'user', content: 'I have a question.', tokenCount: 5 }, |
| | { role: 'user', content: 'I need a coffee, stat!', tokenCount: 19 }, |
| | { role: 'assistant', content: 'Sure, I can help with that.', tokenCount: 18 }, |
| | ]; |
| |
|
| | const lastExpectedMessage = |
| | expectedMessagesToRefine?.[expectedMessagesToRefine.length - 1] ?? {}; |
| | const expectedIndex = messages.findIndex((msg) => msg.content === lastExpectedMessage?.content); |
| |
|
| | const result = await TestClient.getMessagesWithinTokenLimit({ messages }); |
| |
|
| | expect(result.context).toEqual(expectedContext); |
| | expect(result.messagesToRefine.length - 1).toEqual(expectedIndex); |
| | expect(result.remainingContextTokens).toBe(expectedRemainingContextTokens); |
| | expect(result.messagesToRefine).toEqual(expectedMessagesToRefine); |
| | }); |
| |
|
| | describe('getMessagesForConversation', () => { |
| | it('should return an empty array if the parentMessageId does not exist', () => { |
| | const result = TestClient.constructor.getMessagesForConversation({ |
| | messages: unorderedMessages, |
| | parentMessageId: '999', |
| | }); |
| | expect(result).toEqual([]); |
| | }); |
| |
|
| | it('should handle messages with messageId property', () => { |
| | const messagesWithMessageId = [ |
| | { messageId: '1', parentMessageId: null, text: 'Message 1' }, |
| | { messageId: '2', parentMessageId: '1', text: 'Message 2' }, |
| | ]; |
| | const result = TestClient.constructor.getMessagesForConversation({ |
| | messages: messagesWithMessageId, |
| | parentMessageId: '2', |
| | }); |
| | expect(result).toEqual([ |
| | { messageId: '1', parentMessageId: null, text: 'Message 1' }, |
| | { messageId: '2', parentMessageId: '1', text: 'Message 2' }, |
| | ]); |
| | }); |
| |
|
| | const messagesWithNullParent = [ |
| | { id: '1', parentMessageId: null, text: 'Message 1' }, |
| | { id: '2', parentMessageId: null, text: 'Message 2' }, |
| | ]; |
| |
|
| | it('should handle messages with null parentMessageId that are not root', () => { |
| | const result = TestClient.constructor.getMessagesForConversation({ |
| | messages: messagesWithNullParent, |
| | parentMessageId: '2', |
| | }); |
| | expect(result).toEqual([{ id: '2', parentMessageId: null, text: 'Message 2' }]); |
| | }); |
| |
|
| | const cyclicMessages = [ |
| | { id: '3', parentMessageId: '2', text: 'Message 3' }, |
| | { id: '1', parentMessageId: '3', text: 'Message 1' }, |
| | { id: '2', parentMessageId: '1', text: 'Message 2' }, |
| | ]; |
| |
|
| | it('should handle cyclic references without going into an infinite loop', () => { |
| | const result = TestClient.constructor.getMessagesForConversation({ |
| | messages: cyclicMessages, |
| | parentMessageId: '3', |
| | }); |
| | expect(result).toEqual([ |
| | { id: '1', parentMessageId: '3', text: 'Message 1' }, |
| | { id: '2', parentMessageId: '1', text: 'Message 2' }, |
| | { id: '3', parentMessageId: '2', text: 'Message 3' }, |
| | ]); |
| | }); |
| |
|
| | const unorderedMessages = [ |
| | { id: '3', parentMessageId: '2', text: 'Message 3' }, |
| | { id: '2', parentMessageId: '1', text: 'Message 2' }, |
| | { id: '1', parentMessageId: Constants.NO_PARENT, text: 'Message 1' }, |
| | ]; |
| |
|
| | it('should return ordered messages based on parentMessageId', () => { |
| | const result = TestClient.constructor.getMessagesForConversation({ |
| | messages: unorderedMessages, |
| | parentMessageId: '3', |
| | }); |
| | expect(result).toEqual([ |
| | { id: '1', parentMessageId: Constants.NO_PARENT, text: 'Message 1' }, |
| | { id: '2', parentMessageId: '1', text: 'Message 2' }, |
| | { id: '3', parentMessageId: '2', text: 'Message 3' }, |
| | ]); |
| | }); |
| |
|
| | const unorderedBranchedMessages = [ |
| | { id: '4', parentMessageId: '2', text: 'Message 4', summary: 'Summary for Message 4' }, |
| | { id: '10', parentMessageId: '7', text: 'Message 10' }, |
| | { id: '1', parentMessageId: null, text: 'Message 1' }, |
| | { id: '6', parentMessageId: '5', text: 'Message 7' }, |
| | { id: '7', parentMessageId: '5', text: 'Message 7' }, |
| | { id: '2', parentMessageId: '1', text: 'Message 2' }, |
| | { id: '8', parentMessageId: '6', text: 'Message 8' }, |
| | { id: '5', parentMessageId: '3', text: 'Message 5' }, |
| | { id: '3', parentMessageId: '1', text: 'Message 3' }, |
| | { id: '6', parentMessageId: '4', text: 'Message 6' }, |
| | { id: '8', parentMessageId: '7', text: 'Message 9' }, |
| | { id: '9', parentMessageId: '7', text: 'Message 9' }, |
| | { id: '11', parentMessageId: '2', text: 'Message 11', summary: 'Summary for Message 11' }, |
| | ]; |
| |
|
| | it('should return ordered messages from a branched array based on parentMessageId', () => { |
| | const result = TestClient.constructor.getMessagesForConversation({ |
| | messages: unorderedBranchedMessages, |
| | parentMessageId: '10', |
| | summary: true, |
| | }); |
| | expect(result).toEqual([ |
| | { id: '1', parentMessageId: null, text: 'Message 1' }, |
| | { id: '3', parentMessageId: '1', text: 'Message 3' }, |
| | { id: '5', parentMessageId: '3', text: 'Message 5' }, |
| | { id: '7', parentMessageId: '5', text: 'Message 7' }, |
| | { id: '10', parentMessageId: '7', text: 'Message 10' }, |
| | ]); |
| | }); |
| |
|
| | it('should return an empty array if no messages are provided', () => { |
| | const result = TestClient.constructor.getMessagesForConversation({ |
| | messages: [], |
| | parentMessageId: '3', |
| | }); |
| | expect(result).toEqual([]); |
| | }); |
| |
|
| | it('should map over the ordered messages if mapMethod is provided', () => { |
| | const mapMethod = (msg) => msg.text; |
| | const result = TestClient.constructor.getMessagesForConversation({ |
| | messages: unorderedMessages, |
| | parentMessageId: '3', |
| | mapMethod, |
| | }); |
| | expect(result).toEqual(['Message 1', 'Message 2', 'Message 3']); |
| | }); |
| |
|
| | let unorderedMessagesWithSummary = [ |
| | { id: '4', parentMessageId: '3', text: 'Message 4' }, |
| | { id: '2', parentMessageId: '1', text: 'Message 2', summary: 'Summary for Message 2' }, |
| | { id: '3', parentMessageId: '2', text: 'Message 3', summary: 'Summary for Message 3' }, |
| | { id: '1', parentMessageId: null, text: 'Message 1' }, |
| | ]; |
| |
|
| | it('should start with the message that has a summary property and continue until the specified parentMessageId', () => { |
| | const result = TestClient.constructor.getMessagesForConversation({ |
| | messages: unorderedMessagesWithSummary, |
| | parentMessageId: '4', |
| | summary: true, |
| | }); |
| | expect(result).toEqual([ |
| | { |
| | id: '3', |
| | parentMessageId: '2', |
| | role: 'system', |
| | text: 'Summary for Message 3', |
| | summary: 'Summary for Message 3', |
| | }, |
| | { id: '4', parentMessageId: '3', text: 'Message 4' }, |
| | ]); |
| | }); |
| |
|
| | it('should handle multiple summaries and return the branch from the latest to the parentMessageId', () => { |
| | unorderedMessagesWithSummary = [ |
| | { id: '5', parentMessageId: '4', text: 'Message 5' }, |
| | { id: '2', parentMessageId: '1', text: 'Message 2', summary: 'Summary for Message 2' }, |
| | { id: '3', parentMessageId: '2', text: 'Message 3', summary: 'Summary for Message 3' }, |
| | { id: '4', parentMessageId: '3', text: 'Message 4', summary: 'Summary for Message 4' }, |
| | { id: '1', parentMessageId: null, text: 'Message 1' }, |
| | ]; |
| | const result = TestClient.constructor.getMessagesForConversation({ |
| | messages: unorderedMessagesWithSummary, |
| | parentMessageId: '5', |
| | summary: true, |
| | }); |
| | expect(result).toEqual([ |
| | { |
| | id: '4', |
| | parentMessageId: '3', |
| | role: 'system', |
| | text: 'Summary for Message 4', |
| | summary: 'Summary for Message 4', |
| | }, |
| | { id: '5', parentMessageId: '4', text: 'Message 5' }, |
| | ]); |
| | }); |
| |
|
| | it('should handle summary at root edge case and continue until the parentMessageId', () => { |
| | unorderedMessagesWithSummary = [ |
| | { id: '5', parentMessageId: '4', text: 'Message 5' }, |
| | { id: '1', parentMessageId: null, text: 'Message 1', summary: 'Summary for Message 1' }, |
| | { id: '4', parentMessageId: '3', text: 'Message 4', summary: 'Summary for Message 4' }, |
| | { id: '2', parentMessageId: '1', text: 'Message 2', summary: 'Summary for Message 2' }, |
| | { id: '3', parentMessageId: '2', text: 'Message 3', summary: 'Summary for Message 3' }, |
| | ]; |
| | const result = TestClient.constructor.getMessagesForConversation({ |
| | messages: unorderedMessagesWithSummary, |
| | parentMessageId: '5', |
| | summary: true, |
| | }); |
| | expect(result).toEqual([ |
| | { |
| | id: '4', |
| | parentMessageId: '3', |
| | role: 'system', |
| | text: 'Summary for Message 4', |
| | summary: 'Summary for Message 4', |
| | }, |
| | { id: '5', parentMessageId: '4', text: 'Message 5' }, |
| | ]); |
| | }); |
| | }); |
| |
|
| | describe('sendMessage', () => { |
| | test('sendMessage should return a response message', async () => { |
| | const expectedResult = expect.objectContaining({ |
| | sender: TestClient.sender, |
| | text: expect.any(String), |
| | isCreatedByUser: false, |
| | messageId: expect.any(String), |
| | parentMessageId: expect.any(String), |
| | conversationId: expect.any(String), |
| | }); |
| |
|
| | const response = await TestClient.sendMessage(userMessage); |
| | parentMessageId = response.messageId; |
| | conversationId = response.conversationId; |
| | expect(response).toEqual(expectedResult); |
| | }); |
| |
|
| | test('should replace responseMessageId with new UUID when isRegenerate is true and messageId ends with underscore', async () => { |
| | const mockCrypto = require('crypto'); |
| | const newUUID = 'new-uuid-1234'; |
| | jest.spyOn(mockCrypto, 'randomUUID').mockReturnValue(newUUID); |
| |
|
| | const opts = { |
| | isRegenerate: true, |
| | responseMessageId: 'existing-message-id_', |
| | }; |
| |
|
| | await TestClient.setMessageOptions(opts); |
| |
|
| | expect(TestClient.responseMessageId).toBe(newUUID); |
| | expect(TestClient.responseMessageId).not.toBe('existing-message-id_'); |
| |
|
| | mockCrypto.randomUUID.mockRestore(); |
| | }); |
| |
|
| | test('should not replace responseMessageId when isRegenerate is false', async () => { |
| | const opts = { |
| | isRegenerate: false, |
| | responseMessageId: 'existing-message-id_', |
| | }; |
| |
|
| | await TestClient.setMessageOptions(opts); |
| |
|
| | expect(TestClient.responseMessageId).toBe('existing-message-id_'); |
| | }); |
| |
|
| | test('should not replace responseMessageId when it does not end with underscore', async () => { |
| | const opts = { |
| | isRegenerate: true, |
| | responseMessageId: 'existing-message-id', |
| | }; |
| |
|
| | await TestClient.setMessageOptions(opts); |
| |
|
| | expect(TestClient.responseMessageId).toBe('existing-message-id'); |
| | }); |
| |
|
| | test('sendMessage should work with provided conversationId and parentMessageId', async () => { |
| | const userMessage = 'Second message in the conversation'; |
| | const opts = { |
| | conversationId, |
| | parentMessageId, |
| | getReqData: jest.fn(), |
| | onStart: jest.fn(), |
| | }; |
| |
|
| | const expectedResult = expect.objectContaining({ |
| | sender: TestClient.sender, |
| | text: expect.any(String), |
| | isCreatedByUser: false, |
| | messageId: expect.any(String), |
| | parentMessageId: expect.any(String), |
| | conversationId: opts.conversationId, |
| | }); |
| |
|
| | const response = await TestClient.sendMessage(userMessage, opts); |
| | parentMessageId = response.messageId; |
| | expect(response.conversationId).toEqual(conversationId); |
| | expect(response).toEqual(expectedResult); |
| | expect(opts.getReqData).toHaveBeenCalled(); |
| | expect(opts.onStart).toHaveBeenCalled(); |
| | expect(TestClient.getBuildMessagesOptions).toHaveBeenCalled(); |
| | expect(TestClient.getSaveOptions).toHaveBeenCalled(); |
| | }); |
| |
|
| | test('should return chat history', async () => { |
| | TestClient = initializeFakeClient(apiKey, options, messageHistory); |
| | const chatMessages = await TestClient.loadHistory(conversationId, '2'); |
| | expect(TestClient.currentMessages).toHaveLength(2); |
| | expect(chatMessages[0].text).toEqual('Hello'); |
| |
|
| | const chatMessages2 = await TestClient.loadHistory(conversationId, '3'); |
| | expect(TestClient.currentMessages).toHaveLength(3); |
| | expect(chatMessages2[chatMessages2.length - 1].text).toEqual("What's up"); |
| | }); |
| |
|
| | |
| | |
| | |
| | |
| | |
| | it('should not push userMessage to currentMessages when isEdited is true and vice versa', async () => { |
| | const overrideParentMessageId = 'user-message-id'; |
| | const responseMessageId = 'response-message-id'; |
| | const newHistory = messageHistory.slice(); |
| | newHistory.push({ |
| | role: 'assistant', |
| | isCreatedByUser: false, |
| | text: 'test message', |
| | messageId: responseMessageId, |
| | parentMessageId: '3', |
| | }); |
| |
|
| | TestClient = initializeFakeClient(apiKey, options, newHistory); |
| | const sendMessageOptions = { |
| | isEdited: true, |
| | overrideParentMessageId, |
| | parentMessageId: '3', |
| | responseMessageId, |
| | }; |
| |
|
| | await TestClient.sendMessage('test message', sendMessageOptions); |
| | const currentMessages = TestClient.currentMessages; |
| | expect(currentMessages[currentMessages.length - 1].messageId).not.toEqual( |
| | overrideParentMessageId, |
| | ); |
| |
|
| | |
| | sendMessageOptions.isEdited = false; |
| | await TestClient.sendMessage('test message', sendMessageOptions); |
| | const currentMessages2 = TestClient.currentMessages; |
| | expect(currentMessages2[currentMessages2.length - 1].messageId).toEqual( |
| | overrideParentMessageId, |
| | ); |
| | }); |
| |
|
| | test('setOptions is called with the correct arguments only when replaceOptions is set to true', async () => { |
| | TestClient.setOptions = jest.fn(); |
| | const opts = { conversationId: '123', parentMessageId: '456', replaceOptions: true }; |
| | await TestClient.sendMessage('Hello, world!', opts); |
| | expect(TestClient.setOptions).toHaveBeenCalledWith(opts); |
| | TestClient.setOptions.mockClear(); |
| | }); |
| |
|
| | test('loadHistory is called with the correct arguments', async () => { |
| | const opts = { conversationId: '123', parentMessageId: '456' }; |
| | await TestClient.sendMessage('Hello, world!', opts); |
| | expect(TestClient.loadHistory).toHaveBeenCalledWith( |
| | opts.conversationId, |
| | opts.parentMessageId, |
| | ); |
| | }); |
| |
|
| | test('getReqData is called with the correct arguments', async () => { |
| | const getReqData = jest.fn(); |
| | const opts = { getReqData }; |
| | const response = await TestClient.sendMessage('Hello, world!', opts); |
| | expect(getReqData).toHaveBeenCalledWith( |
| | expect.objectContaining({ |
| | userMessage: expect.objectContaining({ text: 'Hello, world!' }), |
| | conversationId: response.conversationId, |
| | responseMessageId: response.messageId, |
| | }), |
| | ); |
| | }); |
| |
|
| | test('onStart is called with the correct arguments', async () => { |
| | const onStart = jest.fn(); |
| | const opts = { onStart }; |
| | await TestClient.sendMessage('Hello, world!', opts); |
| |
|
| | expect(onStart).toHaveBeenCalledWith( |
| | expect.objectContaining({ text: 'Hello, world!' }), |
| | expect.any(String), |
| | |
| | true, |
| | ); |
| | }); |
| |
|
| | test('saveMessageToDatabase is called with the correct arguments', async () => { |
| | const saveOptions = TestClient.getSaveOptions(); |
| | const user = {}; |
| | const opts = { user }; |
| | const saveSpy = jest.spyOn(TestClient, 'saveMessageToDatabase'); |
| | await TestClient.sendMessage('Hello, world!', opts); |
| | expect(saveSpy).toHaveBeenCalledWith( |
| | expect.objectContaining({ |
| | sender: expect.any(String), |
| | text: expect.any(String), |
| | isCreatedByUser: expect.any(Boolean), |
| | messageId: expect.any(String), |
| | parentMessageId: expect.any(String), |
| | conversationId: expect.any(String), |
| | }), |
| | saveOptions, |
| | user, |
| | ); |
| | }); |
| |
|
| | test('should handle existing conversation when getConvo retrieves one', async () => { |
| | const existingConvo = { |
| | conversationId: 'existing-convo-id', |
| | endpoint: 'openai', |
| | endpointType: 'openai', |
| | model: 'gpt-3.5-turbo', |
| | messages: [ |
| | { role: 'user', content: 'Existing message 1' }, |
| | { role: 'assistant', content: 'Existing response 1' }, |
| | ], |
| | temperature: 1, |
| | }; |
| |
|
| | const { temperature: _temp, ...newConvo } = existingConvo; |
| |
|
| | const user = { |
| | id: 'user-id', |
| | }; |
| |
|
| | getConvo.mockResolvedValue(existingConvo); |
| | saveConvo.mockResolvedValue(newConvo); |
| |
|
| | TestClient = initializeFakeClient( |
| | apiKey, |
| | { |
| | ...options, |
| | req: { |
| | user, |
| | }, |
| | }, |
| | [], |
| | ); |
| |
|
| | const saveSpy = jest.spyOn(TestClient, 'saveMessageToDatabase'); |
| |
|
| | const newMessage = 'New message in existing conversation'; |
| | const response = await TestClient.sendMessage(newMessage, { |
| | user, |
| | conversationId: existingConvo.conversationId, |
| | }); |
| |
|
| | expect(getConvo).toHaveBeenCalledWith(user.id, existingConvo.conversationId); |
| | expect(TestClient.conversationId).toBe(existingConvo.conversationId); |
| | expect(response.conversationId).toBe(existingConvo.conversationId); |
| | expect(TestClient.fetchedConvo).toBe(true); |
| |
|
| | expect(saveSpy).toHaveBeenCalledWith( |
| | expect.objectContaining({ |
| | conversationId: existingConvo.conversationId, |
| | text: newMessage, |
| | }), |
| | expect.any(Object), |
| | expect.any(Object), |
| | ); |
| |
|
| | expect(saveConvo).toHaveBeenCalledTimes(2); |
| | expect(saveConvo).toHaveBeenCalledWith( |
| | expect.any(Object), |
| | expect.objectContaining({ |
| | conversationId: existingConvo.conversationId, |
| | }), |
| | expect.objectContaining({ |
| | context: 'api/app/clients/BaseClient.js - saveMessageToDatabase #saveConvo', |
| | unsetFields: { |
| | temperature: 1, |
| | }, |
| | }), |
| | ); |
| |
|
| | await TestClient.sendMessage('Another message', { |
| | conversationId: existingConvo.conversationId, |
| | }); |
| | expect(getConvo).toHaveBeenCalledTimes(1); |
| | }); |
| |
|
| | test('should correctly handle existing conversation and unset fields appropriately', async () => { |
| | const existingConvo = { |
| | conversationId: 'existing-convo-id', |
| | endpoint: 'openai', |
| | endpointType: 'openai', |
| | model: 'gpt-3.5-turbo', |
| | messages: [ |
| | { role: 'user', content: 'Existing message 1' }, |
| | { role: 'assistant', content: 'Existing response 1' }, |
| | ], |
| | title: 'Existing Conversation', |
| | someExistingField: 'existingValue', |
| | anotherExistingField: 'anotherValue', |
| | temperature: 0.7, |
| | modelLabel: 'GPT-3.5', |
| | }; |
| |
|
| | getConvo.mockResolvedValue(existingConvo); |
| | saveConvo.mockResolvedValue(existingConvo); |
| |
|
| | TestClient = initializeFakeClient( |
| | apiKey, |
| | { |
| | ...options, |
| | modelOptions: { |
| | model: 'gpt-4', |
| | temperature: 0.5, |
| | }, |
| | }, |
| | [], |
| | ); |
| |
|
| | const newMessage = 'New message in existing conversation'; |
| | await TestClient.sendMessage(newMessage, { |
| | conversationId: existingConvo.conversationId, |
| | }); |
| |
|
| | expect(saveConvo).toHaveBeenCalledTimes(2); |
| |
|
| | const saveConvoCall = saveConvo.mock.calls[0]; |
| | const [, savedFields, saveOptions] = saveConvoCall; |
| |
|
| | |
| | |
| | expect(savedFields).not.toHaveProperty('messages'); |
| | expect(savedFields).not.toHaveProperty('title'); |
| |
|
| | |
| | expect(saveOptions.unsetFields).toHaveProperty('someExistingField', 1); |
| |
|
| | |
| | saveConvo.mockImplementation((req, fields) => { |
| | return Promise.resolve({ |
| | ...fields, |
| | endpoint: 'openai', |
| | endpointType: 'openai', |
| | model: 'gpt-4', |
| | temperature: 0.5, |
| | }); |
| | }); |
| |
|
| | |
| | expect(savedFields).toHaveProperty('conversationId', 'existing-convo-id'); |
| |
|
| | expect(TestClient.fetchedConvo).toBe(true); |
| |
|
| | await TestClient.sendMessage('Another message', { |
| | conversationId: existingConvo.conversationId, |
| | }); |
| |
|
| | expect(getConvo).toHaveBeenCalledTimes(1); |
| |
|
| | const secondSaveConvoCall = saveConvo.mock.calls[1]; |
| | expect(secondSaveConvoCall[2]).toHaveProperty('unsetFields', {}); |
| | }); |
| |
|
| | test('sendCompletion is called with the correct arguments', async () => { |
| | const payload = {}; |
| | TestClient.buildMessages.mockReturnValue({ prompt: payload, tokenCountMap: null }); |
| | const opts = {}; |
| | await TestClient.sendMessage('Hello, world!', opts); |
| | expect(TestClient.sendCompletion).toHaveBeenCalledWith(payload, opts); |
| | }); |
| |
|
| | test('getTokenCount for response is called with the correct arguments', async () => { |
| | const tokenCountMap = {}; |
| | TestClient.buildMessages.mockReturnValue({ prompt: [], tokenCountMap }); |
| | TestClient.getTokenCountForResponse = jest.fn(); |
| | const response = await TestClient.sendMessage('Hello, world!', {}); |
| | expect(TestClient.getTokenCountForResponse).toHaveBeenCalledWith(response); |
| | }); |
| |
|
| | test('returns an object with the correct shape', async () => { |
| | const response = await TestClient.sendMessage('Hello, world!', {}); |
| | expect(response).toEqual( |
| | expect.objectContaining({ |
| | sender: expect.any(String), |
| | text: expect.any(String), |
| | isCreatedByUser: expect.any(Boolean), |
| | messageId: expect.any(String), |
| | parentMessageId: expect.any(String), |
| | conversationId: expect.any(String), |
| | }), |
| | ); |
| | }); |
| |
|
| | test('userMessagePromise is awaited before saving response message', async () => { |
| | |
| | TestClient.saveMessageToDatabase = jest.fn().mockImplementation(() => { |
| | return new Promise((resolve) => setTimeout(resolve, 100)); |
| | }); |
| |
|
| | |
| | const messagePromise = TestClient.sendMessage('Hello, world!'); |
| |
|
| | |
| | await new Promise((resolve) => setTimeout(resolve, 50)); |
| |
|
| | |
| | expect(TestClient.saveMessageToDatabase).toHaveBeenCalledTimes(1); |
| |
|
| | |
| | await messagePromise; |
| |
|
| | |
| | expect(TestClient.saveMessageToDatabase).toHaveBeenCalledTimes(2); |
| |
|
| | |
| | const calls = TestClient.saveMessageToDatabase.mock.calls; |
| | expect(calls[0][0].isCreatedByUser).toBe(true); |
| | expect(calls[1][0].isCreatedByUser).toBe(false); |
| | }); |
| | }); |
| |
|
| | describe('getMessagesWithinTokenLimit with instructions', () => { |
| | test('should always include instructions when present', async () => { |
| | TestClient.maxContextTokens = 50; |
| | const instructions = { |
| | role: 'system', |
| | content: 'System instructions', |
| | tokenCount: 20, |
| | }; |
| |
|
| | const messages = [ |
| | instructions, |
| | { role: 'user', content: 'Hello', tokenCount: 10 }, |
| | { role: 'assistant', content: 'Hi there', tokenCount: 15 }, |
| | ]; |
| |
|
| | const result = await TestClient.getMessagesWithinTokenLimit({ |
| | messages, |
| | instructions, |
| | }); |
| |
|
| | expect(result.context[0]).toBe(instructions); |
| | expect(result.remainingContextTokens).toBe(2); |
| | }); |
| |
|
| | test('should handle case when messages exceed limit but instructions must be preserved', async () => { |
| | TestClient.maxContextTokens = 30; |
| | const instructions = { |
| | role: 'system', |
| | content: 'System instructions', |
| | tokenCount: 20, |
| | }; |
| |
|
| | const messages = [ |
| | instructions, |
| | { role: 'user', content: 'Hello', tokenCount: 10 }, |
| | { role: 'assistant', content: 'Hi there', tokenCount: 15 }, |
| | ]; |
| |
|
| | const result = await TestClient.getMessagesWithinTokenLimit({ |
| | messages, |
| | instructions, |
| | }); |
| |
|
| | |
| | expect(result.context).toHaveLength(1); |
| | expect(result.context[0].content).toBe(instructions.content); |
| | expect(result.messagesToRefine).toHaveLength(2); |
| | expect(result.remainingContextTokens).toBe(7); |
| | }); |
| |
|
| | test('should work correctly without instructions (1/2)', async () => { |
| | TestClient.maxContextTokens = 50; |
| | const messages = [ |
| | { role: 'user', content: 'Hello', tokenCount: 10 }, |
| | { role: 'assistant', content: 'Hi there', tokenCount: 15 }, |
| | ]; |
| |
|
| | const result = await TestClient.getMessagesWithinTokenLimit({ |
| | messages, |
| | }); |
| |
|
| | expect(result.context).toHaveLength(2); |
| | expect(result.remainingContextTokens).toBe(22); |
| | expect(result.messagesToRefine).toHaveLength(0); |
| | }); |
| |
|
| | test('should work correctly without instructions (2/2)', async () => { |
| | TestClient.maxContextTokens = 30; |
| | const messages = [ |
| | { role: 'user', content: 'Hello', tokenCount: 10 }, |
| | { role: 'assistant', content: 'Hi there', tokenCount: 20 }, |
| | ]; |
| |
|
| | const result = await TestClient.getMessagesWithinTokenLimit({ |
| | messages, |
| | }); |
| |
|
| | expect(result.context).toHaveLength(1); |
| | expect(result.remainingContextTokens).toBe(7); |
| | expect(result.messagesToRefine).toHaveLength(1); |
| | }); |
| |
|
| | test('should handle case when only instructions fit within limit', async () => { |
| | TestClient.maxContextTokens = 25; |
| | const instructions = { |
| | role: 'system', |
| | content: 'System instructions', |
| | tokenCount: 20, |
| | }; |
| |
|
| | const messages = [ |
| | instructions, |
| | { role: 'user', content: 'Hello', tokenCount: 10 }, |
| | { role: 'assistant', content: 'Hi there', tokenCount: 15 }, |
| | ]; |
| |
|
| | const result = await TestClient.getMessagesWithinTokenLimit({ |
| | messages, |
| | instructions, |
| | }); |
| |
|
| | expect(result.context).toHaveLength(1); |
| | expect(result.context[0]).toBe(instructions); |
| | expect(result.messagesToRefine).toHaveLength(2); |
| | expect(result.remainingContextTokens).toBe(2); |
| | }); |
| | }); |
| | }); |
| |
|