jest.mock('uuid', () => ({ v4: jest.fn(() => 'mock-uuid') })); jest.mock('@librechat/data-schemas', () => ({ logger: { warn: jest.fn(), debug: jest.fn(), error: jest.fn() }, })); jest.mock('@librechat/agents', () => ({ EnvVar: { CODE_API_KEY: 'CODE_API_KEY' }, })); jest.mock('@librechat/api', () => ({ sanitizeFilename: jest.fn((n) => n), parseText: jest.fn().mockResolvedValue({ text: '', bytes: 0 }), processAudioFile: jest.fn(), })); jest.mock('librechat-data-provider', () => ({ ...jest.requireActual('librechat-data-provider'), mergeFileConfig: jest.fn(), })); jest.mock('~/server/services/Files/images', () => ({ convertImage: jest.fn(), resizeAndConvert: jest.fn(), resizeImageBuffer: jest.fn(), })); jest.mock('~/server/controllers/assistants/v2', () => ({ addResourceFileId: jest.fn(), deleteResourceFileId: jest.fn(), })); jest.mock('~/models/Agent', () => ({ addAgentResourceFile: jest.fn().mockResolvedValue({}), removeAgentResourceFiles: jest.fn(), })); jest.mock('~/server/controllers/assistants/helpers', () => ({ getOpenAIClient: jest.fn(), })); jest.mock('~/server/services/Tools/credentials', () => ({ loadAuthValues: jest.fn(), })); jest.mock('~/models', () => ({ createFile: jest.fn().mockResolvedValue({ file_id: 'created-file-id' }), updateFileUsage: jest.fn(), deleteFiles: jest.fn(), })); jest.mock('~/server/utils/getFileStrategy', () => ({ getFileStrategy: jest.fn().mockReturnValue('local'), })); jest.mock('~/server/services/Config', () => ({ checkCapability: jest.fn().mockResolvedValue(true), })); jest.mock('~/server/utils/queue', () => ({ LB_QueueAsyncCall: jest.fn(), })); jest.mock('~/server/services/Files/strategies', () => ({ getStrategyFunctions: jest.fn(), })); jest.mock('~/server/utils', () => ({ determineFileType: jest.fn(), })); jest.mock('~/server/services/Files/Audio/STTService', () => ({ STTService: { getInstance: jest.fn() }, })); const { EToolResources, FileSources, AgentCapabilities } = require('librechat-data-provider'); const { mergeFileConfig } = require('librechat-data-provider'); const { checkCapability } = require('~/server/services/Config'); const { getStrategyFunctions } = require('~/server/services/Files/strategies'); const { processAgentFileUpload } = require('./process'); const PDF_MIME = 'application/pdf'; const DOCX_MIME = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'; const XLSX_MIME = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; const XLS_MIME = 'application/vnd.ms-excel'; const makeReq = ({ mimetype = PDF_MIME, ocrConfig = null } = {}) => ({ user: { id: 'user-123' }, file: { path: '/tmp/upload.bin', originalname: 'upload.bin', filename: 'upload-uuid.bin', mimetype, }, body: { model: 'gpt-4o' }, config: { fileConfig: {}, fileStrategy: 'local', ocr: ocrConfig, }, }); const makeMetadata = () => ({ agent_id: 'agent-abc', tool_resource: EToolResources.context, file_id: 'file-uuid-123', }); const mockRes = { status: jest.fn().mockReturnThis(), json: jest.fn().mockReturnValue({}), }; const makeFileConfig = ({ ocrSupportedMimeTypes = [] } = {}) => ({ checkType: (mime, types) => (types ?? []).includes(mime), ocr: { supportedMimeTypes: ocrSupportedMimeTypes }, stt: { supportedMimeTypes: [] }, text: { supportedMimeTypes: [] }, }); describe('processAgentFileUpload', () => { beforeEach(() => { jest.clearAllMocks(); mockRes.status.mockReturnThis(); mockRes.json.mockReturnValue({}); checkCapability.mockResolvedValue(true); getStrategyFunctions.mockReturnValue({ handleFileUpload: jest .fn() .mockResolvedValue({ text: 'extracted text', bytes: 42, filepath: 'doc://result' }), }); mergeFileConfig.mockReturnValue(makeFileConfig()); }); describe('OCR strategy selection', () => { test.each([ ['PDF', PDF_MIME], ['DOCX', DOCX_MIME], ['XLSX', XLSX_MIME], ['XLS', XLS_MIME], ])('uses document_parser automatically for %s when no OCR is configured', async (_, mime) => { mergeFileConfig.mockReturnValue(makeFileConfig()); const req = makeReq({ mimetype: mime, ocrConfig: null }); await processAgentFileUpload({ req, res: mockRes, metadata: makeMetadata() }); expect(getStrategyFunctions).toHaveBeenCalledWith(FileSources.document_parser); }); test('does not check OCR capability when using automatic document_parser fallback', async () => { const req = makeReq({ mimetype: PDF_MIME, ocrConfig: null }); await processAgentFileUpload({ req, res: mockRes, metadata: makeMetadata() }); expect(checkCapability).not.toHaveBeenCalledWith(expect.anything(), AgentCapabilities.ocr); expect(getStrategyFunctions).toHaveBeenCalledWith(FileSources.document_parser); }); test('uses the configured OCR strategy when OCR is set up for the file type', async () => { mergeFileConfig.mockReturnValue(makeFileConfig({ ocrSupportedMimeTypes: [PDF_MIME] })); const req = makeReq({ mimetype: PDF_MIME, ocrConfig: { strategy: FileSources.mistral_ocr }, }); await processAgentFileUpload({ req, res: mockRes, metadata: makeMetadata() }); expect(checkCapability).toHaveBeenCalledWith(expect.anything(), AgentCapabilities.ocr); expect(getStrategyFunctions).toHaveBeenCalledWith(FileSources.mistral_ocr); }); test('uses document_parser as default when OCR is configured but no strategy is specified', async () => { mergeFileConfig.mockReturnValue(makeFileConfig({ ocrSupportedMimeTypes: [PDF_MIME] })); const req = makeReq({ mimetype: PDF_MIME, ocrConfig: { supportedMimeTypes: [PDF_MIME] }, }); await processAgentFileUpload({ req, res: mockRes, metadata: makeMetadata() }); expect(checkCapability).toHaveBeenCalledWith(expect.anything(), AgentCapabilities.ocr); expect(getStrategyFunctions).toHaveBeenCalledWith(FileSources.document_parser); }); test('throws when configured OCR capability is not enabled for the agent', async () => { mergeFileConfig.mockReturnValue(makeFileConfig({ ocrSupportedMimeTypes: [PDF_MIME] })); checkCapability.mockResolvedValue(false); const req = makeReq({ mimetype: PDF_MIME, ocrConfig: { strategy: FileSources.mistral_ocr }, }); await expect( processAgentFileUpload({ req, res: mockRes, metadata: makeMetadata() }), ).rejects.toThrow('OCR capability is not enabled for Agents'); }); test('uses document_parser (no capability check) when OCR capability returns false but no OCR config', async () => { checkCapability.mockResolvedValue(false); const req = makeReq({ mimetype: PDF_MIME, ocrConfig: null }); await processAgentFileUpload({ req, res: mockRes, metadata: makeMetadata() }); expect(checkCapability).not.toHaveBeenCalledWith(expect.anything(), AgentCapabilities.ocr); expect(getStrategyFunctions).toHaveBeenCalledWith(FileSources.document_parser); }); test('uses document_parser when OCR is configured but the file type is not in OCR supported types', async () => { mergeFileConfig.mockReturnValue(makeFileConfig({ ocrSupportedMimeTypes: [PDF_MIME] })); const req = makeReq({ mimetype: DOCX_MIME, ocrConfig: { strategy: FileSources.mistral_ocr }, }); await processAgentFileUpload({ req, res: mockRes, metadata: makeMetadata() }); expect(checkCapability).not.toHaveBeenCalledWith(expect.anything(), AgentCapabilities.ocr); expect(getStrategyFunctions).toHaveBeenCalledWith(FileSources.document_parser); expect(getStrategyFunctions).not.toHaveBeenCalledWith(FileSources.mistral_ocr); }); test('does not invoke any OCR strategy for unsupported MIME types without OCR config', async () => { const req = makeReq({ mimetype: 'text/plain', ocrConfig: null }); await expect( processAgentFileUpload({ req, res: mockRes, metadata: makeMetadata() }), ).rejects.toThrow('File type text/plain is not supported for text parsing.'); expect(getStrategyFunctions).not.toHaveBeenCalled(); }); test('throws instead of falling back to parseText when document_parser fails for a document MIME type', async () => { getStrategyFunctions.mockReturnValue({ handleFileUpload: jest.fn().mockRejectedValue(new Error('No text found in document')), }); const req = makeReq({ mimetype: PDF_MIME, ocrConfig: null }); const { parseText } = require('@librechat/api'); await expect( processAgentFileUpload({ req, res: mockRes, metadata: makeMetadata() }), ).rejects.toThrow(/image-based and requires an OCR service/); expect(parseText).not.toHaveBeenCalled(); }); test('falls back to document_parser when configured OCR fails for a document MIME type', async () => { mergeFileConfig.mockReturnValue(makeFileConfig({ ocrSupportedMimeTypes: [PDF_MIME] })); const failingUpload = jest.fn().mockRejectedValue(new Error('OCR API returned 500')); const fallbackUpload = jest .fn() .mockResolvedValue({ text: 'parsed text', bytes: 11, filepath: 'doc://result' }); getStrategyFunctions .mockReturnValueOnce({ handleFileUpload: failingUpload }) .mockReturnValueOnce({ handleFileUpload: fallbackUpload }); const req = makeReq({ mimetype: PDF_MIME, ocrConfig: { strategy: FileSources.mistral_ocr }, }); await expect( processAgentFileUpload({ req, res: mockRes, metadata: makeMetadata() }), ).resolves.not.toThrow(); expect(getStrategyFunctions).toHaveBeenCalledWith(FileSources.mistral_ocr); expect(getStrategyFunctions).toHaveBeenCalledWith(FileSources.document_parser); }); test('throws when both configured OCR and document_parser fallback fail', async () => { mergeFileConfig.mockReturnValue(makeFileConfig({ ocrSupportedMimeTypes: [PDF_MIME] })); getStrategyFunctions.mockReturnValue({ handleFileUpload: jest.fn().mockRejectedValue(new Error('failure')), }); const req = makeReq({ mimetype: PDF_MIME, ocrConfig: { strategy: FileSources.mistral_ocr }, }); const { parseText } = require('@librechat/api'); await expect( processAgentFileUpload({ req, res: mockRes, metadata: makeMetadata() }), ).rejects.toThrow(/image-based and requires an OCR service/); expect(parseText).not.toHaveBeenCalled(); }); }); describe('text size guard', () => { test('throws before writing to MongoDB when extracted text exceeds 15MB', async () => { const oversizedText = 'x'.repeat(15 * 1024 * 1024 + 1); getStrategyFunctions.mockReturnValue({ handleFileUpload: jest.fn().mockResolvedValue({ text: oversizedText, bytes: Buffer.byteLength(oversizedText, 'utf8'), filepath: 'doc://result', }), }); const req = makeReq({ mimetype: PDF_MIME, ocrConfig: null }); const { createFile } = require('~/models'); await expect( processAgentFileUpload({ req, res: mockRes, metadata: makeMetadata() }), ).rejects.toThrow(/exceeds the 15MB storage limit/); expect(createFile).not.toHaveBeenCalled(); }); test('succeeds when extracted text is within the 15MB limit', async () => { const okText = 'x'.repeat(1024); getStrategyFunctions.mockReturnValue({ handleFileUpload: jest.fn().mockResolvedValue({ text: okText, bytes: Buffer.byteLength(okText, 'utf8'), filepath: 'doc://result', }), }); const req = makeReq({ mimetype: PDF_MIME, ocrConfig: null }); await expect( processAgentFileUpload({ req, res: mockRes, metadata: makeMetadata() }), ).resolves.not.toThrow(); }); }); });