import { Constants } from 'librechat-data-provider'; import type { TConversation, ConversationData } from 'librechat-data-provider'; import { dateKeys, addConversation, updateConvoFields, updateConversation, deleteConversation, findPageForConversation, groupConversationsByDate, } from './convos'; import { convoData } from './convos.fakeData'; import { normalizeData } from './collection'; describe('Conversation Utilities', () => { describe('groupConversationsByDate', () => { it('groups conversations by date correctly', () => { const conversations = [ { conversationId: '1', updatedAt: '2023-04-01T12:00:00Z' }, { conversationId: '2', updatedAt: new Date().toISOString() }, { conversationId: '3', updatedAt: new Date(Date.now() - 86400000).toISOString() }, // 86400 seconds ago = yesterday { conversationId: '4', updatedAt: new Date(Date.now() - 86400000 * 2).toISOString() }, // 2 days ago (previous 7 days) { conversationId: '5', updatedAt: new Date(Date.now() - 86400000 * 8).toISOString() }, // 8 days ago (previous 30 days) ]; const grouped = groupConversationsByDate(conversations as TConversation[]); expect(grouped[0][0]).toBe(dateKeys.today); expect(grouped[0][1]).toHaveLength(1); expect(grouped[1][0]).toBe(dateKeys.yesterday); expect(grouped[1][1]).toHaveLength(1); expect(grouped[2][0]).toBe(dateKeys.previous7Days); expect(grouped[2][1]).toHaveLength(1); expect(grouped[3][0]).toBe(dateKeys.previous30Days); expect(grouped[3][1]).toHaveLength(1); expect(grouped[4][0]).toBe(' 2023'); expect(grouped[4][1]).toHaveLength(1); }); it('returns an empty array for no conversations', () => { expect(groupConversationsByDate([])).toEqual([]); }); it('skips conversations with duplicate conversationIds', () => { const conversations = [ { conversationId: '1', updatedAt: '2023-12-01T12:00:00Z' }, // " 2023" { conversationId: '2', updatedAt: '2023-11-25T12:00:00Z' }, // " 2023" { conversationId: '1', updatedAt: '2023-11-20T12:00:00Z' }, // Should be skipped because of duplicate ID { conversationId: '3', updatedAt: '2022-12-01T12:00:00Z' }, // " 2022" ]; const grouped = groupConversationsByDate(conversations as TConversation[]); expect(grouped).toEqual( expect.arrayContaining([ expect.arrayContaining([' 2023', expect.arrayContaining(conversations.slice(0, 2))]), expect.arrayContaining([' 2022', expect.arrayContaining([conversations[3]])]), ]), ); // No duplicate IDs are present const allGroupedIds = grouped.flatMap(([, convs]) => convs.map((c) => c.conversationId)); const uniqueIds = [...new Set(allGroupedIds)]; expect(allGroupedIds.length).toBe(uniqueIds.length); }); it('sorts conversations by month correctly', () => { const conversations = [ { conversationId: '1', updatedAt: '2023-01-01T12:00:00Z' }, // January 2023 { conversationId: '2', updatedAt: '2023-12-01T12:00:00Z' }, // December 2023 { conversationId: '3', updatedAt: '2023-02-01T12:00:00Z' }, // February 2023 { conversationId: '4', updatedAt: '2023-11-01T12:00:00Z' }, // November 2023 { conversationId: '5', updatedAt: '2022-12-01T12:00:00Z' }, // December 2022 ]; const grouped = groupConversationsByDate(conversations as TConversation[]); // Check if the years are in the correct order (most recent first) expect(grouped.map(([key]) => key)).toEqual([' 2023', ' 2022']); // Check if conversations within 2023 are sorted correctly by month const conversationsIn2023 = grouped[0][1]; const monthsIn2023 = conversationsIn2023.map((c) => new Date(c.updatedAt).getMonth()); expect(monthsIn2023).toEqual([11, 10, 1, 0]); // December (11), November (10), February (1), January (0) // Check if the conversation from 2022 is in its own group expect(grouped[1][1].length).toBe(1); expect(new Date(grouped[1][1][0].updatedAt).getFullYear()).toBe(2022); }); it('handles conversations from multiple years correctly', () => { const conversations = [ { conversationId: '1', updatedAt: '2023-01-01T12:00:00Z' }, // January 2023 { conversationId: '2', updatedAt: '2022-12-01T12:00:00Z' }, // December 2022 { conversationId: '3', updatedAt: '2021-06-01T12:00:00Z' }, // June 2021 { conversationId: '4', updatedAt: '2023-06-01T12:00:00Z' }, // June 2023 { conversationId: '5', updatedAt: '2021-12-01T12:00:00Z' }, // December 2021 ]; const grouped = groupConversationsByDate(conversations as TConversation[]); expect(grouped.map(([key]) => key)).toEqual([' 2023', ' 2022', ' 2021']); expect(grouped[0][1].map((c) => new Date(c.updatedAt).getMonth())).toEqual([5, 0]); // June, January expect(grouped[1][1].map((c) => new Date(c.updatedAt).getMonth())).toEqual([11]); // December expect(grouped[2][1].map((c) => new Date(c.updatedAt).getMonth())).toEqual([11, 5]); // December, June }); it('handles conversations from the same month correctly', () => { const conversations = [ { conversationId: '1', updatedAt: '2023-06-01T12:00:00Z' }, { conversationId: '2', updatedAt: '2023-06-15T12:00:00Z' }, { conversationId: '3', updatedAt: '2023-06-30T12:00:00Z' }, ]; const grouped = groupConversationsByDate(conversations as TConversation[]); expect(grouped.length).toBe(1); expect(grouped[0][0]).toBe(' 2023'); expect(grouped[0][1].map((c) => c.conversationId)).toEqual(['3', '2', '1']); }); it('handles conversations from today, yesterday, and previous days correctly', () => { const today = new Date(); const yesterday = new Date(today); yesterday.setDate(yesterday.getDate() - 1); const twoDaysAgo = new Date(today); twoDaysAgo.setDate(twoDaysAgo.getDate() - 2); const conversations = [ { conversationId: '1', updatedAt: today.toISOString() }, { conversationId: '2', updatedAt: yesterday.toISOString() }, { conversationId: '3', updatedAt: twoDaysAgo.toISOString() }, ]; const grouped = groupConversationsByDate(conversations as TConversation[]); expect(grouped.map(([key]) => key)).toEqual([ dateKeys.today, dateKeys.yesterday, dateKeys.previous7Days, ]); }); it('handles conversations with null or undefined updatedAt correctly', () => { const conversations = [ { conversationId: '1', updatedAt: '2023-06-01T12:00:00Z' }, { conversationId: '2', updatedAt: null }, { conversationId: '3', updatedAt: undefined }, ]; const grouped = groupConversationsByDate(conversations as TConversation[]); expect(grouped.length).toBe(2); // One group for 2023 and one for today (null/undefined dates) expect(grouped[0][0]).toBe(dateKeys.today); expect(grouped[0][1].length).toBe(2); // Two conversations with null/undefined dates expect(grouped[1][0]).toBe(' 2023'); expect(grouped[1][1].length).toBe(1); // One conversation from 2023 }); it('handles an empty array of conversations', () => { const grouped = groupConversationsByDate([]); expect(grouped).toEqual([]); }); it('correctly groups and sorts conversations for every month of the year', () => { const months = [ 'january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december', ]; // Create conversations for each month in both 2023 and 2022 const conversations = months.flatMap((month, index) => [ { conversationId: `2023-${month}`, updatedAt: `2023-${String(index + 1).padStart(2, '0')}-15T12:00:00Z`, }, { conversationId: `2022-${month}`, updatedAt: `2022-${String(index + 1).padStart(2, '0')}-15T12:00:00Z`, }, ]); const grouped = groupConversationsByDate(conversations as TConversation[]); // Check that we have two year groups expect(grouped.length).toBe(2); // Check 2023 months const group2023 = grouped.find(([key]) => key === ' 2023') ?? []; expect(group2023).toBeDefined(); const grouped2023 = group2023[1]; expect(grouped2023?.length).toBe(12); expect(grouped2023?.map((c) => new Date(c.updatedAt).getMonth())).toEqual([ 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, ]); // Check 2022 months const group2022 = grouped.find(([key]) => key === ' 2022') ?? []; expect(group2022).toBeDefined(); const grouped2022 = group2022[1]; expect(grouped2022?.length).toBe(12); expect(grouped2022?.map((c) => new Date(c.updatedAt).getMonth())).toEqual([ 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, ]); // Check that all conversations are accounted for const totalGroupedConversations = // eslint-disable-next-line @typescript-eslint/no-unused-vars grouped.reduce((total, [_, convos]) => total + convos.length, 0); expect(totalGroupedConversations).toBe(conversations.length); // Check that the years are in the correct order const yearOrder = grouped.map(([key]) => key); expect(yearOrder).toEqual([' 2023', ' 2022']); }); }); describe('addConversation', () => { it('adds a new conversation to the top of the list', () => { const data = { pages: [{ conversations: [] }] }; const newConversation = { conversationId: Constants.NEW_CONVO, updatedAt: '2023-04-02T12:00:00Z', }; const newData = addConversation( data as unknown as ConversationData, newConversation as TConversation, ); expect(newData.pages[0].conversations).toHaveLength(1); expect(newData.pages[0].conversations[0].conversationId).toBe(Constants.NEW_CONVO); }); }); describe('updateConversation', () => { it('updates an existing conversation and moves it to the top', () => { const initialData = { pages: [ { conversations: [ { conversationId: '1', updatedAt: '2023-04-01T12:00:00Z' }, { conversationId: '2', updatedAt: '2023-04-01T13:00:00Z' }, ], }, ], }; const updatedConversation = { conversationId: '1', updatedAt: '2023-04-02T12:00:00Z' }; const newData = updateConversation( initialData as unknown as ConversationData, updatedConversation as TConversation, ); expect(newData.pages[0].conversations).toHaveLength(2); expect(newData.pages[0].conversations[0].conversationId).toBe('1'); }); }); describe('updateConvoFields', () => { it('updates specific fields of a conversation', () => { const initialData = { pages: [ { conversations: [ { conversationId: '1', title: 'Old Title', updatedAt: '2023-04-01T12:00:00Z' }, ], }, ], }; const updatedFields = { conversationId: '1', title: 'New Title' }; const newData = updateConvoFields( initialData as ConversationData, updatedFields as TConversation, ); expect(newData.pages[0].conversations[0].title).toBe('New Title'); }); }); describe('deleteConversation', () => { it('removes a conversation by id', () => { const initialData = { pages: [ { conversations: [ { conversationId: '1', updatedAt: '2023-04-01T12:00:00Z' }, { conversationId: '2', updatedAt: '2023-04-01T13:00:00Z' }, ], }, ], }; const newData = deleteConversation(initialData as ConversationData, '1'); expect(newData.pages[0].conversations).toHaveLength(1); expect(newData.pages[0].conversations[0].conversationId).not.toBe('1'); }); }); describe('findPageForConversation', () => { it('finds the correct page and index for a given conversation', () => { const data = { pages: [ { conversations: [ { conversationId: '1', updatedAt: '2023-04-01T12:00:00Z' }, { conversationId: '2', updatedAt: '2023-04-02T13:00:00Z' }, ], }, ], }; const { pageIndex, index } = findPageForConversation(data as ConversationData, { conversationId: '2', }); expect(pageIndex).toBe(0); expect(index).toBe(1); }); }); }); describe('Conversation Utilities with Fake Data', () => { describe('groupConversationsByDate', () => { it('correctly groups conversations from fake data by date', () => { const { pages } = convoData; const allConversations = pages.flatMap((p) => p.conversations); const grouped = groupConversationsByDate(allConversations); expect(grouped).toHaveLength(1); expect(grouped[0][1]).toBeInstanceOf(Array); }); }); describe('addConversation', () => { it('adds a new conversation to the existing fake data', () => { const newConversation = { conversationId: Constants.NEW_CONVO, updatedAt: new Date().toISOString(), } as TConversation; const initialLength = convoData.pages[0].conversations.length; const newData = addConversation(convoData, newConversation); expect(newData.pages[0].conversations.length).toBe(initialLength + 1); expect(newData.pages[0].conversations[0].conversationId).toBe(Constants.NEW_CONVO); }); }); describe('updateConversation', () => { it('updates an existing conversation within fake data', () => { const updatedConversation = { ...convoData.pages[0].conversations[0], title: 'Updated Title', }; const newData = updateConversation(convoData, updatedConversation); expect(newData.pages[0].conversations[0].title).toBe('Updated Title'); }); }); describe('updateConvoFields', () => { it('updates specific fields of a conversation in fake data', () => { const updatedFields = { conversationId: convoData.pages[0].conversations[0].conversationId, title: 'Partially Updated Title', }; const newData = updateConvoFields(convoData, updatedFields as TConversation); const updatedConversation = newData.pages[0].conversations.find( (c) => c.conversationId === updatedFields.conversationId, ); expect(updatedConversation?.title).toBe('Partially Updated Title'); }); }); describe('deleteConversation', () => { it('removes a conversation by id from fake data', () => { const conversationIdToDelete = convoData.pages[0].conversations[0].conversationId as string; const newData = deleteConversation(convoData, conversationIdToDelete); const deletedConvoExists = newData.pages[0].conversations.some( (c) => c.conversationId === conversationIdToDelete, ); expect(deletedConvoExists).toBe(false); }); }); describe('findPageForConversation', () => { it('finds the correct page and index for a given conversation in fake data', () => { const targetConversation = convoData.pages[0].conversations[0]; const { pageIndex, index } = findPageForConversation(convoData, { conversationId: targetConversation.conversationId as string, }); expect(pageIndex).toBeGreaterThanOrEqual(0); expect(index).toBeGreaterThanOrEqual(0); }); }); describe('normalizeConversationData', () => { it('normalizes the number of items on each page after data removal', () => { // Create test data: // Generates 15 conversation items, each with a unique conversationId and an updatedAt timestamp set to a different day. // { conversationId: '1', updatedAt: new Date(Date.now() - 86400000 * i).toISOString() } const conversations = Array.from({ length: 15 }, (_, i) => ({ conversationId: (i + 1).toString(), updatedAt: new Date(Date.now() - 86400000 * i).toISOString(), })); // Paginate Data: // Divides the 15 conversation items into pages, with each page containing up to 5 items (pageSize is set to 5). This results in 3 pages. const pageSize = 5; const totalPageNumber = Math.ceil(conversations.length / pageSize); const paginatedData = Array.from({ length: totalPageNumber }, (_, index) => ({ conversations: conversations.slice(index * pageSize, (index + 1) * pageSize), pages: totalPageNumber, pageNumber: index + 1, pageSize, })); const testData = { pages: paginatedData, pageParams: [null, 2, 3] }; // Removes one item from the first page, resulting in the first page having 4 items, while the second and third pages still have 5 items each. testData.pages[0].conversations.splice(1, 1); expect(testData.pages[0].conversations).toHaveLength(4); expect(testData.pages[1].conversations).toHaveLength(5); expect(testData.pages[2].conversations).toHaveLength(5); // Normalize Data: // Calls the normalizeData function to ensure that each page contains exactly 5 items, redistributing the items across the pages as needed. const normalizedData = normalizeData(testData, 'conversations', pageSize); // Verify Results: // Asserts that the number of conversation data is 5 except for the last page, // that the number of conversation data is 4 only for the last page, // and that the conversation ids are in the expected order. expect(normalizedData.pages[0].conversations).toHaveLength(5); expect(normalizedData.pages[0].conversations[0].conversationId).toBe('1'); expect(normalizedData.pages[0].conversations[4].conversationId).toBe('6'); expect(normalizedData.pages[1].conversations).toHaveLength(5); expect(normalizedData.pages[1].conversations[0].conversationId).toBe('7'); expect(normalizedData.pages[1].conversations[4].conversationId).toBe('11'); expect(normalizedData.pages[2].conversations).toHaveLength(4); expect(normalizedData.pages[2].conversations[0].conversationId).toBe('12'); expect(normalizedData.pages[2].conversations[3].conversationId).toBe('15'); }); it('normalizes the number of items on each page after data addition', () => { // Create test data: // Generates 15 conversation items, each with a unique conversationId and an updatedAt timestamp set to a different day. // { conversationId: '1', updatedAt: new Date(Date.now() - 86400000 * i).toISOString() } const conversations = Array.from({ length: 15 }, (_, i) => ({ conversationId: (i + 1).toString(), updatedAt: new Date(Date.now() - 86400000 * i).toISOString(), })); // Paginate Data: // Divides the 15 conversation items into pages, // with each page containing up to 5 items (pageSize is set to 5). This results in 3 pages. const pageSize = 5; const totalPageNumber = Math.ceil(conversations.length / pageSize); const paginatedData = Array.from({ length: totalPageNumber }, (_, index) => ({ conversations: conversations.slice(index * pageSize, (index + 1) * pageSize), pages: totalPageNumber, pageNumber: index + 1, pageSize, })); const testData = { pages: paginatedData, pageParams: [null, 2, 3] }; // Inserts a new conversation item at the beginning of the first page, testData.pages[0].conversations.unshift({ conversationId: '16', updatedAt: new Date().toISOString(), }); // resulting in the first page having 6 items, // while the second and third pages still have 5 items each. expect(testData.pages[0].conversations).toHaveLength(6); expect(testData.pages[1].conversations).toHaveLength(5); expect(testData.pages[2].conversations).toHaveLength(5); expect(testData.pages[2].conversations[4].conversationId).toBe('15'); expect(testData.pages).toHaveLength(3); const normalizedData = normalizeData(testData, 'conversations', pageSize); // Verify Results: // Asserts that after normalization, each page contains the correct number of items (5 items per page). expect(normalizedData.pages[0].conversations).toHaveLength(5); expect(normalizedData.pages[1].conversations).toHaveLength(5); expect(normalizedData.pages[2].conversations).toHaveLength(5); expect(normalizedData.pages).toHaveLength(3); // Checks that the items are in the expected order, ensuring that the conversationId values are correctly distributed across the pages. expect(normalizedData.pages[0].conversations[0].conversationId).toBe('16'); expect(normalizedData.pages[0].conversations[4].conversationId).toBe('4'); expect(normalizedData.pages[1].conversations[0].conversationId).toBe('5'); expect(normalizedData.pages[1].conversations[4].conversationId).toBe('9'); expect(normalizedData.pages[2].conversations[0].conversationId).toBe('10'); expect(normalizedData.pages[2].conversations[4].conversationId).toBe('14'); expect(normalizedData.pageParams).toHaveLength(3); }); it('returns empty data when there is no data', () => { const normalizedData = normalizeData( { pages: [{ conversations: [], pageNumber: 1, pageSize: 5, pages: 1 }], pageParams: [] }, 'conversations', 5, ); console.log(normalizedData); expect(normalizedData.pages[0].conversations).toHaveLength(0); }); it('does not normalize data when not needed', () => { const normalizedData = normalizeData( { pages: [{ conversations: ['1'], pageNumber: 1, pageSize: 5, pages: 1 }], pageParams: [] }, 'conversations', 5, ); expect(normalizedData.pages[0].conversations).toHaveLength(1); }); it('deletes pages that have no data as a result of normalization', () => { const conversations = Array.from({ length: 15 }, (_, i) => ({ conversationId: (i + 1).toString(), updatedAt: new Date(Date.now() - 86400000 * i).toISOString(), })); const pageSize = 5; const totalPageNumber = Math.ceil(conversations.length / pageSize); const paginatedData = Array.from({ length: totalPageNumber }, (_, index) => ({ conversations: conversations.slice(index * pageSize, (index + 1) * pageSize), pages: totalPageNumber, pageNumber: index + 1, pageSize, })); const testData = { pages: paginatedData, pageParams: [null, 2, 3] }; // Removes all data from the last page, resulting in the last page having 0 items. testData.pages[2].conversations = []; expect(testData.pages[0].conversations).toHaveLength(5); expect(testData.pages[1].conversations).toHaveLength(5); expect(testData.pages[2].conversations).toHaveLength(0); expect(testData.pageParams).toHaveLength(3); const normalizedData = normalizeData(testData, 'conversations', pageSize); // Verify Results: // Asserts that the last page is removed after normalization, leaving only the first and second pages. expect(normalizedData.pages).toHaveLength(2); expect(normalizedData.pages[0].conversations).toHaveLength(5); expect(normalizedData.pages[1].conversations).toHaveLength(5); expect(normalizedData.pageParams).toHaveLength(2); }); }); });