| import { FileSources } from 'librechat-data-provider'; |
| import { Readable } from 'stream'; |
|
|
| jest.mock('@librechat/data-schemas', () => ({ |
| logger: { |
| debug: jest.fn(), |
| warn: jest.fn(), |
| error: jest.fn(), |
| }, |
| })); |
|
|
| jest.mock('fs', () => ({ |
| readFileSync: jest.fn(), |
| createReadStream: jest.fn(), |
| })); |
|
|
| jest.mock('../crypto/jwt', () => ({ |
| generateShortLivedToken: jest.fn(), |
| })); |
|
|
| jest.mock('axios', () => ({ |
| get: jest.fn(), |
| post: jest.fn(), |
| interceptors: { |
| request: { use: jest.fn(), eject: jest.fn() }, |
| response: { use: jest.fn(), eject: jest.fn() }, |
| }, |
| })); |
|
|
| jest.mock('form-data', () => { |
| return jest.fn().mockImplementation(() => ({ |
| append: jest.fn(), |
| getHeaders: jest.fn().mockReturnValue({ 'content-type': 'multipart/form-data' }), |
| })); |
| }); |
|
|
| |
| jest.mock('../utils', () => ({ |
| logAxiosError: jest.fn((args) => { |
| if (typeof args === 'object' && args.message) { |
| return args.message; |
| } |
| return 'Error'; |
| }), |
| readFileAsString: jest.fn(), |
| })); |
|
|
| |
| import { parseTextNative, parseText } from './text'; |
| import fs, { ReadStream } from 'fs'; |
| import axios from 'axios'; |
| import FormData from 'form-data'; |
| import type { ServerRequest } from '~/types'; |
| import { generateShortLivedToken } from '~/crypto/jwt'; |
| import { readFileAsString } from '~/utils'; |
|
|
| const mockedFs = fs as jest.Mocked<typeof fs>; |
| const mockedAxios = axios as jest.Mocked<typeof axios>; |
| const mockedFormData = FormData as jest.MockedClass<typeof FormData>; |
| const mockedGenerateShortLivedToken = generateShortLivedToken as jest.MockedFunction< |
| typeof generateShortLivedToken |
| >; |
| const mockedReadFileAsString = readFileAsString as jest.MockedFunction<typeof readFileAsString>; |
|
|
| describe('text', () => { |
| const mockFile: Express.Multer.File = { |
| fieldname: 'file', |
| originalname: 'test.txt', |
| encoding: '7bit', |
| mimetype: 'text/plain', |
| size: 100, |
| destination: '/tmp', |
| filename: 'test.txt', |
| path: '/tmp/test.txt', |
| buffer: Buffer.from('test content'), |
| stream: new Readable(), |
| }; |
|
|
| const mockReq = { |
| user: { id: 'user123' }, |
| } as ServerRequest; |
|
|
| const mockFileId = 'file123'; |
|
|
| beforeEach(() => { |
| jest.clearAllMocks(); |
| delete process.env.RAG_API_URL; |
| }); |
|
|
| describe('parseTextNative', () => { |
| it('should successfully parse a text file', async () => { |
| const mockText = 'Hello, world!'; |
| const mockBytes = Buffer.byteLength(mockText, 'utf8'); |
|
|
| mockedReadFileAsString.mockResolvedValue({ |
| content: mockText, |
| bytes: mockBytes, |
| }); |
|
|
| const result = await parseTextNative(mockFile); |
|
|
| expect(mockedReadFileAsString).toHaveBeenCalledWith('/tmp/test.txt', { |
| fileSize: 100, |
| }); |
| expect(result).toEqual({ |
| text: mockText, |
| bytes: mockBytes, |
| source: FileSources.text, |
| }); |
| }); |
|
|
| it('should handle file read errors', async () => { |
| const mockError = new Error('File not found'); |
| mockedReadFileAsString.mockRejectedValue(mockError); |
|
|
| await expect(parseTextNative(mockFile)).rejects.toThrow('File not found'); |
| }); |
| }); |
|
|
| describe('parseText', () => { |
| beforeEach(() => { |
| mockedGenerateShortLivedToken.mockReturnValue('mock-jwt-token'); |
|
|
| const mockFormDataInstance = { |
| append: jest.fn(), |
| getHeaders: jest.fn().mockReturnValue({ 'content-type': 'multipart/form-data' }), |
| }; |
| mockedFormData.mockImplementation(() => mockFormDataInstance as unknown as FormData); |
|
|
| mockedFs.createReadStream.mockReturnValue({} as unknown as ReadStream); |
| }); |
|
|
| it('should fall back to native parsing when RAG_API_URL is not defined', async () => { |
| const mockText = 'Native parsing result'; |
| const mockBytes = Buffer.byteLength(mockText, 'utf8'); |
|
|
| mockedReadFileAsString.mockResolvedValue({ |
| content: mockText, |
| bytes: mockBytes, |
| }); |
|
|
| const result = await parseText({ |
| req: mockReq, |
| file: mockFile, |
| file_id: mockFileId, |
| }); |
|
|
| expect(result).toEqual({ |
| text: mockText, |
| bytes: mockBytes, |
| source: FileSources.text, |
| }); |
| expect(mockedAxios.get).not.toHaveBeenCalled(); |
| }); |
|
|
| it('should fall back to native parsing when health check fails', async () => { |
| process.env.RAG_API_URL = 'http://rag-api.test'; |
| const mockText = 'Native parsing result'; |
| const mockBytes = Buffer.byteLength(mockText, 'utf8'); |
|
|
| mockedReadFileAsString.mockResolvedValue({ |
| content: mockText, |
| bytes: mockBytes, |
| }); |
|
|
| mockedAxios.get.mockRejectedValue(new Error('Health check failed')); |
|
|
| const result = await parseText({ |
| req: mockReq, |
| file: mockFile, |
| file_id: mockFileId, |
| }); |
|
|
| expect(mockedAxios.get).toHaveBeenCalledWith('http://rag-api.test/health', { |
| timeout: 10000, |
| }); |
| expect(result).toEqual({ |
| text: mockText, |
| bytes: mockBytes, |
| source: FileSources.text, |
| }); |
| }); |
|
|
| it('should fall back to native parsing when health check returns non-OK status', async () => { |
| process.env.RAG_API_URL = 'http://rag-api.test'; |
| const mockText = 'Native parsing result'; |
| const mockBytes = Buffer.byteLength(mockText, 'utf8'); |
|
|
| mockedReadFileAsString.mockResolvedValue({ |
| content: mockText, |
| bytes: mockBytes, |
| }); |
|
|
| mockedAxios.get.mockResolvedValue({ |
| status: 500, |
| statusText: 'Internal Server Error', |
| }); |
|
|
| const result = await parseText({ |
| req: mockReq, |
| file: mockFile, |
| file_id: mockFileId, |
| }); |
|
|
| expect(result).toEqual({ |
| text: mockText, |
| bytes: mockBytes, |
| source: FileSources.text, |
| }); |
| }); |
|
|
| it('should accept empty text as valid RAG API response', async () => { |
| process.env.RAG_API_URL = 'http://rag-api.test'; |
|
|
| mockedAxios.get.mockResolvedValue({ |
| status: 200, |
| statusText: 'OK', |
| }); |
|
|
| mockedAxios.post.mockResolvedValue({ |
| data: { |
| text: '', |
| }, |
| }); |
|
|
| const result = await parseText({ |
| req: mockReq, |
| file: mockFile, |
| file_id: mockFileId, |
| }); |
|
|
| expect(mockedAxios.post).toHaveBeenCalledWith( |
| 'http://rag-api.test/text', |
| expect.any(Object), |
| expect.objectContaining({ |
| timeout: 300000, |
| }), |
| ); |
| expect(result).toEqual({ |
| text: '', |
| bytes: 0, |
| source: FileSources.text, |
| }); |
| }); |
|
|
| it('should fall back to native parsing when RAG API response lacks text property', async () => { |
| process.env.RAG_API_URL = 'http://rag-api.test'; |
| const mockText = 'Native parsing result'; |
| const mockBytes = Buffer.byteLength(mockText, 'utf8'); |
|
|
| mockedReadFileAsString.mockResolvedValue({ |
| content: mockText, |
| bytes: mockBytes, |
| }); |
|
|
| mockedAxios.get.mockResolvedValue({ |
| status: 200, |
| statusText: 'OK', |
| }); |
|
|
| mockedAxios.post.mockResolvedValue({ |
| data: {}, |
| }); |
|
|
| const result = await parseText({ |
| req: mockReq, |
| file: mockFile, |
| file_id: mockFileId, |
| }); |
|
|
| expect(result).toEqual({ |
| text: mockText, |
| bytes: mockBytes, |
| source: FileSources.text, |
| }); |
| }); |
|
|
| it('should fall back to native parsing when user is undefined', async () => { |
| process.env.RAG_API_URL = 'http://rag-api.test'; |
| const mockText = 'Native parsing result'; |
| const mockBytes = Buffer.byteLength(mockText, 'utf8'); |
|
|
| mockedReadFileAsString.mockResolvedValue({ |
| content: mockText, |
| bytes: mockBytes, |
| }); |
|
|
| const result = await parseText({ |
| req: { user: undefined } as ServerRequest, |
| file: mockFile, |
| file_id: mockFileId, |
| }); |
|
|
| expect(mockedGenerateShortLivedToken).not.toHaveBeenCalled(); |
| expect(mockedAxios.get).not.toHaveBeenCalled(); |
| expect(mockedAxios.post).not.toHaveBeenCalled(); |
| expect(result).toEqual({ |
| text: mockText, |
| bytes: mockBytes, |
| source: FileSources.text, |
| }); |
| }); |
| }); |
| }); |
|
|