const { Tools } = require('librechat-data-provider'); // Mock all dependencies before requiring the module jest.mock('nanoid', () => ({ nanoid: jest.fn(() => 'mock-id'), })); jest.mock('@librechat/api', () => ({ sendEvent: jest.fn(), HOST_FILE_AUTHORING_ARTIFACT_KEY: '__librechat_file_authoring', isCodeSessionToolName: jest.fn((name) => ['execute_code', 'bash_tool', 'read_file'].includes(name), ), })); jest.mock('@librechat/data-schemas', () => ({ logger: { error: jest.fn(), }, })); jest.mock('@librechat/agents', () => ({ ...jest.requireActual('@librechat/agents'), getMessageId: jest.fn(), ToolEndHandler: jest.fn(), handleToolCalls: jest.fn(), })); jest.mock('~/server/services/Files/Citations', () => ({ processFileCitations: jest.fn(), })); jest.mock('~/server/services/Files/Code/process', () => ({ processCodeOutput: jest.fn(), /* `runPreviewFinalize` is the runtime pairing for `finalize` (defined * alongside processCodeOutput in process.js). The callback wires * the deferred render through it; reproduce the basic happy-path here so the * SSE-emit assertions still work. The catch/defensive-updateFile * branch is unit-tested directly against the real helper in * process.spec.js — exercising it here would add test coupling * without coverage benefit. */ runPreviewFinalize: ({ finalize, onResolved }) => { if (typeof finalize !== 'function') { return; } finalize() .then((updated) => { if (!updated || !onResolved) { return; } onResolved(updated); }) .catch(() => { /* swallowed in the mock — see process.spec.js for catch coverage */ }); }, })); jest.mock('~/server/services/Tools/credentials', () => ({ loadAuthValues: jest.fn(), })); jest.mock('~/server/services/Files/process', () => ({ saveBase64Image: jest.fn(), })); describe('createToolEndCallback', () => { let req, res, artifactPromises, createToolEndCallback; let logger; beforeEach(() => { jest.clearAllMocks(); // Get the mocked logger logger = require('@librechat/data-schemas').logger; // Now require the module after all mocks are set up const callbacks = require('../callbacks'); createToolEndCallback = callbacks.createToolEndCallback; req = { user: { id: 'user123' }, }; res = { headersSent: false, write: jest.fn(), }; artifactPromises = []; }); describe('ui_resources artifact handling', () => { it('should process ui_resources artifact and return attachment when headers not sent', async () => { const toolEndCallback = createToolEndCallback({ req, res, artifactPromises }); const output = { tool_call_id: 'tool123', artifact: { [Tools.ui_resources]: { data: [ { type: 'button', label: 'Click me' }, { type: 'input', placeholder: 'Enter text' }, ], }, }, }; const metadata = { run_id: 'run456', thread_id: 'thread789', }; await toolEndCallback({ output }, metadata); // Wait for all promises to resolve const results = await Promise.all(artifactPromises); // When headers are not sent, it returns attachment without writing expect(res.write).not.toHaveBeenCalled(); const attachment = results[0]; expect(attachment).toEqual({ type: Tools.ui_resources, messageId: 'run456', toolCallId: 'tool123', conversationId: 'thread789', [Tools.ui_resources]: [ { type: 'button', label: 'Click me' }, { type: 'input', placeholder: 'Enter text' }, ], }); }); it('should write to response when headers are already sent', async () => { res.headersSent = true; const toolEndCallback = createToolEndCallback({ req, res, artifactPromises }); const output = { tool_call_id: 'tool123', artifact: { [Tools.ui_resources]: { data: [{ type: 'carousel', items: [] }], }, }, }; const metadata = { run_id: 'run456', thread_id: 'thread789', }; await toolEndCallback({ output }, metadata); const results = await Promise.all(artifactPromises); expect(res.write).toHaveBeenCalled(); expect(results[0]).toEqual({ type: Tools.ui_resources, messageId: 'run456', toolCallId: 'tool123', conversationId: 'thread789', [Tools.ui_resources]: [{ type: 'carousel', items: [] }], }); }); it('should handle errors when processing ui_resources', async () => { const toolEndCallback = createToolEndCallback({ req, res, artifactPromises }); // Mock res.write to throw an error res.headersSent = true; res.write.mockImplementation(() => { throw new Error('Write failed'); }); const output = { tool_call_id: 'tool123', artifact: { [Tools.ui_resources]: { data: [{ type: 'test' }], }, }, }; const metadata = { run_id: 'run456', thread_id: 'thread789', }; await toolEndCallback({ output }, metadata); const results = await Promise.all(artifactPromises); expect(logger.error).toHaveBeenCalledWith( 'Error processing artifact content:', expect.any(Error), ); expect(results[0]).toBeNull(); }); it('should handle multiple artifacts including ui_resources', async () => { const toolEndCallback = createToolEndCallback({ req, res, artifactPromises }); const output = { tool_call_id: 'tool123', artifact: { [Tools.ui_resources]: { data: [{ type: 'chart', data: [] }], }, [Tools.web_search]: { results: ['result1', 'result2'], }, }, }; const metadata = { run_id: 'run456', thread_id: 'thread789', }; await toolEndCallback({ output }, metadata); const results = await Promise.all(artifactPromises); // Both ui_resources and web_search should be processed expect(artifactPromises).toHaveLength(2); expect(results).toHaveLength(2); // Check ui_resources attachment const uiResourceAttachment = results.find((r) => r?.type === Tools.ui_resources); expect(uiResourceAttachment).toBeTruthy(); expect(uiResourceAttachment[Tools.ui_resources]).toEqual([{ type: 'chart', data: [] }]); // Check web_search attachment const webSearchAttachment = results.find((r) => r?.type === Tools.web_search); expect(webSearchAttachment).toBeTruthy(); expect(webSearchAttachment[Tools.web_search]).toEqual({ results: ['result1', 'result2'], }); }); it('should not process artifacts when output has no artifacts', async () => { const toolEndCallback = createToolEndCallback({ req, res, artifactPromises }); const output = { tool_call_id: 'tool123', content: 'Some regular content', // No artifact property }; const metadata = { run_id: 'run456', thread_id: 'thread789', }; await toolEndCallback({ output }, metadata); expect(artifactPromises).toHaveLength(0); expect(res.write).not.toHaveBeenCalled(); }); }); describe('edge cases', () => { it('should handle empty ui_resources data object', async () => { const toolEndCallback = createToolEndCallback({ req, res, artifactPromises }); const output = { tool_call_id: 'tool123', artifact: { [Tools.ui_resources]: { data: [], }, }, }; const metadata = { run_id: 'run456', thread_id: 'thread789', }; await toolEndCallback({ output }, metadata); const results = await Promise.all(artifactPromises); expect(results[0]).toEqual({ type: Tools.ui_resources, messageId: 'run456', toolCallId: 'tool123', conversationId: 'thread789', [Tools.ui_resources]: [], }); }); it('should handle ui_resources with complex nested data', async () => { const toolEndCallback = createToolEndCallback({ req, res, artifactPromises }); const complexData = { 0: { type: 'form', fields: [ { name: 'field1', type: 'text', required: true }, { name: 'field2', type: 'select', options: ['a', 'b', 'c'] }, ], nested: { deep: { value: 123, array: [1, 2, 3], }, }, }, }; const output = { tool_call_id: 'tool123', artifact: { [Tools.ui_resources]: { data: complexData, }, }, }; const metadata = { run_id: 'run456', thread_id: 'thread789', }; await toolEndCallback({ output }, metadata); const results = await Promise.all(artifactPromises); expect(results[0][Tools.ui_resources]).toEqual(complexData); }); it('should handle when output is undefined', async () => { const toolEndCallback = createToolEndCallback({ req, res, artifactPromises }); const metadata = { run_id: 'run456', thread_id: 'thread789', }; await toolEndCallback({ output: undefined }, metadata); expect(artifactPromises).toHaveLength(0); expect(res.write).not.toHaveBeenCalled(); }); it('should handle when data parameter is undefined', async () => { const toolEndCallback = createToolEndCallback({ req, res, artifactPromises }); const metadata = { run_id: 'run456', thread_id: 'thread789', }; await toolEndCallback(undefined, metadata); expect(artifactPromises).toHaveLength(0); expect(res.write).not.toHaveBeenCalled(); }); }); describe('code execution deferred-preview emit', () => { /* The deferred-preview code-execution flow emits the attachment twice over * SSE: the initial emit with `status: 'pending'` and the current run's * messageId, the deferred render with the resolved record. The preview update emit * must use the CURRENT run's messageId (not the persisted DB one) * because `processCodeOutput` intentionally preserves the original * `messageId` on cross-turn filename reuse — `getCodeGeneratedFiles` * needs that for prior-turn priming. * * Codex P1 review on PR #12957: shipping `updated.messageId` * straight from the DB record routed preview-update patches to the wrong * message slot, leaving the current turn's pending chip stuck. */ const { processCodeOutput } = require('~/server/services/Files/Code/process'); function makeCodeExecutionEvent({ runId, threadId, toolCallId, fileId, name, toolName = 'execute_code', hostFileAuthoring = false, }) { return { output: { name: toolName, tool_call_id: toolCallId, artifact: { ...(hostFileAuthoring ? { __librechat_file_authoring: true } : {}), session_id: 'sess-1', files: [{ id: fileId, name, session_id: 'sess-1' }], }, }, metadata: { run_id: runId, thread_id: threadId }, }; } /** Parse the SSE frame `res.write` produces back to a payload object. */ function parseSseAttachment(call) { const frame = call[0]; const dataLine = frame.split('\n').find((l) => l.startsWith('data: ')); return JSON.parse(dataLine.slice('data: '.length)); } it('the preview update emit uses the current run messageId, not the persisted DB messageId (cross-turn filename reuse)', async () => { /* Simulate turn-2 reusing `output.csv` from turn-1. The DB record * surfaced by `updateFile` carries the original `turn-1-msg` * messageId; the runtime emit must rewrite to `turn-2-msg`. */ res.headersSent = true; const finalize = jest.fn().mockResolvedValue({ file_id: 'fid-shared', filename: 'output.csv', filepath: '/uploads/output.csv', type: 'text/csv', conversationId: 'thread789', messageId: 'turn-1-original-msg', // persisted DB id (older turn) status: 'ready', text: '