const { Tools, StepEvents } = 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(), writeAttachmentEvent: jest.fn(), GenerationJobManager: { emitChunk: jest.fn(), }, HOST_FILE_AUTHORING_ARTIFACT_KEY: '__librechat_file_authoring', getToolInputValidationDetails: jest.fn((result, validationError) => validationError != null ? { toolName: result.tool_call.name, reason: 'option_label_too_long', fieldPath: validationError.fieldPath, } : null, ), isCodeSessionToolName: jest.fn((name) => ['execute_code', 'bash_tool', 'read_file'].includes(name), ), })); jest.mock('@librechat/data-schemas', () => ({ logger: { debug: jest.fn(), 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/Files/Code/preflight', () => ({ preflightCodeOutputBatch: jest.fn(async ({ artifact }) => (artifact.files ?? []) .filter((file) => file.inherited !== true) .map((file) => ({ file, sessionId: file.storage_session_id ?? artifact.session_id, })), ), })); jest.mock('~/server/services/Tools/credentials', () => ({ loadAuthValues: jest.fn(), })); jest.mock('~/server/services/Files/process', () => ({ saveBase64Image: jest.fn(), })); describe('resumable event generation fencing', () => { beforeEach(() => { jest.clearAllMocks(); }); it('forwards the originating job epoch with run-step events', async () => { const { GenerationJobManager } = require('@librechat/api'); const { GraphEvents } = jest.requireActual('@librechat/agents'); const { getDefaultHandlers } = require('../callbacks'); const data = { id: 'step-1', index: 0, stepDetails: { type: 'tool_calls', tool_calls: [{ id: 'call-1', name: 'approval_probe', args: '{}' }], }, }; const handlers = getDefaultHandlers({ res: { write: jest.fn() }, aggregateContent: jest.fn(), toolEndCallback: jest.fn(), collectedUsage: [], streamId: 'conversation-1', jobCreatedAt: 1234, }); await handlers[GraphEvents.ON_RUN_STEP].handle(GraphEvents.ON_RUN_STEP, data); expect(GenerationJobManager.emitChunk).toHaveBeenCalledWith( 'conversation-1', { event: GraphEvents.ON_RUN_STEP, data }, { expectedCreatedAt: 1234 }, ); }); it('resolves MCP identity from a function-shaped root tool call', async () => { const { GraphEvents } = jest.requireActual('@librechat/agents'); const { getDefaultHandlers } = require('../callbacks'); const resolveMcpServerName = jest.fn(() => 'server'); const data = { id: 'step-function-tool', index: 0, stepDetails: { type: 'tool_calls', tool_calls: [ { id: 'call-function-tool', function: { name: 'lookup_mcp_server', arguments: '{}' }, }, ], }, }; const handlers = getDefaultHandlers({ res: { write: jest.fn() }, aggregateContent: jest.fn(), toolEndCallback: jest.fn(), collectedUsage: [], resolveMcpServerName, }); await handlers[GraphEvents.ON_RUN_STEP].handle(GraphEvents.ON_RUN_STEP, data, { agent_id: 'lazy-agent', }); expect(resolveMcpServerName).toHaveBeenCalledWith('lookup_mcp_server', 'lazy-agent'); expect(data.stepDetails.tool_calls[0]).toEqual( expect.objectContaining({ name: 'lookup_mcp_server', mcpServerName: 'server' }), ); }); it('publishes root event-child progress through the child activity transport', async () => { const { nanoid } = require('nanoid'); nanoid.mockReturnValueOnce('invocation-1').mockReturnValueOnce('invocation-2'); const { GraphEvents } = jest.requireActual('@librechat/agents'); const { getDefaultHandlers } = require('../callbacks'); const publish = jest.fn().mockResolvedValue(undefined); const data = { id: 'step-1', index: 0, stepDetails: { type: 'message_creation' }, }; const handlers = getDefaultHandlers({ res: { write: jest.fn() }, aggregateContent: jest.fn(), toolEndCallback: jest.fn(), collectedUsage: [], streamId: 'event-thread', jobCreatedAt: 1234, eventChildActivity: { runId: 'event-thread', parentRunId: 'parent-conversation', subagentRunId: 'delivery-1', subagentType: 'agent-1', subagentAgentId: 'agent-1', parentAgentId: 'director', publish, }, }); await handlers[GraphEvents.ON_RUN_STEP].handle(GraphEvents.ON_RUN_STEP, data); await Promise.resolve(); await Promise.resolve(); const firstUpdate = publish.mock.calls[0][0]; expect(firstUpdate).toEqual( expect.objectContaining({ runId: 'event-thread', parentRunId: 'parent-conversation', subagentRunId: 'delivery-1', phase: 'run_step', activityEventId: expect.stringMatching(/^delivery-1:.+:0$/), data, }), ); expect(firstUpdate).not.toHaveProperty('activitySequence'); const resumedPublish = jest.fn().mockResolvedValue(undefined); const resumedHandlers = getDefaultHandlers({ res: { write: jest.fn() }, aggregateContent: jest.fn(), toolEndCallback: jest.fn(), collectedUsage: [], streamId: 'event-thread', jobCreatedAt: 1234, eventChildActivity: { runId: 'event-thread', parentRunId: 'parent-conversation', subagentRunId: 'delivery-1', subagentType: 'agent-1', subagentAgentId: 'agent-1', parentAgentId: 'director', publish: resumedPublish, }, }); await resumedHandlers[GraphEvents.ON_RUN_STEP].handle(GraphEvents.ON_RUN_STEP, data); await Promise.resolve(); await Promise.resolve(); expect(resumedPublish.mock.calls[0][0].activityEventId).not.toBe(firstUpdate.activityEventId); }); it('forwards the originating job epoch with deferred attachments', () => { const { GenerationJobManager } = require('@librechat/api'); const { createAttachmentEmitter } = require('../callbacks'); const attachment = { file_id: 'file-1', status: 'ready' }; const emitAttachment = createAttachmentEmitter({ res: { write: jest.fn() }, streamId: 'conversation-1', jobCreatedAt: 1234, }); emitAttachment(attachment); expect(GenerationJobManager.emitChunk).toHaveBeenCalledWith( 'conversation-1', { event: 'attachment', data: attachment }, { expectedCreatedAt: 1234 }, ); }); }); describe('createPtcProgressEmitter', () => { const ptcEvent = { tool_call_id: 'call_ptc', call_id: 'call_ptc:0', name: 'read_file', status: 'running', args: 'path=a.ts', }; beforeEach(() => jest.clearAllMocks()); it('emits the inner tool-call event on the resumable job stream', () => { const { GenerationJobManager } = require('@librechat/api'); const { createPtcProgressEmitter } = require('../callbacks'); const emit = createPtcProgressEmitter({ res: { write: jest.fn() }, streamId: 'conversation-1', jobCreatedAt: 1234, }); emit(ptcEvent); expect(GenerationJobManager.emitChunk).toHaveBeenCalledWith( 'conversation-1', { event: StepEvents.ON_PTC_TOOL_CALL, data: ptcEvent }, { expectedCreatedAt: 1234 }, ); }); it('writes to the live response when no stream id is in play', () => { const { sendEvent } = require('@librechat/api'); const { createPtcProgressEmitter } = require('../callbacks'); const res = { write: jest.fn(), headersSent: true, writableEnded: false }; const emit = createPtcProgressEmitter({ res }); emit(ptcEvent); expect(sendEvent).toHaveBeenCalledWith(res, { event: StepEvents.ON_PTC_TOOL_CALL, data: ptcEvent, }); }); it('absorbs a rejected resumable emit instead of leaving an unhandled rejection', async () => { const { GenerationJobManager } = require('@librechat/api'); const { createPtcProgressEmitter } = require('../callbacks'); GenerationJobManager.emitChunk.mockRejectedValueOnce(new Error('transport down')); const unhandled = jest.fn(); process.on('unhandledRejection', unhandled); const emit = createPtcProgressEmitter({ res: { write: jest.fn() }, streamId: 'conversation-1', jobCreatedAt: 1234, }); expect(() => emit(ptcEvent)).not.toThrow(); await new Promise((resolve) => setImmediate(resolve)); process.off('unhandledRejection', unhandled); expect(unhandled).not.toHaveBeenCalled(); }); it('drops the event once the response has closed', () => { const { sendEvent } = require('@librechat/api'); const { createPtcProgressEmitter } = require('../callbacks'); const emit = createPtcProgressEmitter({ res: { write: jest.fn(), headersSent: true, writableEnded: true }, }); emit(ptcEvent); expect(sendEvent).not.toHaveBeenCalled(); }); }); 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'); const { preflightCodeOutputBatch } = require('~/server/services/Files/Code/preflight'); function makeCodeExecutionEvent({ runId, threadId, toolCallId, fileId, name, toolName = 'execute_code', hostFileAuthoring = false, created, codeExecutionContext, }) { return { output: { name: toolName, tool_call_id: toolCallId, artifact: { ...(hostFileAuthoring ? { __librechat_file_authoring: true } : {}), ...(created === undefined ? {} : { created }), path: name, session_id: 'sess-1', files: [{ id: fileId, name, session_id: 'sess-1' }], }, }, metadata: { run_id: runId, thread_id: threadId, codeExecutionContext }, }; } /** 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: '