From 9c7547db96c49699c8388612a700e16a7ef21b9b Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Wed, 15 Jul 2026 18:08:32 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=A7=B7=20fix:=20Flush=20Pending=20File=20?= =?UTF-8?q?Deletion=20on=20Unmount=20(#14293)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removing the last file from an agent's Context panel fired no request and the file reappeared on reload. `FileContext` mounts `FileRow` only while `fileCount > 0`, and `useFileDeletion` lives inside `FileRow` behind a 1s debounce whose unmount cleanup called `debouncedDelete.cancel()`. Deleting the last file drops the count to 0, unmounting `FileRow` and cancelling the delete the user had already confirmed. Removing a non-last file kept the row mounted and worked, which made the failure look erratic. The same cancel also dropped deletes in any panel closed within the debounce window. Flush the pending batch on unmount instead of cancelling it. --- .../Files/__tests__/useFileDeletion.spec.tsx | 135 ++++++++++++++++++ client/src/hooks/Files/useFileDeletion.ts | 9 +- 2 files changed, 140 insertions(+), 4 deletions(-) create mode 100644 client/src/hooks/Files/__tests__/useFileDeletion.spec.tsx diff --git a/client/src/hooks/Files/__tests__/useFileDeletion.spec.tsx b/client/src/hooks/Files/__tests__/useFileDeletion.spec.tsx new file mode 100644 index 0000000000..da084426c8 --- /dev/null +++ b/client/src/hooks/Files/__tests__/useFileDeletion.spec.tsx @@ -0,0 +1,135 @@ +import React, { useState } from 'react'; +import { FileSources, EToolResources } from 'librechat-data-provider'; +import { render, screen, act, renderHook } from '@testing-library/react'; +import type { ExtendedFile } from '~/common'; +import FileRow from '~/components/Chat/Input/Files/FileRow'; +import useFileDeletion from '../useFileDeletion'; + +const mockMutateAsync = jest.fn(); + +jest.mock('~/data-provider', () => ({ + useDeleteFilesMutation: () => ({ mutateAsync: mockMutateAsync }), +})); + +jest.mock('@librechat/client', () => ({ + useToastContext: () => ({ showToast: jest.fn() }), +})); + +jest.mock('~/hooks', () => ({ + useLocalize: () => (key: string) => key, +})); + +jest.mock('~/utils', () => ({ + ...jest.requireActual('~/utils'), + logger: { log: jest.fn() }, + getCachedPreview: () => undefined, + deletePreview: jest.fn(), +})); + +jest.mock('../useSetFilesToDelete', () => ({ + __esModule: true, + default: () => jest.fn(), +})); + +jest.mock('~/components/Chat/Input/Files/FileContainer', () => { + return function MockFileContainer({ + file, + onDelete, + }: { + file: ExtendedFile; + onDelete: () => void; + }) { + return