🧽 fix: Clear Deleted Chats From Message Cache (#14270)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions

* fix: clear deleted conversation message caches

* test: cover deleted chat cache cleanup

* test: clarify deleted cache scenarios
This commit is contained in:
Danny Avila 2026-07-14 18:05:16 -04:00 committed by GitHub
parent 481e2a9257
commit 305e0f5003
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 86 additions and 1 deletions

View file

@ -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

View file

@ -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 });

View file

@ -312,6 +312,33 @@ export const clearMessagesCache = (
queryClient.setQueryData<TMessage[]>([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<TMessage[]>([
QueryKeys.messages,
conversationId,
]);
const newConversationMessages = queryClient.getQueryData<TMessage[]>([
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<TMessage[]>([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) {

View file

@ -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();