From 305e0f50038577ae69ae4504004acaa1ae57ae80 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Tue, 14 Jul 2026 18:05:16 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=A7=BD=20fix:=20Clear=20Deleted=20Chats?= =?UTF-8?q?=20From=20Message=20Cache=20(#14270)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: clear deleted conversation message caches * test: cover deleted chat cache cleanup * test: clarify deleted cache scenarios --- client/src/data-provider/mutations.ts | 2 + client/src/utils/__tests__/messages.test.ts | 52 +++++++++++++++++++ client/src/utils/messages.ts | 27 ++++++++++ .../mock/conversation-management.spec.ts | 6 ++- 4 files changed, 86 insertions(+), 1 deletion(-) diff --git a/client/src/data-provider/mutations.ts b/client/src/data-provider/mutations.ts index 9cecb49517..391a8a74ed 100644 --- a/client/src/data-provider/mutations.ts +++ b/client/src/data-provider/mutations.ts @@ -14,6 +14,7 @@ import { findConversationInInfinite, updateConvoInAllQueries, removeConvoFromAllQueries, + clearDeletedConversationMessagesCache, } from '~/utils'; import useUpdateTagsInConvo from '~/hooks/Conversations/useUpdateTagsInConvo'; import { updateConversationTag } from '~/utils/conversationTags'; @@ -544,6 +545,7 @@ export const useDeleteConversationMutation = ( if (vars.conversationId) { removeConvoFromAllQueries(queryClient, vars.conversationId); + clearDeletedConversationMessagesCache(queryClient, vars.conversationId); } // Also remove from all archivedConversations caches diff --git a/client/src/utils/__tests__/messages.test.ts b/client/src/utils/__tests__/messages.test.ts index b2419c291e..2a628353e1 100644 --- a/client/src/utils/__tests__/messages.test.ts +++ b/client/src/utils/__tests__/messages.test.ts @@ -4,6 +4,7 @@ import type { TMessage } from 'librechat-data-provider'; import type { LocalizeFunction } from '~/common'; import { clearMessagesCache, + clearDeletedConversationMessagesCache, isValidTimestamp, getMessageAriaLabel, getMessageTimestamp, @@ -50,6 +51,57 @@ describe('clearMessagesCache', () => { }); }); +describe('clearDeletedConversationMessagesCache', () => { + it('clears both caches when the new-conversation cache contains deleted chat messages', () => { + const queryClient = new QueryClient(); + const conversationId = 'conversation-1'; + const messages = [makeMessage({ conversationId })]; + queryClient.setQueryData([QueryKeys.messages, conversationId], messages); + queryClient.setQueryData( + [QueryKeys.messages, Constants.NEW_CONVO], + messages.map((message) => ({ ...message })), + ); + + clearDeletedConversationMessagesCache(queryClient, conversationId); + + expect(queryClient.getQueryData([QueryKeys.messages, conversationId])).toBeUndefined(); + expect(queryClient.getQueryData([QueryKeys.messages, Constants.NEW_CONVO])).toEqual([]); + }); + + it('clears a shared new-conversation cache before its message IDs are hydrated', () => { + const queryClient = new QueryClient(); + const conversationId = 'conversation-1'; + const messages = [makeMessage({ conversationId: Constants.NEW_CONVO as string })]; + queryClient.setQueryData([QueryKeys.messages, conversationId], messages); + queryClient.setQueryData([QueryKeys.messages, Constants.NEW_CONVO], messages); + + clearDeletedConversationMessagesCache(queryClient, conversationId); + + expect(queryClient.getQueryData([QueryKeys.messages, conversationId])).toBeUndefined(); + expect(queryClient.getQueryData([QueryKeys.messages, Constants.NEW_CONVO])).toEqual([]); + }); + + it('preserves an unrelated new-conversation message cache', () => { + const queryClient = new QueryClient(); + const conversationId = 'conversation-1'; + const newConversationMessages = [ + makeMessage({ messageId: 'new-message', conversationId: Constants.NEW_CONVO as string }), + ]; + queryClient.setQueryData( + [QueryKeys.messages, conversationId], + [makeMessage({ conversationId })], + ); + queryClient.setQueryData([QueryKeys.messages, Constants.NEW_CONVO], newConversationMessages); + + clearDeletedConversationMessagesCache(queryClient, conversationId); + + expect(queryClient.getQueryData([QueryKeys.messages, conversationId])).toBeUndefined(); + expect(queryClient.getQueryData([QueryKeys.messages, Constants.NEW_CONVO])).toEqual( + newConversationMessages, + ); + }); +}); + describe('getMessageAriaLabel', () => { it('returns "Message N" when depth is present and valid', () => { const msg = makeMessage({ depth: 2 }); diff --git a/client/src/utils/messages.ts b/client/src/utils/messages.ts index f768dc652c..d88edecf69 100644 --- a/client/src/utils/messages.ts +++ b/client/src/utils/messages.ts @@ -312,6 +312,33 @@ export const clearMessagesCache = ( queryClient.setQueryData([QueryKeys.messages, Constants.NEW_CONVO], []); }; +/** Removes a deleted conversation's message cache and any matching new-chat cache alias. */ +export const clearDeletedConversationMessagesCache = ( + queryClient: QueryClient, + conversationId: string, +): void => { + const deletedMessages = queryClient.getQueryData([ + QueryKeys.messages, + conversationId, + ]); + const newConversationMessages = queryClient.getQueryData([ + QueryKeys.messages, + Constants.NEW_CONVO, + ]); + const newConversationAliasesDeleted = + newConversationMessages != null && + (newConversationMessages === deletedMessages || + newConversationMessages.some((message) => message.conversationId === conversationId)); + + queryClient.removeQueries([QueryKeys.messages, conversationId], { exact: true }); + + if (!newConversationAliasesDeleted) { + return; + } + + queryClient.setQueryData([QueryKeys.messages, Constants.NEW_CONVO], []); +}; + /** Returns a 1-based message number, or null if depth is absent or invalid. */ const getMessageNumber = (message: TMessage): number | null => { if (message.depth == null || message.depth < 0) { diff --git a/e2e/specs/mock/conversation-management.spec.ts b/e2e/specs/mock/conversation-management.spec.ts index c289aa1699..1953b45117 100644 --- a/e2e/specs/mock/conversation-management.spec.ts +++ b/e2e/specs/mock/conversation-management.spec.ts @@ -84,7 +84,9 @@ test.describe('conversation management', () => { await expect(page.getByTestId('convo-item').filter({ hasText: renamedTitle })).toBeVisible(); }); - test('deletes a conversation from the sidebar and blocks direct URL access', async ({ page }) => { + test('deletes a conversation, clears its messages, and blocks direct URL access', async ({ + page, + }) => { const label = uniqueLabel('sidebar-delete'); const renamedTitle = `Delete ${label}`; @@ -111,6 +113,8 @@ test.describe('conversation management', () => { await expect(page).toHaveURL(/\/c\/new$/); await expect(page.getByTestId('convo-item').filter({ hasText: renamedTitle })).toHaveCount(0); + await expect(messagesView(page).getByText(turn.prompt)).toHaveCount(0); + await expect(messagesView(page).getByText(turn.reply)).toHaveCount(0); await page.goto(conversationUrl, { timeout: 10000 }); await expect(page.getByRole('textbox', { name: 'Message input' })).toBeVisible();