import { describe, it, expect, vi, beforeEach } from 'vitest'; import { FileToolModifier } from '#file_tool_modifier'; import ComposioClient from '@composio/client'; import { Tool } from '../../src/types/tool.types'; import { ComposioFileUploadAbortedError, ComposioFileUploadError, } from '../../src/errors/FileModifierErrors'; import { ComposioRequestCancelledError } from '../../src/errors/SDKErrors'; import * as fileUtils from '../../src/utils/fileUtils.node'; import { Tools } from '../../src/models/Tools'; import { createTestContext, setupTest, mockToolExecution } from '../utils/toolExecuteUtils'; import { mockClient } from '../utils/mocks/client.mock'; // Mock the fileUtils module vi.mock('../../src/utils/fileUtils.node', () => ({ downloadFileFromS3: vi.fn(), getFileDataAfterUploadingToS3: vi.fn(), isHttpUrl: (value: string): boolean => { try { const url = new URL(value); return url.protocol === 'http:' || url.protocol === 'https:'; } catch { return false; } }, })); describe('FileToolModifier', () => { let fileToolModifier: FileToolModifier; let mockClient: ComposioClient; beforeEach(() => { mockClient = { baseURL: 'https://api.composio.dev', apiKey: 'test-api-key', } as ComposioClient; fileToolModifier = new FileToolModifier(mockClient); vi.clearAllMocks(); }); describe('modifyToolSchema', () => { it('should return schema unchanged if no input parameters', async () => { const schema: Tool = { slug: 'test-tool', name: 'Test Tool', description: 'A test tool', tags: ['test'], version: '20251201_01', availableVersions: ['20251201_01'], }; const result = await fileToolModifier.modifyToolSchema(schema); expect(result).toEqual(schema); }); it('should add format: "path" to file_uploadable properties', async () => { const schema: Tool = { slug: 'test-tool', name: 'Test Tool', description: 'A test tool', tags: ['test'], version: '20251201_01', availableVersions: ['20251201_01'], inputParameters: { type: 'object', properties: { file: { type: 'string', file_uploadable: true, }, text: { type: 'string', }, }, }, }; const result = await fileToolModifier.modifyToolSchema(schema); expect(result.inputParameters?.properties?.file).toHaveProperty('format', 'path'); expect(result.inputParameters?.properties?.text).not.toHaveProperty('format'); }); it('should transform file_uploadable properties inside anyOf', async () => { const schema: Tool = { slug: 'test-tool', name: 'Test Tool', description: 'A test tool', tags: ['test'], version: '20251201_01', availableVersions: ['20251201_01'], inputParameters: { type: 'object', properties: { fileInput: { anyOf: [ { type: 'string', file_uploadable: true, description: 'File path', }, { type: 'null', }, ], }, text: { type: 'string', }, }, }, }; const result = await fileToolModifier.modifyToolSchema(schema); expect(result.inputParameters?.properties?.fileInput?.anyOf?.[0]).toHaveProperty( 'format', 'path' ); expect(result.inputParameters?.properties?.fileInput?.anyOf?.[0]).toHaveProperty( 'file_uploadable', true ); expect(result.inputParameters?.properties?.fileInput?.anyOf?.[1]).not.toHaveProperty( 'format' ); expect(result.inputParameters?.properties?.text).not.toHaveProperty('format'); }); it('should transform file_uploadable properties inside oneOf', async () => { const schema: Tool = { slug: 'test-tool', name: 'Test Tool', description: 'A test tool', tags: ['test'], version: '20251201_01', availableVersions: ['20251201_01'], inputParameters: { type: 'object', properties: { fileInput: { oneOf: [ { type: 'string', file_uploadable: true, title: 'Upload File', }, { type: 'string', description: 'URL reference', }, ], }, }, }, }; const result = await fileToolModifier.modifyToolSchema(schema); expect(result.inputParameters?.properties?.fileInput?.oneOf?.[0]).toHaveProperty( 'format', 'path' ); expect(result.inputParameters?.properties?.fileInput?.oneOf?.[0]).toHaveProperty( 'file_uploadable', true ); expect(result.inputParameters?.properties?.fileInput?.oneOf?.[1]).not.toHaveProperty( 'format' ); }); it('should transform file_uploadable properties inside allOf', async () => { const schema: Tool = { slug: 'test-tool', name: 'Test Tool', description: 'A test tool', tags: ['test'], version: '20251201_01', availableVersions: ['20251201_01'], inputParameters: { type: 'object', properties: { fileInput: { allOf: [ { type: 'string', file_uploadable: true, }, { minLength: 1, }, ], }, }, }, }; const result = await fileToolModifier.modifyToolSchema(schema); expect(result.inputParameters?.properties?.fileInput?.allOf?.[0]).toHaveProperty( 'format', 'path' ); expect(result.inputParameters?.properties?.fileInput?.allOf?.[0]).toHaveProperty( 'file_uploadable', true ); expect(result.inputParameters?.properties?.fileInput?.allOf?.[1]).not.toHaveProperty( 'format' ); }); it('should transform nested file_uploadable properties inside anyOf with objects', async () => { const schema: Tool = { slug: 'test-tool', name: 'Test Tool', description: 'A test tool', tags: ['test'], version: '20251201_01', availableVersions: ['20251201_01'], inputParameters: { type: 'object', properties: { content: { anyOf: [ { type: 'object', properties: { attachment: { type: 'string', file_uploadable: true, }, }, }, { type: 'string', }, ], }, }, }, }; const result = await fileToolModifier.modifyToolSchema(schema); expect( result.inputParameters?.properties?.content?.anyOf?.[0]?.properties?.attachment ).toHaveProperty('format', 'path'); expect( result.inputParameters?.properties?.content?.anyOf?.[0]?.properties?.attachment ).toHaveProperty('file_uploadable', true); }); it('should transform file_uploadable properties inside array items with anyOf', async () => { const schema: Tool = { slug: 'test-tool', name: 'Test Tool', description: 'A test tool', tags: ['test'], version: '20251201_01', availableVersions: ['20251201_01'], inputParameters: { type: 'object', properties: { files: { type: 'array', items: { anyOf: [ { type: 'string', file_uploadable: true, }, { type: 'null', }, ], }, }, }, }, }; const result = await fileToolModifier.modifyToolSchema(schema); expect(result.inputParameters?.properties?.files?.items?.anyOf?.[0]).toHaveProperty( 'format', 'path' ); expect(result.inputParameters?.properties?.files?.items?.anyOf?.[0]).toHaveProperty( 'file_uploadable', true ); }); }); describe('fileUploadModifier', () => { const mockTool: Tool = { slug: 'test-tool', name: 'Test Tool', description: 'A test tool', tags: ['test'], inputParameters: { type: 'object', properties: { file: { type: 'string', file_uploadable: true, }, text: { type: 'string', }, }, }, version: '20251201_01', availableVersions: ['20251201_01'], }; it('should upload file for file_uploadable parameters', async () => { const mockFileData = { name: 'file.txt', mimetype: 'text/plain', s3key: 'uploads/file.txt', }; vi.mocked(fileUtils.getFileDataAfterUploadingToS3).mockResolvedValue(mockFileData); const params = { arguments: { file: '/path/to/file.txt', text: 'some text', }, userId: 'test-user', }; const result = await fileToolModifier.fileUploadModifier(mockTool, { toolSlug: 'test-tool', toolkitSlug: 'test-toolkit', params, }); expect(fileUtils.getFileDataAfterUploadingToS3).toHaveBeenCalledWith('/path/to/file.txt', { toolSlug: 'test-tool', toolkitSlug: 'test-toolkit', client: mockClient, }); expect(result.arguments?.file).toEqual(mockFileData); expect(result.arguments?.text).toBe('some text'); }); it('should call beforeFileUpload and use returned path for string files', async () => { const mockFileData = { name: 'file.txt', mimetype: 'text/plain', s3key: 'uploads/file.txt', }; vi.mocked(fileUtils.getFileDataAfterUploadingToS3).mockResolvedValue(mockFileData); const withHook = new FileToolModifier(mockClient, { beforeFileUpload: async ({ path, toolSlug, toolkitSlug }) => { expect(toolSlug).toBe('test-tool'); expect(toolkitSlug).toBe('test-toolkit'); return path.replace('original', 'rewritten'); }, }); const params = { arguments: { file: '/path/to/original.txt' }, userId: 'test-user', }; const result = await withHook.fileUploadModifier(mockTool, { toolSlug: 'test-tool', toolkitSlug: 'test-toolkit', params, }); expect(fileUtils.getFileDataAfterUploadingToS3).toHaveBeenCalledWith( '/path/to/rewritten.txt', { toolSlug: 'test-tool', toolkitSlug: 'test-toolkit', client: mockClient, } ); expect(result.arguments?.file).toEqual(mockFileData); }); it('should pass source="path" to beforeFileUpload for local filesystem paths', async () => { const mockFileData = { name: 'file.txt', mimetype: 'text/plain', s3key: 'uploads/file.txt', }; vi.mocked(fileUtils.getFileDataAfterUploadingToS3).mockResolvedValue(mockFileData); const hook = vi.fn(async ({ path }) => path); const withHook = new FileToolModifier(mockClient, { beforeFileUpload: hook }); await withHook.fileUploadModifier(mockTool, { toolSlug: 'test-tool', toolkitSlug: 'test-toolkit', params: { arguments: { file: '/path/to/a.txt' }, userId: 'u' }, }); expect(hook).toHaveBeenCalledWith( expect.objectContaining({ path: '/path/to/a.txt', source: 'path' }) ); }); it('should pass source="url" to beforeFileUpload for http(s) URLs', async () => { const mockFileData = { name: 'file.txt', mimetype: 'text/plain', s3key: 'uploads/file.txt', }; vi.mocked(fileUtils.getFileDataAfterUploadingToS3).mockResolvedValue(mockFileData); const hook = vi.fn(async ({ path }) => path); const withHook = new FileToolModifier(mockClient, { beforeFileUpload: hook }); await withHook.fileUploadModifier(mockTool, { toolSlug: 'test-tool', toolkitSlug: 'test-toolkit', params: { arguments: { file: 'https://example.com/report.pdf' }, userId: 'u', }, }); expect(hook).toHaveBeenCalledWith( expect.objectContaining({ path: 'https://example.com/report.pdf', source: 'url', }) ); }); it('should pass source="file" to beforeFileUpload for File objects', async () => { const mockFileData = { name: 'file.txt', mimetype: 'text/plain', s3key: 'uploads/file.txt', }; vi.mocked(fileUtils.getFileDataAfterUploadingToS3).mockResolvedValue(mockFileData); const hook = vi.fn(async () => undefined as unknown as string); const withHook = new FileToolModifier(mockClient, { beforeFileUpload: hook }); const fileObject = new File(['x'], 'uploaded.txt', { type: 'text/plain' }); await withHook.fileUploadModifier(mockTool, { toolSlug: 'test-tool', toolkitSlug: 'test-toolkit', params: { arguments: { file: fileObject }, userId: 'u' }, }); // `path` is the File's name, not a filesystem path. expect(hook).toHaveBeenCalledWith( expect.objectContaining({ path: 'uploaded.txt', source: 'file' }) ); }); it('should throw ComposioFileUploadAbortedError when beforeFileUpload returns false', async () => { const withHook = new FileToolModifier(mockClient, { beforeFileUpload: async () => false, }); await expect( withHook.fileUploadModifier(mockTool, { toolSlug: 'test-tool', toolkitSlug: 'test-toolkit', params: { arguments: { file: '/any/path' }, userId: 'u' }, }) ).rejects.toThrow(ComposioFileUploadAbortedError); expect(fileUtils.getFileDataAfterUploadingToS3).not.toHaveBeenCalled(); }); it('should throw ComposioFileUploadError on upload failure', async () => { vi.mocked(fileUtils.getFileDataAfterUploadingToS3).mockRejectedValue( new Error('Upload failed') ); const params = { arguments: { file: '/path/to/file.txt', }, userId: 'test-user', }; await expect( fileToolModifier.fileUploadModifier(mockTool, { toolSlug: 'test-tool', toolkitSlug: 'test-toolkit', params, }) ).rejects.toThrow(ComposioFileUploadError); }); it('should throw ComposioRequestCancelledError on caller-aborted upload', async () => { const controller = new AbortController(); vi.mocked(fileUtils.getFileDataAfterUploadingToS3).mockImplementationOnce(async () => { controller.abort(); const error = new Error('The operation was aborted'); error.name = 'AbortError'; throw error; }); const params = { arguments: { file: '/path/to/file.txt', }, userId: 'test-user', }; await expect( fileToolModifier.fileUploadModifier(mockTool, { toolSlug: 'test-tool', toolkitSlug: 'test-toolkit', params, signal: controller.signal, }) ).rejects.toBeInstanceOf(ComposioRequestCancelledError); }); it('should handle File object for file_uploadable parameters', async () => { const mockFileData = { name: 'file.txt', mimetype: 'text/plain', s3key: 'uploads/file.txt', }; vi.mocked(fileUtils.getFileDataAfterUploadingToS3).mockResolvedValue(mockFileData); const fileObject = new File(['test content'], 'file.txt', { type: 'text/plain' }); const params = { arguments: { file: fileObject, text: 'some text', }, userId: 'test-user', }; const result = await fileToolModifier.fileUploadModifier(mockTool, { toolSlug: 'test-tool', toolkitSlug: 'test-toolkit', params, }); expect(fileUtils.getFileDataAfterUploadingToS3).toHaveBeenCalledWith(fileObject, { toolSlug: 'test-tool', toolkitSlug: 'test-toolkit', client: mockClient, }); expect(result.arguments?.file).toEqual(mockFileData); expect(result.arguments?.text).toBe('some text'); }); it('should upload file for file_uploadable inside anyOf schema', async () => { const mockFileData = { name: 'file.txt', mimetype: 'text/plain', s3key: 'uploads/file.txt', }; vi.mocked(fileUtils.getFileDataAfterUploadingToS3).mockResolvedValue(mockFileData); const toolWithAnyOf: Tool = { slug: 'test-tool', name: 'Test Tool', description: 'A test tool', tags: ['test'], inputParameters: { type: 'object', properties: { fileInput: { anyOf: [ { type: 'string', file_uploadable: true, }, { type: 'null', }, ], }, }, }, version: '20251201_01', availableVersions: ['20251201_01'], }; const params = { arguments: { fileInput: '/path/to/file.txt', }, userId: 'test-user', }; const result = await fileToolModifier.fileUploadModifier(toolWithAnyOf, { toolSlug: 'test-tool', toolkitSlug: 'test-toolkit', params, }); expect(fileUtils.getFileDataAfterUploadingToS3).toHaveBeenCalledWith('/path/to/file.txt', { toolSlug: 'test-tool', toolkitSlug: 'test-toolkit', client: mockClient, }); expect(result.arguments?.fileInput).toEqual(mockFileData); }); it('should upload file for file_uploadable inside oneOf schema', async () => { const mockFileData = { name: 'file.txt', mimetype: 'text/plain', s3key: 'uploads/file.txt', }; vi.mocked(fileUtils.getFileDataAfterUploadingToS3).mockResolvedValue(mockFileData); const toolWithOneOf: Tool = { slug: 'test-tool', name: 'Test Tool', description: 'A test tool', tags: ['test'], inputParameters: { type: 'object', properties: { fileInput: { oneOf: [ { type: 'string', file_uploadable: true, }, { type: 'string', description: 'URL', }, ], }, }, }, version: '20251201_01', availableVersions: ['20251201_01'], }; const params = { arguments: { fileInput: '/path/to/file.txt', }, userId: 'test-user', }; const result = await fileToolModifier.fileUploadModifier(toolWithOneOf, { toolSlug: 'test-tool', toolkitSlug: 'test-toolkit', params, }); expect(fileUtils.getFileDataAfterUploadingToS3).toHaveBeenCalledWith('/path/to/file.txt', { toolSlug: 'test-tool', toolkitSlug: 'test-toolkit', client: mockClient, }); expect(result.arguments?.fileInput).toEqual(mockFileData); }); it('should upload file for file_uploadable inside allOf schema', async () => { const mockFileData = { name: 'file.txt', mimetype: 'text/plain', s3key: 'uploads/file.txt', }; vi.mocked(fileUtils.getFileDataAfterUploadingToS3).mockResolvedValue(mockFileData); const toolWithAllOf: Tool = { slug: 'test-tool', name: 'Test Tool', description: 'A test tool', tags: ['test'], inputParameters: { type: 'object', properties: { fileInput: { allOf: [ { type: 'string', file_uploadable: true, }, { minLength: 1, }, ], }, }, }, version: '20251201_01', availableVersions: ['20251201_01'], }; const params = { arguments: { fileInput: '/path/to/file.txt', }, userId: 'test-user', }; const result = await fileToolModifier.fileUploadModifier(toolWithAllOf, { toolSlug: 'test-tool', toolkitSlug: 'test-toolkit', params, }); expect(fileUtils.getFileDataAfterUploadingToS3).toHaveBeenCalledWith('/path/to/file.txt', { toolSlug: 'test-tool', toolkitSlug: 'test-toolkit', client: mockClient, }); expect(result.arguments?.fileInput).toEqual(mockFileData); }); it('should upload file for nested file_uploadable inside anyOf with objects', async () => { const mockFileData = { name: 'attachment.pdf', mimetype: 'application/pdf', s3key: 'uploads/attachment.pdf', }; vi.mocked(fileUtils.getFileDataAfterUploadingToS3).mockResolvedValue(mockFileData); const toolWithNestedAnyOf: Tool = { slug: 'test-tool', name: 'Test Tool', description: 'A test tool', tags: ['test'], inputParameters: { type: 'object', properties: { content: { anyOf: [ { type: 'object', properties: { attachment: { type: 'string', file_uploadable: true, }, }, }, { type: 'string', }, ], }, }, }, version: '20251201_01', availableVersions: ['20251201_01'], }; const params = { arguments: { content: { attachment: '/path/to/attachment.pdf', }, }, userId: 'test-user', }; const result = await fileToolModifier.fileUploadModifier(toolWithNestedAnyOf, { toolSlug: 'test-tool', toolkitSlug: 'test-toolkit', params, }); expect(fileUtils.getFileDataAfterUploadingToS3).toHaveBeenCalledWith( '/path/to/attachment.pdf', { toolSlug: 'test-tool', toolkitSlug: 'test-toolkit', client: mockClient, } ); expect((result.arguments?.content as Record)?.attachment).toEqual( mockFileData ); }); it('uploads once when multiple oneOf variants are file_uploadable', async () => { // Regression: previously hydrateFiles iterated over every uploadable // variant in anyOf/oneOf/allOf and re-uploaded the same file once per // variant. With `oneOf` semantics this is doubly wrong (only one // variant matches at runtime). Match the Python SDK and short-circuit // on the first uploadable variant. const mockFileData = { name: 'file.txt', mimetype: 'text/plain', s3key: 'uploads/file.txt', }; vi.mocked(fileUtils.getFileDataAfterUploadingToS3).mockResolvedValue(mockFileData); const toolWithTwoUploadableVariants: Tool = { slug: 'test-tool', name: 'Test Tool', description: 'A test tool', tags: ['test'], inputParameters: { type: 'object', properties: { fileInput: { oneOf: [ { type: 'string', file_uploadable: true }, { type: 'string', file_uploadable: true }, ], }, }, }, version: '20251201_01', availableVersions: ['20251201_01'], }; const params = { arguments: { fileInput: '/path/to/file.txt' }, userId: 'test-user', }; const result = await fileToolModifier.fileUploadModifier(toolWithTwoUploadableVariants, { toolSlug: 'test-tool', toolkitSlug: 'test-toolkit', params, }); expect(fileUtils.getFileDataAfterUploadingToS3).toHaveBeenCalledTimes(1); expect(result.arguments?.fileInput).toEqual(mockFileData); }); it('should upload each file when anyOf accepts a single file or an array of files', async () => { const mockFileData1 = { name: 'file1.txt', mimetype: 'text/plain', s3key: 'uploads/file1.txt', }; const mockFileData2 = { name: 'file2.txt', mimetype: 'text/plain', s3key: 'uploads/file2.txt', }; vi.mocked(fileUtils.getFileDataAfterUploadingToS3) .mockResolvedValueOnce(mockFileData1) .mockResolvedValueOnce(mockFileData2); const toolWithSingleOrArrayFile: Tool = { slug: 'test-tool', name: 'Test Tool', description: 'A test tool', tags: ['test'], inputParameters: { type: 'object', properties: { attachment: { anyOf: [ { type: 'string', file_uploadable: true }, { type: 'array', items: { type: 'string', file_uploadable: true }, }, ], }, }, }, version: '20251201_01', availableVersions: ['20251201_01'], }; const result = await fileToolModifier.fileUploadModifier(toolWithSingleOrArrayFile, { toolSlug: 'test-tool', toolkitSlug: 'test-toolkit', params: { arguments: { attachment: ['/path/to/file1.txt', '/path/to/file2.txt'], }, userId: 'test-user', }, }); expect(fileUtils.getFileDataAfterUploadingToS3).toHaveBeenCalledTimes(2); expect(fileUtils.getFileDataAfterUploadingToS3).toHaveBeenNthCalledWith( 1, '/path/to/file1.txt', { toolSlug: 'test-tool', toolkitSlug: 'test-toolkit', client: mockClient, } ); expect(fileUtils.getFileDataAfterUploadingToS3).toHaveBeenNthCalledWith( 2, '/path/to/file2.txt', { toolSlug: 'test-tool', toolkitSlug: 'test-toolkit', client: mockClient, } ); expect(result.arguments?.attachment).toEqual([mockFileData1, mockFileData2]); }); it('should keep single-file behavior when anyOf also accepts an array of files', async () => { const mockFileData = { name: 'file.txt', mimetype: 'text/plain', s3key: 'uploads/file.txt', }; vi.mocked(fileUtils.getFileDataAfterUploadingToS3).mockResolvedValue(mockFileData); const toolWithSingleOrArrayFile: Tool = { slug: 'test-tool', name: 'Test Tool', description: 'A test tool', tags: ['test'], inputParameters: { type: 'object', properties: { attachment: { anyOf: [ { type: 'string', file_uploadable: true }, { type: 'array', items: { type: 'string', file_uploadable: true }, }, ], }, }, }, version: '20251201_01', availableVersions: ['20251201_01'], }; const result = await fileToolModifier.fileUploadModifier(toolWithSingleOrArrayFile, { toolSlug: 'test-tool', toolkitSlug: 'test-toolkit', params: { arguments: { attachment: '/path/to/file.txt', }, userId: 'test-user', }, }); expect(fileUtils.getFileDataAfterUploadingToS3).toHaveBeenCalledTimes(1); expect(fileUtils.getFileDataAfterUploadingToS3).toHaveBeenCalledWith('/path/to/file.txt', { toolSlug: 'test-tool', toolkitSlug: 'test-toolkit', client: mockClient, }); expect(result.arguments?.attachment).toEqual(mockFileData); }); it('should upload file array items whose item schema is an anyOf file branch', async () => { const mockFileData1 = { name: 'file1.txt', mimetype: 'text/plain', s3key: 'uploads/file1.txt', }; const mockFileData2 = { name: 'file2.txt', mimetype: 'text/plain', s3key: 'uploads/file2.txt', }; vi.mocked(fileUtils.getFileDataAfterUploadingToS3) .mockResolvedValueOnce(mockFileData1) .mockResolvedValueOnce(mockFileData2); const toolWithArrayItemAnyOf: Tool = { slug: 'test-tool', name: 'Test Tool', description: 'A test tool', tags: ['test'], inputParameters: { type: 'object', properties: { attachments: { type: 'array', items: { anyOf: [{ type: 'string', file_uploadable: true }, { type: 'null' }], }, }, }, }, version: '20251201_01', availableVersions: ['20251201_01'], }; const result = await fileToolModifier.fileUploadModifier(toolWithArrayItemAnyOf, { toolSlug: 'test-tool', toolkitSlug: 'test-toolkit', params: { arguments: { attachments: ['/path/to/file1.txt', null, '/path/to/file2.txt'], }, userId: 'test-user', }, }); expect(fileUtils.getFileDataAfterUploadingToS3).toHaveBeenCalledTimes(2); expect(result.arguments?.attachments).toEqual([mockFileData1, null, mockFileData2]); }); it('should not upload when value is null for anyOf with null variant', async () => { const toolWithAnyOf: Tool = { slug: 'test-tool', name: 'Test Tool', description: 'A test tool', tags: ['test'], inputParameters: { type: 'object', properties: { fileInput: { anyOf: [ { type: 'string', file_uploadable: true, }, { type: 'null', }, ], }, }, }, version: '20251201_01', availableVersions: ['20251201_01'], }; const params = { arguments: { fileInput: null, }, userId: 'test-user', }; const result = await fileToolModifier.fileUploadModifier(toolWithAnyOf, { toolSlug: 'test-tool', toolkitSlug: 'test-toolkit', params, }); expect(fileUtils.getFileDataAfterUploadingToS3).not.toHaveBeenCalled(); expect(result.arguments?.fileInput).toBeNull(); }); it('should upload file from base properties when anyOf has no file_uploadable', async () => { const mockFileData = { name: 'file.txt', mimetype: 'text/plain', s3key: 'uploads/file.txt', }; vi.mocked(fileUtils.getFileDataAfterUploadingToS3).mockResolvedValue(mockFileData); // Schema with anyOf at property level (no file_uploadable) but file_uploadable in sibling property const toolWithMixedSchema: Tool = { slug: 'test-tool', name: 'Test Tool', description: 'A test tool', tags: ['test'], inputParameters: { type: 'object', properties: { metadata: { anyOf: [{ type: 'string' }, { type: 'null' }], }, file: { type: 'string', file_uploadable: true, }, }, }, version: '20251201_01', availableVersions: ['20251201_01'], }; const params = { arguments: { metadata: 'some metadata', file: '/path/to/file.txt', }, userId: 'test-user', }; const result = await fileToolModifier.fileUploadModifier(toolWithMixedSchema, { toolSlug: 'test-tool', toolkitSlug: 'test-toolkit', params, }); expect(fileUtils.getFileDataAfterUploadingToS3).toHaveBeenCalledWith('/path/to/file.txt', { toolSlug: 'test-tool', toolkitSlug: 'test-toolkit', client: mockClient, }); expect(result.arguments?.file).toEqual(mockFileData); expect(result.arguments?.metadata).toBe('some metadata'); }); it('should upload file when schema has anyOf without file_uploadable alongside properties with file_uploadable', async () => { const mockFileData = { name: 'document.pdf', mimetype: 'application/pdf', s3key: 'uploads/document.pdf', }; vi.mocked(fileUtils.getFileDataAfterUploadingToS3).mockResolvedValue(mockFileData); // Schema where the root has both anyOf (without file_uploadable) and properties (with file_uploadable) const toolWithAnyOfAndProperties: Tool = { slug: 'test-tool', name: 'Test Tool', description: 'A test tool', tags: ['test'], inputParameters: { type: 'object', anyOf: [{ required: ['text'] }, { required: ['file'] }], properties: { text: { type: 'string', }, file: { type: 'string', file_uploadable: true, }, }, }, version: '20251201_01', availableVersions: ['20251201_01'], }; const params = { arguments: { file: '/path/to/document.pdf', }, userId: 'test-user', }; const result = await fileToolModifier.fileUploadModifier(toolWithAnyOfAndProperties, { toolSlug: 'test-tool', toolkitSlug: 'test-toolkit', params, }); expect(fileUtils.getFileDataAfterUploadingToS3).toHaveBeenCalledWith( '/path/to/document.pdf', { toolSlug: 'test-tool', toolkitSlug: 'test-toolkit', client: mockClient, } ); expect(result.arguments?.file).toEqual(mockFileData); }); }); describe('fileDownloadModifier', () => { const mockTool: Tool = { slug: 'test-tool', name: 'Test Tool', description: 'A test tool', tags: ['test'], availableVersions: ['20251201_01'], version: '20251201_01', }; it('should download file from S3 URL', async () => { const mockDownloadResult = { name: 'file.txt', mimeType: 'text/plain', s3Url: 'downloads/file.txt', filePath: '/downloaded/file.txt', }; vi.mocked(fileUtils.downloadFileFromS3).mockResolvedValue(mockDownloadResult); const result = { data: { file: { s3url: 'https://s3.example.com/file.txt', mimetype: 'text/plain', }, }, error: null, successful: true, }; const modifiedResult = await fileToolModifier.fileDownloadModifier(mockTool, { toolSlug: 'test-tool', toolkitSlug: 'test-toolkit', result, }); expect(fileUtils.downloadFileFromS3).toHaveBeenCalledWith({ toolSlug: 'test-tool', s3Url: 'https://s3.example.com/file.txt', mimeType: 'text/plain', }); expect(modifiedResult.data.file).toEqual({ uri: '/downloaded/file.txt', file_downloaded: true, s3url: 'https://s3.example.com/file.txt', mimeType: 'text/plain', }); }); it('should handle download failure gracefully', async () => { vi.mocked(fileUtils.downloadFileFromS3).mockRejectedValue(new Error('Download failed')); const result = { data: { file: { s3url: 'https://s3.example.com/file.txt', mimetype: 'text/plain', }, }, error: null, successful: true, }; const modifiedResult = await fileToolModifier.fileDownloadModifier(mockTool, { toolSlug: 'test-tool', toolkitSlug: 'test-toolkit', result, }); expect(modifiedResult.data.file).toEqual({ uri: '', file_downloaded: false, s3url: 'https://s3.example.com/file.txt', mimeType: 'text/plain', }); }); it('should skip fields without s3url', async () => { const result = { data: { text: 'some text', file: { other_field: 'value', }, }, error: null, successful: true, }; const modifiedResult = await fileToolModifier.fileDownloadModifier(mockTool, { toolSlug: 'test-tool', toolkitSlug: 'test-toolkit', result, }); expect(fileUtils.downloadFileFromS3).not.toHaveBeenCalled(); expect(modifiedResult).toEqual(result); }); it('should download file with file_downloadable inside anyOf schema', async () => { const mockDownloadResult = { name: 'file.txt', mimeType: 'text/plain', s3Url: 'downloads/file.txt', filePath: '/downloaded/file.txt', }; vi.mocked(fileUtils.downloadFileFromS3).mockResolvedValue(mockDownloadResult); const toolWithAnyOf: Tool = { slug: 'test-tool', name: 'Test Tool', description: 'A test tool', tags: ['test'], outputParameters: { type: 'object', properties: { fileOutput: { anyOf: [ { type: 'object', file_downloadable: true, properties: { s3url: { type: 'string' }, mimetype: { type: 'string' }, }, }, { type: 'null', }, ], }, }, }, version: '20251201_01', availableVersions: ['20251201_01'], }; const result = { data: { fileOutput: { s3url: 'https://s3.example.com/file.txt', mimetype: 'text/plain', }, }, error: null, successful: true, }; const modifiedResult = await fileToolModifier.fileDownloadModifier(toolWithAnyOf, { toolSlug: 'test-tool', toolkitSlug: 'test-toolkit', result, }); expect(fileUtils.downloadFileFromS3).toHaveBeenCalledWith({ toolSlug: 'test-tool', s3Url: 'https://s3.example.com/file.txt', mimeType: 'text/plain', }); expect(modifiedResult.data.fileOutput).toEqual({ uri: '/downloaded/file.txt', file_downloaded: true, s3url: 'https://s3.example.com/file.txt', mimeType: 'text/plain', }); }); it('should download file with file_downloadable inside oneOf schema', async () => { const mockDownloadResult = { name: 'document.pdf', mimeType: 'application/pdf', s3Url: 'downloads/document.pdf', filePath: '/downloaded/document.pdf', }; vi.mocked(fileUtils.downloadFileFromS3).mockResolvedValue(mockDownloadResult); const toolWithOneOf: Tool = { slug: 'test-tool', name: 'Test Tool', description: 'A test tool', tags: ['test'], outputParameters: { type: 'object', properties: { result: { oneOf: [ { type: 'object', file_downloadable: true, properties: { s3url: { type: 'string' }, mimetype: { type: 'string' }, }, }, { type: 'string', }, ], }, }, }, version: '20251201_01', availableVersions: ['20251201_01'], }; const result = { data: { result: { s3url: 'https://s3.example.com/document.pdf', mimetype: 'application/pdf', }, }, error: null, successful: true, }; const modifiedResult = await fileToolModifier.fileDownloadModifier(toolWithOneOf, { toolSlug: 'test-tool', toolkitSlug: 'test-toolkit', result, }); expect(fileUtils.downloadFileFromS3).toHaveBeenCalledWith({ toolSlug: 'test-tool', s3Url: 'https://s3.example.com/document.pdf', mimeType: 'application/pdf', }); expect(modifiedResult.data.result).toEqual({ uri: '/downloaded/document.pdf', file_downloaded: true, s3url: 'https://s3.example.com/document.pdf', mimeType: 'application/pdf', }); }); it('should download file with file_downloadable inside allOf schema', async () => { const mockDownloadResult = { name: 'image.png', mimeType: 'image/png', s3Url: 'downloads/image.png', filePath: '/downloaded/image.png', }; vi.mocked(fileUtils.downloadFileFromS3).mockResolvedValue(mockDownloadResult); const toolWithAllOf: Tool = { slug: 'test-tool', name: 'Test Tool', description: 'A test tool', tags: ['test'], outputParameters: { type: 'object', properties: { image: { allOf: [ { type: 'object', file_downloadable: true, properties: { s3url: { type: 'string' }, mimetype: { type: 'string' }, }, }, { required: ['s3url'], }, ], }, }, }, version: '20251201_01', availableVersions: ['20251201_01'], }; const result = { data: { image: { s3url: 'https://s3.example.com/image.png', mimetype: 'image/png', }, }, error: null, successful: true, }; const modifiedResult = await fileToolModifier.fileDownloadModifier(toolWithAllOf, { toolSlug: 'test-tool', toolkitSlug: 'test-toolkit', result, }); expect(fileUtils.downloadFileFromS3).toHaveBeenCalledWith({ toolSlug: 'test-tool', s3Url: 'https://s3.example.com/image.png', mimeType: 'image/png', }); expect(modifiedResult.data.image).toEqual({ uri: '/downloaded/image.png', file_downloaded: true, s3url: 'https://s3.example.com/image.png', mimeType: 'image/png', }); }); it('should download nested file with file_downloadable inside anyOf with objects', async () => { const mockDownloadResult = { name: 'attachment.docx', mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', s3Url: 'downloads/attachment.docx', filePath: '/downloaded/attachment.docx', }; vi.mocked(fileUtils.downloadFileFromS3).mockResolvedValue(mockDownloadResult); const toolWithNestedAnyOf: Tool = { slug: 'test-tool', name: 'Test Tool', description: 'A test tool', tags: ['test'], outputParameters: { type: 'object', properties: { response: { anyOf: [ { type: 'object', properties: { attachment: { type: 'object', file_downloadable: true, properties: { s3url: { type: 'string' }, mimetype: { type: 'string' }, }, }, }, }, { type: 'string', }, ], }, }, }, version: '20251201_01', availableVersions: ['20251201_01'], }; const result = { data: { response: { attachment: { s3url: 'https://s3.example.com/attachment.docx', mimetype: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', }, }, }, error: null, successful: true, }; const modifiedResult = await fileToolModifier.fileDownloadModifier(toolWithNestedAnyOf, { toolSlug: 'test-tool', toolkitSlug: 'test-toolkit', result, }); expect(fileUtils.downloadFileFromS3).toHaveBeenCalledWith({ toolSlug: 'test-tool', s3Url: 'https://s3.example.com/attachment.docx', mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', }); expect((modifiedResult.data.response as Record)?.attachment).toEqual({ uri: '/downloaded/attachment.docx', file_downloaded: true, s3url: 'https://s3.example.com/attachment.docx', mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', }); }); it('should download files in array with anyOf schema', async () => { const mockDownloadResult = { name: 'file.txt', mimeType: 'text/plain', s3Url: 'downloads/file.txt', filePath: '/downloaded/file.txt', }; vi.mocked(fileUtils.downloadFileFromS3).mockResolvedValue(mockDownloadResult); const toolWithArrayAnyOf: Tool = { slug: 'test-tool', name: 'Test Tool', description: 'A test tool', tags: ['test'], outputParameters: { type: 'object', properties: { files: { type: 'array', items: { anyOf: [ { type: 'object', file_downloadable: true, properties: { s3url: { type: 'string' }, mimetype: { type: 'string' }, }, }, { type: 'null', }, ], }, }, }, }, version: '20251201_01', availableVersions: ['20251201_01'], }; const result = { data: { files: [ { s3url: 'https://s3.example.com/file1.txt', mimetype: 'text/plain', }, { s3url: 'https://s3.example.com/file2.txt', mimetype: 'text/plain', }, ], }, error: null, successful: true, }; const modifiedResult = await fileToolModifier.fileDownloadModifier(toolWithArrayAnyOf, { toolSlug: 'test-tool', toolkitSlug: 'test-toolkit', result, }); expect(fileUtils.downloadFileFromS3).toHaveBeenCalledTimes(2); expect((modifiedResult.data.files as unknown[])?.[0]).toEqual({ uri: '/downloaded/file.txt', file_downloaded: true, s3url: 'https://s3.example.com/file1.txt', mimeType: 'text/plain', }); }); it('should download each file when anyOf accepts a single file or an array of files', async () => { const mockDownloadResult1 = { name: 'file1.txt', mimeType: 'text/plain', s3Url: 'downloads/file1.txt', filePath: '/downloaded/file1.txt', }; const mockDownloadResult2 = { name: 'file2.txt', mimeType: 'text/plain', s3Url: 'downloads/file2.txt', filePath: '/downloaded/file2.txt', }; vi.mocked(fileUtils.downloadFileFromS3) .mockResolvedValueOnce(mockDownloadResult1) .mockResolvedValueOnce(mockDownloadResult2); const toolWithSingleOrArrayFile: Tool = { slug: 'test-tool', name: 'Test Tool', description: 'A test tool', tags: ['test'], outputParameters: { type: 'object', properties: { attachment: { anyOf: [ { type: 'object', file_downloadable: true, properties: { s3url: { type: 'string' }, mimetype: { type: 'string' }, }, }, { type: 'array', items: { type: 'object', file_downloadable: true, properties: { s3url: { type: 'string' }, mimetype: { type: 'string' }, }, }, }, ], }, }, }, version: '20251201_01', availableVersions: ['20251201_01'], }; const modifiedResult = await fileToolModifier.fileDownloadModifier( toolWithSingleOrArrayFile, { toolSlug: 'test-tool', toolkitSlug: 'test-toolkit', result: { data: { attachment: [ { s3url: 'https://s3.example.com/file1.txt', mimetype: 'text/plain', }, { s3url: 'https://s3.example.com/file2.txt', mimetype: 'text/plain', }, ], }, error: null, successful: true, }, } ); expect(fileUtils.downloadFileFromS3).toHaveBeenCalledTimes(2); expect(modifiedResult.data.attachment).toEqual([ { uri: '/downloaded/file1.txt', file_downloaded: true, s3url: 'https://s3.example.com/file1.txt', mimeType: 'text/plain', }, { uri: '/downloaded/file2.txt', file_downloaded: true, s3url: 'https://s3.example.com/file2.txt', mimeType: 'text/plain', }, ]); }); it('should not download when value is null for anyOf with null variant', async () => { const toolWithAnyOf: Tool = { slug: 'test-tool', name: 'Test Tool', description: 'A test tool', tags: ['test'], outputParameters: { type: 'object', properties: { fileOutput: { anyOf: [ { type: 'object', file_downloadable: true, properties: { s3url: { type: 'string' }, mimetype: { type: 'string' }, }, }, { type: 'null', }, ], }, }, }, version: '20251201_01', availableVersions: ['20251201_01'], }; const result = { data: { fileOutput: null, }, error: null, successful: true, }; const modifiedResult = await fileToolModifier.fileDownloadModifier(toolWithAnyOf, { toolSlug: 'test-tool', toolkitSlug: 'test-toolkit', result, }); expect(fileUtils.downloadFileFromS3).not.toHaveBeenCalled(); expect(modifiedResult.data.fileOutput).toBeNull(); }); }); // The schema walkers recurse properties/anyOf/oneOf/allOf/items but never // dereference `$ref`. Composio toolkits express file flags through a // `$ref`/`$defs` indirection (e.g. GMAIL_GET_ATTACHMENT), so without // dereferencing the flag is invisible and the file is silently skipped. // See https://github.com/ComposioHQ/composio/issues/3506. describe('$ref / $defs indirection', () => { it('marks a file_uploadable behind a $ref/$defs as format: "path" in modifyToolSchema', async () => { const schema: Tool = { slug: 'test-tool', name: 'Test Tool', description: 'A test tool', tags: ['test'], version: '20251201_01', availableVersions: ['20251201_01'], inputParameters: { type: 'object', properties: { attachment: { $ref: '#/$defs/Attachment' }, legacyAttachment: { $ref: '#/definitions/LegacyAttachment' }, text: { type: 'string' }, }, $defs: { Attachment: { type: 'string', file_uploadable: true, description: 'Local path to attach', }, }, definitions: { LegacyAttachment: { type: 'string', file_uploadable: true, description: 'Legacy local path to attach', }, }, }, }; const result = await fileToolModifier.modifyToolSchema(schema); expect(result.inputParameters?.properties?.attachment).toHaveProperty('format', 'path'); expect(result.inputParameters?.properties?.attachment).toHaveProperty( 'file_uploadable', true ); expect(result.inputParameters?.properties?.legacyAttachment).toHaveProperty('format', 'path'); expect(result.inputParameters?.properties?.legacyAttachment).toHaveProperty( 'file_uploadable', true ); expect(result.inputParameters?.properties?.text).not.toHaveProperty('format'); }); it('uploads a file whose file_uploadable flag is reachable only via $ref/$defs', async () => { const mockFileData = { name: 'doc.pdf', mimetype: 'application/pdf', s3key: 'uploads/doc.pdf', }; vi.mocked(fileUtils.getFileDataAfterUploadingToS3).mockResolvedValue(mockFileData); const toolWithRef: Tool = { slug: 'test-tool', name: 'Test Tool', description: 'A test tool', tags: ['test'], version: '20251201_01', availableVersions: ['20251201_01'], inputParameters: { type: 'object', properties: { attachment: { $ref: '#/$defs/Attachment' }, }, $defs: { Attachment: { type: 'string', file_uploadable: true }, }, }, }; const result = await fileToolModifier.fileUploadModifier(toolWithRef, { toolSlug: 'test-tool', toolkitSlug: 'test-toolkit', params: { arguments: { attachment: '/path/to/doc.pdf' }, userId: 'test-user', }, }); expect(fileUtils.getFileDataAfterUploadingToS3).toHaveBeenCalledWith('/path/to/doc.pdf', { toolSlug: 'test-tool', toolkitSlug: 'test-toolkit', client: mockClient, }); expect(result.arguments?.attachment).toEqual(mockFileData); }); it('degrades gracefully when a $ref has no matching $defs target', async () => { // Some toolkits ship a `$ref` without ever declaring `$defs` // (https://github.com/ComposioHQ/composio/issues/3307). The modifier // must not throw; the unresolved branch simply carries no file flag. const schema: Tool = { slug: 'test-tool', name: 'Test Tool', description: 'A test tool', tags: ['test'], version: '20251201_01', availableVersions: ['20251201_01'], inputParameters: { type: 'object', properties: { attachment: { $ref: '#/$defs/Missing' }, }, }, }; const result = await fileToolModifier.modifyToolSchema(schema); expect(result.inputParameters?.properties?.attachment).not.toHaveProperty('format'); }); }); }); describe('Tools with dangerouslyAllowAutoUploadDownloadFiles', () => { const context = createTestContext(); setupTest(context); const mockToolWithFileUpload: Tool = { slug: 'COMPOSIO_TOOL', name: 'Composio Tool', description: 'A test tool', tags: ['test'], toolkit: { slug: 'test-toolkit', name: 'Test Toolkit', }, inputParameters: { type: 'object' as const, properties: { file: { type: 'string', file_uploadable: true, }, }, additionalProperties: false, }, outputParameters: { type: 'object' as const, properties: { file: { type: 'object', properties: { s3url: { type: 'string', }, mimetype: { type: 'string', }, }, }, }, additionalProperties: false, }, version: '20251201_01', availableVersions: ['20251201_01'], }; const mockRawToolWithFileUpload = { slug: 'COMPOSIO_TOOL', name: 'Composio Tool', description: 'A test tool', tags: ['test'], toolkit: { slug: 'test-toolkit', name: 'Test Toolkit', }, input_parameters: { type: 'object', properties: { file: { type: 'string', file_uploadable: true, }, }, additionalProperties: false, }, output_parameters: { type: 'object', properties: { file: { type: 'object', properties: { s3url: { type: 'string', }, mimetype: { type: 'string', }, }, }, }, additionalProperties: false, }, version: '20251201_01', availableVersions: ['20251201_01'], }; describe('when dangerouslyAllowAutoUploadDownloadFiles is false', () => { beforeEach(async () => { context.tools = new Tools(mockClient as unknown as ComposioClient, { provider: context.mockProvider, dangerouslyAllowAutoUploadDownloadFiles: false, }); // Mock the tool execution const { getRawComposioToolBySlugSpy } = await mockToolExecution(context.tools); getRawComposioToolBySlugSpy.mockReset(); getRawComposioToolBySlugSpy.mockResolvedValue(mockToolWithFileUpload); // Mock the client's tools.list method mockClient.tools.list.mockResolvedValue({ items: [mockRawToolWithFileUpload], totalPages: 1, }); // Mock the file upload and download utilities vi.mocked(fileUtils.getFileDataAfterUploadingToS3).mockResolvedValue({ name: 'file.txt', mimetype: 'text/plain', s3key: 'uploads/file.txt', }); vi.mocked(fileUtils.downloadFileFromS3).mockResolvedValue({ name: 'file.txt', mimeType: 'text/plain', s3Url: 'downloads/file.txt', filePath: '/path/to/downloaded/file.txt', }); // Mock the provider's wrapTools method context.mockProvider.wrapTools.mockImplementation(tools => tools); }); it('should not modify tool schema for file upload', async () => { // The collapsed `{ type: 'string', format: 'path' }` shape is a promise // that the SDK will stage local paths for the caller. That promise is // only true when auto-upload is on, so we leave the raw shape // untouched when the flag is off — callers are expected to pre-stage // via `composio.files.upload()`. Runtime behavior is documented by // the warning emitted from `applyBeforeExecuteModifiers`. const result = await context.tools.getRawComposioTools({ tools: ['COMPOSIO_TOOL'] }); expect(result[0].inputParameters?.properties?.file).not.toHaveProperty('format'); expect(fileUtils.getFileDataAfterUploadingToS3).not.toHaveBeenCalled(); }); it('should not upload files during execution', async () => { // Mock getRawComposioToolBySlug for this test vi.spyOn(context.tools, 'getRawComposioToolBySlug').mockResolvedValueOnce( mockToolWithFileUpload ); await context.tools.execute('COMPOSIO_TOOL', { arguments: { file: '/path/to/file.txt', }, userId: 'test-user', dangerouslySkipVersionCheck: true, }); expect(fileUtils.getFileDataAfterUploadingToS3).not.toHaveBeenCalled(); expect(mockClient.tools.execute).toHaveBeenCalledWith( 'COMPOSIO_TOOL', { arguments: { file: '/path/to/file.txt', }, allow_tracing: undefined, connected_account_id: undefined, custom_auth_params: undefined, custom_connection_data: undefined, text: undefined, user_id: 'test-user', version: 'latest', }, undefined ); }); it('warns once per tool when auto-upload is off and the tool has a file-uploadable input', async () => { const warnSpy = vi.spyOn((await import('../../src/utils/logger')).default, 'warn'); warnSpy.mockImplementation(() => {}); // silence the log in test output vi.spyOn(context.tools, 'getRawComposioToolBySlug').mockResolvedValue(mockToolWithFileUpload); // First call emits the warning. await context.tools.execute('COMPOSIO_TOOL', { arguments: { file: '/path/to/file.txt' }, userId: 'u', dangerouslySkipVersionCheck: true, }); expect(warnSpy).toHaveBeenCalledTimes(1); const msg = String(warnSpy.mock.calls[0]?.[0] ?? ''); expect(msg).toContain('COMPOSIO_TOOL'); expect(msg).toContain('dangerouslyAllowAutoUploadDownloadFiles'); expect(msg).toContain('composio.files.upload'); // Repeated executions of the same tool do NOT re-warn. await context.tools.execute('COMPOSIO_TOOL', { arguments: { file: '/path/to/file.txt' }, userId: 'u', dangerouslySkipVersionCheck: true, }); expect(warnSpy).toHaveBeenCalledTimes(1); warnSpy.mockRestore(); }); it('should not download files from execution results', async () => { // Mock getRawComposioToolBySlug for this test vi.spyOn(context.tools, 'getRawComposioToolBySlug').mockResolvedValueOnce( mockToolWithFileUpload ); // Mock the response data structure correctly const mockResponse = { data: { file: { s3url: 'https://s3.example.com/file.txt', mimetype: 'text/plain', }, }, error: null, successful: true, log_id: '123', session_info: {}, }; // Mock the client's execute method to return the raw response mockClient.tools.execute.mockResolvedValueOnce(mockResponse); // Mock transformToolExecuteResponse to return the same data structure vi.spyOn(context.tools as any, 'transformToolExecuteResponse').mockReturnValue({ data: mockResponse.data, error: mockResponse.error, successful: mockResponse.successful, logId: mockResponse.log_id, sessionInfo: mockResponse.session_info, }); const result = await context.tools.execute('COMPOSIO_TOOL', { arguments: { file: '/path/to/file.txt', }, userId: 'test-user', dangerouslySkipVersionCheck: true, }); expect(fileUtils.downloadFileFromS3).not.toHaveBeenCalled(); expect(result.data.file).toEqual({ s3url: 'https://s3.example.com/file.txt', mimetype: 'text/plain', }); }); }); });