Files
2026-07-13 12:38:34 +08:00

2115 lines
62 KiB
TypeScript

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<string, unknown>)?.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<string, unknown>)?.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',
});
});
});
});