mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-07-11 08:43:48 +00:00
* 🔧 feat: Implement deleteRagFile utility for RAG API document deletion across storage strategies
* chore: import order
* chore: import order & remove unnecessary comments
---------
Co-authored-by: Danny Avila <danacordially@gmail.com>
150 lines
4.8 KiB
TypeScript
150 lines
4.8 KiB
TypeScript
jest.mock('@librechat/data-schemas', () => ({
|
|
logger: {
|
|
debug: jest.fn(),
|
|
warn: jest.fn(),
|
|
error: jest.fn(),
|
|
},
|
|
}));
|
|
|
|
jest.mock('~/crypto/jwt', () => ({
|
|
generateShortLivedToken: jest.fn().mockReturnValue('mock-jwt-token'),
|
|
}));
|
|
|
|
jest.mock('axios', () => ({
|
|
delete: jest.fn(),
|
|
interceptors: {
|
|
request: { use: jest.fn(), eject: jest.fn() },
|
|
response: { use: jest.fn(), eject: jest.fn() },
|
|
},
|
|
}));
|
|
|
|
import axios from 'axios';
|
|
import { deleteRagFile } from './rag';
|
|
import { logger } from '@librechat/data-schemas';
|
|
import { generateShortLivedToken } from '~/crypto/jwt';
|
|
|
|
const mockedAxios = axios as jest.Mocked<typeof axios>;
|
|
const mockedLogger = logger as jest.Mocked<typeof logger>;
|
|
const mockedGenerateShortLivedToken = generateShortLivedToken as jest.MockedFunction<
|
|
typeof generateShortLivedToken
|
|
>;
|
|
|
|
describe('deleteRagFile', () => {
|
|
const originalEnv = process.env;
|
|
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
process.env = { ...originalEnv };
|
|
process.env.RAG_API_URL = 'http://localhost:8000';
|
|
});
|
|
|
|
afterEach(() => {
|
|
process.env = originalEnv;
|
|
});
|
|
|
|
describe('when file is embedded and RAG_API_URL is configured', () => {
|
|
it('should delete the document from RAG API successfully', async () => {
|
|
const file = { file_id: 'file-123', embedded: true };
|
|
mockedAxios.delete.mockResolvedValueOnce({ status: 200 });
|
|
|
|
const result = await deleteRagFile({ userId: 'user123', file });
|
|
|
|
expect(result).toBe(true);
|
|
expect(mockedGenerateShortLivedToken).toHaveBeenCalledWith('user123');
|
|
expect(mockedAxios.delete).toHaveBeenCalledWith('http://localhost:8000/documents', {
|
|
headers: {
|
|
Authorization: 'Bearer mock-jwt-token',
|
|
'Content-Type': 'application/json',
|
|
accept: 'application/json',
|
|
},
|
|
data: ['file-123'],
|
|
});
|
|
expect(mockedLogger.debug).toHaveBeenCalledWith(
|
|
'[deleteRagFile] Successfully deleted document file-123 from RAG API',
|
|
);
|
|
});
|
|
|
|
it('should return true and log warning when document is not found (404)', async () => {
|
|
const file = { file_id: 'file-not-found', embedded: true };
|
|
const error = new Error('Not Found') as Error & { response?: { status?: number } };
|
|
error.response = { status: 404 };
|
|
mockedAxios.delete.mockRejectedValueOnce(error);
|
|
|
|
const result = await deleteRagFile({ userId: 'user123', file });
|
|
|
|
expect(result).toBe(true);
|
|
expect(mockedLogger.warn).toHaveBeenCalledWith(
|
|
'[deleteRagFile] Document file-not-found not found in RAG API, may have been deleted already',
|
|
);
|
|
});
|
|
|
|
it('should return false and log error on other errors', async () => {
|
|
const file = { file_id: 'file-error', embedded: true };
|
|
const error = new Error('Server Error') as Error & { response?: { status?: number } };
|
|
error.response = { status: 500 };
|
|
mockedAxios.delete.mockRejectedValueOnce(error);
|
|
|
|
const result = await deleteRagFile({ userId: 'user123', file });
|
|
|
|
expect(result).toBe(false);
|
|
expect(mockedLogger.error).toHaveBeenCalledWith(
|
|
'[deleteRagFile] Error deleting document from RAG API:',
|
|
'Server Error',
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('when file is not embedded', () => {
|
|
it('should skip RAG deletion and return true', async () => {
|
|
const file = { file_id: 'file-123', embedded: false };
|
|
|
|
const result = await deleteRagFile({ userId: 'user123', file });
|
|
|
|
expect(result).toBe(true);
|
|
expect(mockedAxios.delete).not.toHaveBeenCalled();
|
|
expect(mockedGenerateShortLivedToken).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should skip RAG deletion when embedded is undefined', async () => {
|
|
const file = { file_id: 'file-123' };
|
|
|
|
const result = await deleteRagFile({ userId: 'user123', file });
|
|
|
|
expect(result).toBe(true);
|
|
expect(mockedAxios.delete).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('when RAG_API_URL is not configured', () => {
|
|
it('should skip RAG deletion and return true', async () => {
|
|
delete process.env.RAG_API_URL;
|
|
const file = { file_id: 'file-123', embedded: true };
|
|
|
|
const result = await deleteRagFile({ userId: 'user123', file });
|
|
|
|
expect(result).toBe(true);
|
|
expect(mockedAxios.delete).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('userId handling', () => {
|
|
it('should return false when no userId is provided', async () => {
|
|
const file = { file_id: 'file-123', embedded: true };
|
|
|
|
const result = await deleteRagFile({ userId: '', file });
|
|
|
|
expect(result).toBe(false);
|
|
expect(mockedLogger.error).toHaveBeenCalledWith('[deleteRagFile] No user ID provided');
|
|
expect(mockedAxios.delete).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should return false when userId is undefined', async () => {
|
|
const file = { file_id: 'file-123', embedded: true };
|
|
|
|
const result = await deleteRagFile({ userId: undefined as unknown as string, file });
|
|
|
|
expect(result).toBe(false);
|
|
expect(mockedLogger.error).toHaveBeenCalledWith('[deleteRagFile] No user ID provided');
|
|
});
|
|
});
|
|
});
|