import { describe, it, expect, vi, beforeEach } from 'vitest'; import { z } from 'zod/v3'; import { ToolRouter } from '../../src/models/ToolRouter'; import ComposioClient from '@composio/client'; import { telemetry } from '../../src/telemetry/Telemetry'; import { MockProvider } from '../utils/mocks/provider.mock'; import { Tools } from '../../src/models/Tools'; import { ConnectedAccountStatuses } from '../../src/types/connectedAccounts.types'; import { ToolRouterCreateSessionConfig, Session, SessionPreset, } from '../../src/types/toolRouter.types'; import { createCustomTool } from '../../src/models/CustomTool'; import { DIRECT_CUSTOM_TOOL_DESCRIPTION_PREFIX } from '../../src/models/ToolRouterSession'; // Mock dependencies vi.mock('../../src/telemetry/Telemetry', () => ({ telemetry: { instrument: vi.fn(), }, })); vi.mock('../../src/models/Tools', () => { return { Tools: vi.fn().mockImplementation(function () { return { getRawComposioTools: vi.fn().mockResolvedValue([{ slug: 'GMAIL_FETCH_EMAILS' }]), getRawToolRouterSessionTools: vi .fn() .mockResolvedValue([{ slug: 'COMPOSIO_SEARCH_TOOLS' }]), wrapToolsForToolRouter: vi.fn().mockReturnValue('mocked-wrapped-tools'), }; }), }; }); // Create mock client with ToolRouter-related methods const createMockClient = () => ({ baseURL: 'https://api.composio.dev', apiKey: 'test-api-key', post: vi.fn(), delete: vi.fn(), toolRouter: { session: { create: vi.fn(), retrieve: vi.fn(), attach: vi.fn(), link: vi.fn(), toolkits: vi.fn(), search: vi.fn(), execute: vi.fn(), }, }, tools: { list: vi.fn(), retrieve: vi.fn(), execute: vi.fn(), }, }); // Mock response data const mockSessionCreateResponse = { session_id: 'session_123', mcp: { type: 'http', url: 'https://mcp.example.com/session_123', }, tool_router_tools: ['GMAIL_FETCH_EMAILS', 'SLACK_SEND_MESSAGE', 'GITHUB_CREATE_ISSUE'], config: { preload: { tools: [] }, }, config_version: 1, }; const mockLinkResponse = { connected_account_id: 'conn_456', redirect_url: 'https://composio.dev/auth/redirect', }; const mockSessionRetrieveResponse = { session_id: 'session_123', mcp: { type: 'http', url: 'https://mcp.example.com/session_123', }, tool_router_tools: ['GMAIL_FETCH_EMAILS', 'SLACK_SEND_MESSAGE', 'GITHUB_CREATE_ISSUE'], config: { user_id: 'user_123', toolkits: { enable: ['gmail', 'slack', 'github'] }, auth_configs: {}, connected_accounts: {}, manage_connections: { enable: true, }, preload: { tools: ['GMAIL_FETCH_EMAILS'] }, }, config_version: 7, }; const mockToolkitsResponse = { items: [ { slug: 'gmail', name: 'Gmail', meta: { logo: 'https://example.com/gmail-logo.png', }, is_no_auth: false, connected_account: { id: 'conn_123', status: 'ACTIVE', auth_config: { id: 'auth_config_123', auth_scheme: 'OAUTH2', is_composio_managed: true, }, }, }, { slug: 'slack', name: 'Slack', meta: { logo: 'https://example.com/slack-logo.png', }, is_no_auth: false, connected_account: { id: 'conn_456', status: 'INITIATED', auth_config: { id: 'auth_config_456', auth_scheme: 'OAUTH2', is_composio_managed: true, }, }, }, { slug: 'github', name: 'GitHub', meta: { logo: 'https://example.com/github-logo.png', }, is_no_auth: false, connected_account: null, }, ], next_cursor: 'cursor_789', total_pages: 2, }; describe('ToolRouter', () => { let toolRouter: ToolRouter; let mockClient: ReturnType; let mockProvider: MockProvider; // Helper function to create expected manage_connections object const createExpectedManageConnections = ( overrides: { enable?: boolean; callbackUrl?: string; } = {} ) => ({ enable: overrides.enable ?? true, callback_url: overrides.callbackUrl ?? undefined, }); beforeEach(() => { vi.clearAllMocks(); mockClient = createMockClient(); mockProvider = new MockProvider(); toolRouter = new ToolRouter(mockClient as unknown as ComposioClient, { provider: mockProvider, apiKey: 'test-api-key', }); mockClient.toolRouter.session.retrieve.mockResolvedValue(mockSessionRetrieveResponse); mockClient.toolRouter.session.attach.mockResolvedValue(mockSessionRetrieveResponse); }); describe('constructor', () => { it('should create an instance successfully', () => { expect(toolRouter).toBeInstanceOf(ToolRouter); expect(telemetry.instrument).toHaveBeenCalledWith(toolRouter, 'ToolRouter'); }); it('should store the client reference', () => { expect(toolRouter['client']).toBe(mockClient); }); it('should store the config reference', () => { expect(toolRouter['config']).toEqual({ provider: mockProvider, apiKey: 'test-api-key' }); }); }); describe('create method', () => { const userId = 'user_123'; describe('basic session creation', () => { it('should create a session with minimal configuration', async () => { mockClient.toolRouter.session.create.mockResolvedValueOnce(mockSessionCreateResponse); const session = await toolRouter.create(userId); expect(mockClient.toolRouter.session.create).toHaveBeenCalledWith( { user_id: userId, toolkits: undefined, auth_configs: undefined, connected_accounts: undefined, tools: undefined, tags: undefined, manage_connections: createExpectedManageConnections(), workbench: undefined, }, undefined ); expect(session).toHaveProperty('sessionId', 'session_123'); expect(session).toHaveProperty('mcp'); expect(session.mcp).toEqual({ type: 'http', url: 'https://mcp.example.com/session_123', headers: { 'x-api-key': 'test-api-key', }, }); expect(session).toHaveProperty('tools'); expect(session).toHaveProperty('authorize'); expect(session).toHaveProperty('toolkits'); }); it('should create a session with empty config object', async () => { mockClient.toolRouter.session.create.mockResolvedValueOnce(mockSessionCreateResponse); const session = await toolRouter.create(userId, {}); expect(mockClient.toolRouter.session.create).toHaveBeenCalledWith( { user_id: userId, toolkits: undefined, auth_configs: undefined, connected_accounts: undefined, tools: undefined, tags: undefined, manage_connections: createExpectedManageConnections(), workbench: undefined, }, undefined ); expect(session.sessionId).toBe('session_123'); expect(session.preload.tools).toEqual([]); expect(session.configVersion).toBe(1); }); it('should create a session with preloaded tools', async () => { mockClient.toolRouter.session.create.mockResolvedValueOnce({ ...mockSessionCreateResponse, config: { preload: { tools: ['GMAIL_FETCH_EMAILS'] }, }, config_version: 2, }); const session = await toolRouter.create(userId, { toolkits: ['gmail'], preload: { tools: ['GMAIL_FETCH_EMAILS'] }, }); expect(mockClient.toolRouter.session.create).toHaveBeenCalledWith( { user_id: userId, toolkits: { enable: ['gmail'], }, auth_configs: undefined, connected_accounts: undefined, tools: undefined, tags: undefined, manage_connections: createExpectedManageConnections(), workbench: undefined, preload: { tools: ['GMAIL_FETCH_EMAILS'] }, }, undefined ); expect(session.preload.tools).toEqual(['GMAIL_FETCH_EMAILS']); expect(session.configVersion).toBe(2); }); it('should create a session with preload all', async () => { mockClient.toolRouter.session.create.mockResolvedValueOnce({ ...mockSessionCreateResponse, config: { preload: { tools: 'all' }, }, config_version: 2, }); const session = await toolRouter.create(userId, { toolkits: ['github'], preload: { tools: 'all' }, }); expect(mockClient.toolRouter.session.create).toHaveBeenCalledWith( { user_id: userId, toolkits: { enable: ['github'], }, auth_configs: undefined, connected_accounts: undefined, tools: undefined, tags: undefined, manage_connections: createExpectedManageConnections(), workbench: undefined, preload: { tools: 'all' }, }, undefined ); expect(session.preload.tools).toBe('all'); }); it('should reject custom tool slugs in top-level preload', async () => { const grepTool = createCustomTool('GREP', { name: 'Grep', description: 'Search local text', inputParams: z.object({ pattern: z.string() }), execute: vi.fn(async () => ({ matches: [] })), }); await expect( toolRouter.create(userId, { preload: { tools: ['GREP'] }, experimental: { customTools: [grepTool] }, }) ).rejects.toThrow('Set preload: true on the SDK custom tool'); expect(mockClient.toolRouter.session.create).not.toHaveBeenCalled(); }); it('should apply the direct_tools session preset defaults', async () => { mockClient.toolRouter.session.create.mockResolvedValueOnce({ ...mockSessionCreateResponse, config: { preload: { tools: 'all' }, }, }); const session = await toolRouter.create(userId, { sessionPreset: SessionPreset.DIRECT_TOOLS, toolkits: ['github'], }); expect(mockClient.toolRouter.session.create).toHaveBeenCalledWith( { user_id: userId, toolkits: { enable: ['github'], }, auth_configs: undefined, connected_accounts: undefined, tools: undefined, tags: undefined, manage_connections: { enable: false }, workbench: { enable: false }, multi_account: undefined, preload: { tools: 'all' }, search: { enable: false }, execute: { enable_multi_execute: false }, experimental: undefined, }, undefined ); expect(session.preload.tools).toBe('all'); }); it('should respect explicit overrides with the direct_tools session preset', async () => { mockClient.toolRouter.session.create.mockResolvedValueOnce({ ...mockSessionCreateResponse, config: { preload: { tools: ['GITHUB_CREATE_ISSUE'] }, }, }); const session = await toolRouter.create(userId, { sessionPreset: SessionPreset.DIRECT_TOOLS, toolkits: ['github'], manageConnections: true, workbench: { enable: true }, preload: { tools: ['GITHUB_CREATE_ISSUE'] }, }); expect(mockClient.toolRouter.session.create).toHaveBeenCalledWith( { user_id: userId, toolkits: { enable: ['github'], }, auth_configs: undefined, connected_accounts: undefined, tools: undefined, tags: undefined, manage_connections: { enable: true }, workbench: { enable: true }, multi_account: undefined, preload: { tools: ['GITHUB_CREATE_ISSUE'] }, search: { enable: false }, execute: { enable_multi_execute: false }, experimental: undefined, }, undefined ); expect(session.preload.tools).toEqual(['GITHUB_CREATE_ISSUE']); }); it('should not apply direct_tools custom preload default when preload is explicitly overridden', async () => { const grepTool = createCustomTool('GREP', { name: 'Grep', description: 'Search local text', inputParams: z.object({ pattern: z.string() }), execute: vi.fn(async () => ({ matches: [] })), }); mockClient.toolRouter.session.create.mockResolvedValueOnce({ ...mockSessionCreateResponse, config: { preload: { tools: ['GITHUB_CREATE_ISSUE'] }, }, experimental: { custom_tools: [ { slug: 'SERVER_GREP', original_slug: 'GREP', extends_toolkit: null, }, ], }, }); await toolRouter.create(userId, { sessionPreset: SessionPreset.DIRECT_TOOLS, toolkits: ['github'], preload: { tools: ['GITHUB_CREATE_ISSUE'] }, experimental: { customTools: [grepTool] }, }); const payload = mockClient.toolRouter.session.create.mock.calls[0][0]; expect(payload.preload).toEqual({ tools: ['GITHUB_CREATE_ISSUE'] }); expect(payload.experimental?.custom_tools?.[0]).not.toHaveProperty('preload'); }); it('should create a session with user ID only and verify MCP type transformation', async () => { mockClient.toolRouter.session.create.mockResolvedValueOnce(mockSessionCreateResponse); const session = await toolRouter.create(userId); expect(session.mcp.type).toBe('http'); expect(mockClient.toolRouter.session.create).toHaveBeenCalledTimes(1); }); }); describe('toolkits configuration', () => { it('should create a session with toolkits as array (enable toolkits)', async () => { mockClient.toolRouter.session.create.mockResolvedValueOnce(mockSessionCreateResponse); const config: ToolRouterCreateSessionConfig = { toolkits: ['gmail', 'slack', 'github'], }; await toolRouter.create(userId, config); expect(mockClient.toolRouter.session.create).toHaveBeenCalledWith( { user_id: userId, toolkits: { enable: ['gmail', 'slack', 'github'], }, auth_configs: undefined, connected_accounts: undefined, tools: undefined, tags: undefined, manage_connections: createExpectedManageConnections(), workbench: undefined, }, undefined ); }); it('should create a session with enable toolkits configuration', async () => { mockClient.toolRouter.session.create.mockResolvedValueOnce(mockSessionCreateResponse); const config: ToolRouterCreateSessionConfig = { toolkits: { enable: ['gmail', 'slack'], }, }; await toolRouter.create(userId, config); expect(mockClient.toolRouter.session.create).toHaveBeenCalledWith( { user_id: userId, toolkits: { enable: ['gmail', 'slack'], }, auth_configs: undefined, connected_accounts: undefined, tools: undefined, tags: undefined, manage_connections: createExpectedManageConnections(), workbench: undefined, }, undefined ); }); it('should create a session with disable toolkits configuration', async () => { mockClient.toolRouter.session.create.mockResolvedValueOnce(mockSessionCreateResponse); const config: ToolRouterCreateSessionConfig = { toolkits: { disable: ['notion', 'trello'], }, }; await toolRouter.create(userId, config); expect(mockClient.toolRouter.session.create).toHaveBeenCalledWith( { user_id: userId, toolkits: { disable: ['notion', 'trello'], }, auth_configs: undefined, connected_accounts: undefined, tools: undefined, tags: undefined, manage_connections: createExpectedManageConnections(), workbench: undefined, }, undefined ); }); }); describe('tags configuration', () => { it('should create a session with tags as array', async () => { mockClient.toolRouter.session.create.mockResolvedValueOnce(mockSessionCreateResponse); const config: ToolRouterCreateSessionConfig = { tags: ['readOnlyHint', 'idempotentHint'], }; await toolRouter.create(userId, config); expect(mockClient.toolRouter.session.create).toHaveBeenCalledWith( { user_id: userId, toolkits: undefined, auth_configs: undefined, connected_accounts: undefined, tools: undefined, tags: { enable: ['readOnlyHint', 'idempotentHint'], }, manage_connections: createExpectedManageConnections(), workbench: undefined, }, undefined ); }); it('should create a session with tags object with enable property', async () => { mockClient.toolRouter.session.create.mockResolvedValueOnce(mockSessionCreateResponse); const config: ToolRouterCreateSessionConfig = { tags: { enable: ['readOnlyHint', 'idempotentHint'], }, }; await toolRouter.create(userId, config); expect(mockClient.toolRouter.session.create).toHaveBeenCalledWith( { user_id: userId, toolkits: undefined, auth_configs: undefined, connected_accounts: undefined, tools: undefined, tags: { enable: ['readOnlyHint', 'idempotentHint'], }, manage_connections: createExpectedManageConnections(), workbench: undefined, }, undefined ); }); it('should create a session with tags object with disable property', async () => { mockClient.toolRouter.session.create.mockResolvedValueOnce(mockSessionCreateResponse); const config: ToolRouterCreateSessionConfig = { tags: { disable: ['destructiveHint'], }, }; await toolRouter.create(userId, config); expect(mockClient.toolRouter.session.create).toHaveBeenCalledWith( { user_id: userId, toolkits: undefined, auth_configs: undefined, connected_accounts: undefined, tools: undefined, tags: { enable: undefined, disable: ['destructiveHint'], }, manage_connections: createExpectedManageConnections(), workbench: undefined, }, undefined ); }); it('should create a session with tags object with both enable and disable properties', async () => { mockClient.toolRouter.session.create.mockResolvedValueOnce(mockSessionCreateResponse); const config: ToolRouterCreateSessionConfig = { tags: { enable: ['readOnlyHint', 'idempotentHint'], disable: ['destructiveHint'], }, }; await toolRouter.create(userId, config); expect(mockClient.toolRouter.session.create).toHaveBeenCalledWith( { user_id: userId, toolkits: undefined, auth_configs: undefined, connected_accounts: undefined, tools: undefined, tags: { enable: ['readOnlyHint', 'idempotentHint'], disable: ['destructiveHint'], }, manage_connections: createExpectedManageConnections(), workbench: undefined, }, undefined ); }); it('should create a session with single tag in array', async () => { mockClient.toolRouter.session.create.mockResolvedValueOnce(mockSessionCreateResponse); const config: ToolRouterCreateSessionConfig = { tags: ['readOnlyHint'], }; await toolRouter.create(userId, config); expect(mockClient.toolRouter.session.create).toHaveBeenCalledWith( { user_id: userId, toolkits: undefined, auth_configs: undefined, connected_accounts: undefined, tools: undefined, tags: { enable: ['readOnlyHint'], }, manage_connections: createExpectedManageConnections(), workbench: undefined, }, undefined ); }); it('should create a session with all tag types', async () => { mockClient.toolRouter.session.create.mockResolvedValueOnce(mockSessionCreateResponse); const config: ToolRouterCreateSessionConfig = { tags: ['readOnlyHint', 'destructiveHint', 'idempotentHint', 'openWorldHint'], }; await toolRouter.create(userId, config); expect(mockClient.toolRouter.session.create).toHaveBeenCalledWith( { user_id: userId, toolkits: undefined, auth_configs: undefined, connected_accounts: undefined, tools: undefined, tags: { enable: ['readOnlyHint', 'destructiveHint', 'idempotentHint', 'openWorldHint'], }, manage_connections: createExpectedManageConnections(), workbench: undefined, }, undefined ); }); it('should create a session with tags combined with toolkits', async () => { mockClient.toolRouter.session.create.mockResolvedValueOnce(mockSessionCreateResponse); const config: ToolRouterCreateSessionConfig = { toolkits: ['gmail', 'slack'], tags: ['readOnlyHint'], }; await toolRouter.create(userId, config); expect(mockClient.toolRouter.session.create).toHaveBeenCalledWith( { user_id: userId, toolkits: { enable: ['gmail', 'slack'], }, auth_configs: undefined, connected_accounts: undefined, tools: undefined, tags: { enable: ['readOnlyHint'], }, manage_connections: createExpectedManageConnections(), workbench: undefined, }, undefined ); }); }); // describe('tools configuration', () => { // it('should create a session with tools overrides as array', async () => { // mockClient.toolRouter.session.create.mockResolvedValueOnce(mockSessionCreateResponse); // const config: ToolRouterCreateSessionConfig = { // tools: { // overrides: { // gmail: ['GMAIL_FETCH_EMAILS', 'GMAIL_SEND_EMAIL'], // }, // }, // }; // await toolRouter.create(userId, config); // expect(mockClient.toolRouter.session.create).toHaveBeenCalledWith({ // user_id: userId, // toolkits: undefined, // auth_configs: undefined, // connected_accounts: undefined, // tools: { // overrides: { // gmail: { enable: ['GMAIL_FETCH_EMAILS', 'GMAIL_SEND_EMAIL'] }, // }, // filters: undefined, // }, // connections: createExpectedConnections(), // execution: undefined, // }); // }); // it('should create a session with tools overrides with enable tools', async () => { // mockClient.toolRouter.session.create.mockResolvedValueOnce(mockSessionCreateResponse); // const config: ToolRouterCreateSessionConfig = { // tools: { // overrides: { // gmail: { enable: ['GMAIL_FETCH_EMAILS', 'GMAIL_SEND_EMAIL'] }, // }, // }, // }; // await toolRouter.create(userId, config); // expect(mockClient.toolRouter.session.create).toHaveBeenCalledWith({ // user_id: userId, // toolkits: undefined, // auth_configs: undefined, // connected_accounts: undefined, // tools: { // overrides: { // gmail: { enable: ['GMAIL_FETCH_EMAILS', 'GMAIL_SEND_EMAIL'] }, // }, // filters: undefined, // }, // connections: createExpectedConnections(), // execution: undefined, // }); // }); // it('should create a session with tools overrides with disable tools', async () => { // mockClient.toolRouter.session.create.mockResolvedValueOnce(mockSessionCreateResponse); // const config: ToolRouterCreateSessionConfig = { // tools: { // overrides: { // gmail: { disable: ['GMAIL_DELETE_EMAIL'] }, // }, // }, // }; // await toolRouter.create(userId, config); // expect(mockClient.toolRouter.session.create).toHaveBeenCalledWith({ // user_id: userId, // toolkits: undefined, // auth_configs: undefined, // connected_accounts: undefined, // tools: { // overrides: { // gmail: { disable: ['GMAIL_DELETE_EMAIL'] }, // }, // filters: undefined, // }, // connections: createExpectedConnections(), // execution: undefined, // }); // }); // it('should create a session with tag filters as array', async () => { // mockClient.toolRouter.session.create.mockResolvedValueOnce(mockSessionCreateResponse); // const config: ToolRouterCreateSessionConfig = { // tools: { // tags: ['important', 'email'], // }, // }; // await toolRouter.create(userId, config); // expect(mockClient.toolRouter.session.create).toHaveBeenCalledWith({ // user_id: userId, // toolkits: undefined, // auth_configs: undefined, // connected_accounts: undefined, // tools: { // overrides: undefined, // filters: { // tags: { include: ['important', 'email'] }, // }, // }, // connections: createExpectedConnections(), // execution: undefined, // }); // }); // it('should create a session with enable tag filters', async () => { // mockClient.toolRouter.session.create.mockResolvedValueOnce(mockSessionCreateResponse); // const config: ToolRouterCreateSessionConfig = { // tools: { // tags: { enable: ['important', 'email'] }, // }, // }; // await toolRouter.create(userId, config); // expect(mockClient.toolRouter.session.create).toHaveBeenCalledWith({ // user_id: userId, // toolkits: undefined, // auth_configs: undefined, // connected_accounts: undefined, // tools: { // overrides: undefined, // filters: { // tags: { include: ['important', 'email'] }, // }, // }, // connections: createExpectedConnections(), // execution: undefined, // }); // }); // it('should create a session with disable tag filters', async () => { // mockClient.toolRouter.session.create.mockResolvedValueOnce(mockSessionCreateResponse); // const config: ToolRouterCreateSessionConfig = { // tools: { // tags: { disable: ['dangerous'] }, // }, // }; // await toolRouter.create(userId, config); // expect(mockClient.toolRouter.session.create).toHaveBeenCalledWith({ // user_id: userId, // toolkits: undefined, // auth_configs: undefined, // connected_accounts: undefined, // tools: { // overrides: undefined, // filters: { // tags: { exclude: ['dangerous'] }, // }, // }, // connections: createExpectedConnections(), // execution: undefined, // }); // }); // it('should create a session with both overrides and tag filters', async () => { // mockClient.toolRouter.session.create.mockResolvedValueOnce(mockSessionCreateResponse); // const config: ToolRouterCreateSessionConfig = { // tools: { // overrides: { // gmail: ['GMAIL_FETCH_EMAILS'], // slack: { disable: ['SLACK_DELETE_MESSAGE'] }, // }, // tags: ['important'], // }, // }; // await toolRouter.create(userId, config); // expect(mockClient.toolRouter.session.create).toHaveBeenCalledWith({ // user_id: userId, // toolkits: undefined, // auth_configs: undefined, // connected_accounts: undefined, // tools: { // overrides: { // gmail: { enable: ['GMAIL_FETCH_EMAILS'] }, // slack: { disable: ['SLACK_DELETE_MESSAGE'] }, // }, // filters: { // tags: { include: ['important'] }, // }, // }, // connections: createExpectedConnections(), // execution: undefined, // }); // }); // }); describe('manageConnections configuration', () => { it('should create a session with manageConnections as boolean (true)', async () => { mockClient.toolRouter.session.create.mockResolvedValueOnce(mockSessionCreateResponse); const config: ToolRouterCreateSessionConfig = { manageConnections: true, }; await toolRouter.create(userId, config); expect(mockClient.toolRouter.session.create).toHaveBeenCalledWith( { user_id: userId, toolkits: undefined, auth_configs: undefined, connected_accounts: undefined, tools: undefined, tags: undefined, manage_connections: createExpectedManageConnections({ enable: true }), workbench: undefined, }, undefined ); }); it('should create a session with manageConnections as boolean (false)', async () => { mockClient.toolRouter.session.create.mockResolvedValueOnce(mockSessionCreateResponse); const config: ToolRouterCreateSessionConfig = { manageConnections: false, }; await toolRouter.create(userId, config); expect(mockClient.toolRouter.session.create).toHaveBeenCalledWith( { user_id: userId, toolkits: undefined, auth_configs: undefined, connected_accounts: undefined, tools: undefined, tags: undefined, manage_connections: createExpectedManageConnections({ enable: false }), workbench: undefined, }, undefined ); }); it('should create a session with manageConnections as object with enable', async () => { mockClient.toolRouter.session.create.mockResolvedValueOnce(mockSessionCreateResponse); const config: ToolRouterCreateSessionConfig = { manageConnections: { enable: true, }, }; await toolRouter.create(userId, config); expect(mockClient.toolRouter.session.create).toHaveBeenCalledWith( { user_id: userId, toolkits: undefined, auth_configs: undefined, connected_accounts: undefined, tools: undefined, tags: undefined, manage_connections: createExpectedManageConnections({ enable: true }), workbench: undefined, }, undefined ); }); it('should create a session with manageConnections object with callbackUrl', async () => { mockClient.toolRouter.session.create.mockResolvedValueOnce(mockSessionCreateResponse); const config: ToolRouterCreateSessionConfig = { manageConnections: { enable: true, callbackUrl: 'https://myapp.com/callback', }, }; await toolRouter.create(userId, config); expect(mockClient.toolRouter.session.create).toHaveBeenCalledWith( { user_id: userId, toolkits: undefined, auth_configs: undefined, connected_accounts: undefined, tools: undefined, tags: undefined, manage_connections: createExpectedManageConnections({ enable: true, callbackUrl: 'https://myapp.com/callback', }), workbench: undefined, }, undefined ); }); it('should create a session with full manageConnections configuration', async () => { mockClient.toolRouter.session.create.mockResolvedValueOnce(mockSessionCreateResponse); const config: ToolRouterCreateSessionConfig = { manageConnections: { enable: true, callbackUrl: 'https://myapp.com/callback', }, }; await toolRouter.create(userId, config); expect(mockClient.toolRouter.session.create).toHaveBeenCalledWith( { user_id: userId, toolkits: undefined, auth_configs: undefined, connected_accounts: undefined, tools: undefined, tags: undefined, manage_connections: createExpectedManageConnections({ enable: true, callbackUrl: 'https://myapp.com/callback', }), workbench: undefined, }, undefined ); }); }); describe('authConfigs and connectedAccounts configuration', () => { it('should create a session with authConfigs', async () => { mockClient.toolRouter.session.create.mockResolvedValueOnce(mockSessionCreateResponse); const config: ToolRouterCreateSessionConfig = { authConfigs: { gmail: 'auth_config_123', slack: 'auth_config_456', }, }; await toolRouter.create(userId, config); expect(mockClient.toolRouter.session.create).toHaveBeenCalledWith( { user_id: userId, toolkits: undefined, auth_configs: { gmail: 'auth_config_123', slack: 'auth_config_456', }, connected_accounts: undefined, tools: undefined, tags: undefined, manage_connections: createExpectedManageConnections(), workbench: undefined, }, undefined ); }); it('should create a session with connectedAccounts (string coerced to array)', async () => { mockClient.toolRouter.session.create.mockResolvedValueOnce(mockSessionCreateResponse); const config: ToolRouterCreateSessionConfig = { connectedAccounts: { gmail: 'conn_123', slack: ['conn_456'], }, }; await toolRouter.create(userId, config); expect(mockClient.toolRouter.session.create).toHaveBeenCalledWith( { user_id: userId, toolkits: undefined, auth_configs: undefined, connected_accounts: { gmail: ['conn_123'], slack: ['conn_456'], }, tools: undefined, tags: undefined, manage_connections: createExpectedManageConnections(), workbench: undefined, }, undefined ); }); it('should create a session with both authConfigs and connectedAccounts', async () => { mockClient.toolRouter.session.create.mockResolvedValueOnce(mockSessionCreateResponse); const config: ToolRouterCreateSessionConfig = { authConfigs: { gmail: 'auth_config_123', }, connectedAccounts: { slack: 'conn_456', }, }; await toolRouter.create(userId, config); expect(mockClient.toolRouter.session.create).toHaveBeenCalledWith( { user_id: userId, toolkits: undefined, auth_configs: { gmail: 'auth_config_123', }, connected_accounts: { slack: ['conn_456'], }, tools: undefined, tags: undefined, manage_connections: createExpectedManageConnections(), workbench: undefined, }, undefined ); }); }); describe('workbench configuration', () => { it('should create a session with enableProxyExecution only', async () => { mockClient.toolRouter.session.create.mockResolvedValueOnce(mockSessionCreateResponse); const config: ToolRouterCreateSessionConfig = { workbench: { enableProxyExecution: true, }, }; await toolRouter.create(userId, config); expect(mockClient.toolRouter.session.create).toHaveBeenCalledWith( { user_id: userId, toolkits: undefined, auth_configs: undefined, connected_accounts: undefined, tools: undefined, tags: undefined, manage_connections: createExpectedManageConnections(), workbench: { enable: true, enable_proxy_execution: true, auto_offload_threshold: undefined, }, }, undefined ); }); it('should create a session with autoOffloadThreshold only', async () => { mockClient.toolRouter.session.create.mockResolvedValueOnce(mockSessionCreateResponse); const config: ToolRouterCreateSessionConfig = { workbench: { autoOffloadThreshold: 1000, }, }; await toolRouter.create(userId, config); expect(mockClient.toolRouter.session.create).toHaveBeenCalledWith( { user_id: userId, toolkits: undefined, auth_configs: undefined, connected_accounts: undefined, tools: undefined, tags: undefined, manage_connections: createExpectedManageConnections(), workbench: { enable: true, enable_proxy_execution: undefined, auto_offload_threshold: 1000, }, }, undefined ); }); it('should create a session with full workbench configuration', async () => { mockClient.toolRouter.session.create.mockResolvedValueOnce(mockSessionCreateResponse); const config: ToolRouterCreateSessionConfig = { workbench: { enableProxyExecution: true, autoOffloadThreshold: 500, }, }; await toolRouter.create(userId, config); expect(mockClient.toolRouter.session.create).toHaveBeenCalledWith( { user_id: userId, toolkits: undefined, auth_configs: undefined, connected_accounts: undefined, tools: undefined, tags: undefined, manage_connections: createExpectedManageConnections(), workbench: { enable: true, enable_proxy_execution: true, auto_offload_threshold: 500, }, }, undefined ); }); it('should create a session with workbench proxy and offload disabled', async () => { mockClient.toolRouter.session.create.mockResolvedValueOnce(mockSessionCreateResponse); const config: ToolRouterCreateSessionConfig = { workbench: { enableProxyExecution: false, autoOffloadThreshold: 0, }, }; await toolRouter.create(userId, config); expect(mockClient.toolRouter.session.create).toHaveBeenCalledWith( { user_id: userId, toolkits: undefined, auth_configs: undefined, connected_accounts: undefined, tools: undefined, tags: undefined, manage_connections: createExpectedManageConnections(), workbench: { enable: true, enable_proxy_execution: false, auto_offload_threshold: 0, }, }, undefined ); }); it('should create a session with workbench entirely disabled', async () => { mockClient.toolRouter.session.create.mockResolvedValueOnce(mockSessionCreateResponse); const config: ToolRouterCreateSessionConfig = { workbench: { enable: false, }, }; await toolRouter.create(userId, config); expect(mockClient.toolRouter.session.create).toHaveBeenCalledWith( { user_id: userId, toolkits: undefined, auth_configs: undefined, connected_accounts: undefined, tools: undefined, tags: undefined, manage_connections: createExpectedManageConnections(), workbench: { enable: false, enable_proxy_execution: undefined, auto_offload_threshold: undefined, }, }, undefined ); }); it('should create a session with workbench explicitly enabled', async () => { mockClient.toolRouter.session.create.mockResolvedValueOnce(mockSessionCreateResponse); const config: ToolRouterCreateSessionConfig = { workbench: { enable: true, enableProxyExecution: true, autoOffloadThreshold: 20000, }, }; await toolRouter.create(userId, config); expect(mockClient.toolRouter.session.create).toHaveBeenCalledWith( { user_id: userId, toolkits: undefined, auth_configs: undefined, connected_accounts: undefined, tools: undefined, tags: undefined, manage_connections: createExpectedManageConnections(), workbench: { enable: true, enable_proxy_execution: true, auto_offload_threshold: 20000, }, }, undefined ); }); it('forwards sandbox config to the existing workbench wire field', async () => { mockClient.toolRouter.session.create.mockResolvedValueOnce(mockSessionCreateResponse); await toolRouter.create(userId, { sandbox: { enableProxyExecution: false, autoOffloadThreshold: 300, sandboxSize: 'large' }, }); const payload = mockClient.toolRouter.session.create.mock.calls[0]?.[0]; expect(payload?.workbench).toEqual({ enable: true, enable_proxy_execution: false, auto_offload_threshold: 300, sandbox_size: 'large', }); }); it('forwards sandboxSize as snake_case sandbox_size on the wire', async () => { mockClient.toolRouter.session.create.mockResolvedValueOnce(mockSessionCreateResponse); await toolRouter.create(userId, { workbench: { sandboxSize: 'large' }, }); const payload = mockClient.toolRouter.session.create.mock.calls[0]?.[0]; expect(payload?.workbench?.sandbox_size).toBe('large'); }); it('rejects create config that passes both sandbox and workbench', async () => { await expect( toolRouter.create(userId, { sandbox: { enable: true }, workbench: { enable: true }, }) ).rejects.toThrow('Pass either sandbox or workbench'); }); it('rejects an invalid sandboxSize value via the zod schema', async () => { await expect( toolRouter.create(userId, { workbench: { sandboxSize: 'huge' }, } as unknown as ToolRouterCreateSessionConfig) ).rejects.toThrow(); }); }); // describe('complex configuration combinations', () => { // it('should create a session with all configuration options', async () => { // mockClient.toolRouter.session.create.mockResolvedValueOnce(mockSessionCreateResponse); // const config: ToolRouterCreateSessionConfig = { // toolkits: ['gmail', 'slack'], // tools: { // overrides: { // gmail: ['GMAIL_FETCH_EMAILS'], // }, // tags: ['important'], // }, // authConfigs: { // gmail: 'auth_config_123', // }, // connectedAccounts: { // slack: 'conn_456', // }, // manageConnections: { // enable: true, // callbackUri: 'https://myapp.com/callback', // }, // execution: { // enableProxyExecution: true, // autoOffloadThreshould: 30, // }, // }; // await toolRouter.create(userId, config); // expect(mockClient.toolRouter.session.create).toHaveBeenCalledWith({ // user_id: userId, // toolkits: { // enable: ['gmail', 'slack'], // }, // auth_configs: { // gmail: 'auth_config_123', // }, // connected_accounts: { // slack: 'conn_456', // }, // tools: { // overrides: { // gmail: { enable: ['GMAIL_FETCH_EMAILS'] }, // }, // filters: { // tags: { include: ['important'] }, // }, // }, // connections: createExpectedConnections({ // manageConnections: true, // callbackUri: 'https://myapp.com/callback', // }), // execution: { // enable_proxy_execution: true, // auto_offload_threshould: 30, // }, // }); // }); // it('should create a session with toolkits and disable tools', async () => { // mockClient.toolRouter.session.create.mockResolvedValueOnce(mockSessionCreateResponse); // const config: ToolRouterCreateSessionConfig = { // toolkits: { // enable: ['gmail', 'slack', 'github'], // }, // tools: { // overrides: { // gmail: { disable: ['GMAIL_DELETE_EMAIL'] }, // slack: { disable: ['SLACK_DELETE_MESSAGE'] }, // }, // }, // }; // await toolRouter.create(userId, config); // expect(mockClient.toolRouter.session.create).toHaveBeenCalledWith({ // user_id: userId, // toolkits: { // enable: ['gmail', 'slack', 'github'], // }, // auth_configs: undefined, // connected_accounts: undefined, // tools: { // overrides: { // gmail: { disable: ['GMAIL_DELETE_EMAIL'] }, // slack: { disable: ['SLACK_DELETE_MESSAGE'] }, // }, // filters: undefined, // }, // connections: createExpectedConnections(), // execution: undefined, // }); // }); // it('should create a session with complex nested configuration', async () => { // mockClient.toolRouter.session.create.mockResolvedValueOnce(mockSessionCreateResponse); // const config: ToolRouterCreateSessionConfig = { // toolkits: { // disable: ['notion'], // }, // tools: { // overrides: { // gmail: { disable: ['GMAIL_DELETE_EMAIL'] }, // }, // tags: { disable: ['dangerous'] }, // }, // authConfigs: { // gmail: 'auth_config_123', // slack: 'auth_config_456', // github: 'auth_config_789', // }, // connectedAccounts: {}, // manageConnections: { // enable: false, // }, // }; // await toolRouter.create(userId, config); // expect(mockClient.toolRouter.session.create).toHaveBeenCalledWith({ // user_id: userId, // toolkits: { // disable: ['notion'], // }, // auth_configs: { // gmail: 'auth_config_123', // slack: 'auth_config_456', // github: 'auth_config_789', // }, // connected_accounts: {}, // tools: { // overrides: { // gmail: { disable: ['GMAIL_DELETE_EMAIL'] }, // }, // filters: { // tags: { exclude: ['dangerous'] }, // }, // }, // connections: createExpectedConnections({ // manageConnections: false, // }), // execution: undefined, // }); // }); // }); describe('multiAccount configuration', () => { it('should create a session with multiAccount enable only', async () => { mockClient.toolRouter.session.create.mockResolvedValueOnce(mockSessionCreateResponse); const config: ToolRouterCreateSessionConfig = { multiAccount: { enable: true, }, }; await toolRouter.create(userId, config); expect(mockClient.toolRouter.session.create).toHaveBeenCalledWith( expect.objectContaining({ user_id: userId, multi_account: { enable: true, max_accounts_per_toolkit: undefined, require_explicit_selection: true, }, }), undefined ); }); it('should create a session with full multiAccount configuration', async () => { mockClient.toolRouter.session.create.mockResolvedValueOnce(mockSessionCreateResponse); const config: ToolRouterCreateSessionConfig = { multiAccount: { enable: true, maxAccountsPerToolkit: 3, requireExplicitSelection: true, }, }; await toolRouter.create(userId, config); expect(mockClient.toolRouter.session.create).toHaveBeenCalledWith( expect.objectContaining({ user_id: userId, multi_account: { enable: true, max_accounts_per_toolkit: 3, require_explicit_selection: true, }, }), undefined ); }); it('should not include multi_account when multiAccount is not provided', async () => { mockClient.toolRouter.session.create.mockResolvedValueOnce(mockSessionCreateResponse); await toolRouter.create(userId, {}); const callArgs = mockClient.toolRouter.session.create.mock.calls[0][0]; expect(callArgs.multi_account).toBeUndefined(); }); it('should create a session with multiAccount combined with other options', async () => { mockClient.toolRouter.session.create.mockResolvedValueOnce(mockSessionCreateResponse); const config: ToolRouterCreateSessionConfig = { toolkits: ['gmail', 'slack'], multiAccount: { enable: true, maxAccountsPerToolkit: 5, }, }; await toolRouter.create(userId, config); expect(mockClient.toolRouter.session.create).toHaveBeenCalledWith( expect.objectContaining({ user_id: userId, toolkits: { enable: ['gmail', 'slack'] }, multi_account: { enable: true, max_accounts_per_toolkit: 5, require_explicit_selection: true, }, }), undefined ); }); }); describe('experimental configuration', () => { it('should create a session with experimental assistivePrompt userTimezone', async () => { mockClient.toolRouter.session.create.mockResolvedValueOnce(mockSessionCreateResponse); const config: ToolRouterCreateSessionConfig = { experimental: { assistivePrompt: { userTimezone: 'America/New_York', }, }, }; await toolRouter.create(userId, config); expect(mockClient.toolRouter.session.create).toHaveBeenCalledWith( { user_id: userId, toolkits: undefined, auth_configs: undefined, connected_accounts: undefined, tools: undefined, tags: undefined, manage_connections: createExpectedManageConnections(), workbench: undefined, experimental: { assistive_prompt_config: { user_timezone: 'America/New_York', }, }, }, undefined ); }); it('should not include experimental in payload when assistivePrompt is not provided', async () => { mockClient.toolRouter.session.create.mockResolvedValueOnce(mockSessionCreateResponse); const config: ToolRouterCreateSessionConfig = { experimental: {}, }; await toolRouter.create(userId, config); expect(mockClient.toolRouter.session.create).toHaveBeenCalledWith( { user_id: userId, toolkits: undefined, auth_configs: undefined, connected_accounts: undefined, tools: undefined, tags: undefined, manage_connections: createExpectedManageConnections(), workbench: undefined, experimental: undefined, }, undefined ); }); it('should not include experimental in payload when userTimezone is not provided', async () => { mockClient.toolRouter.session.create.mockResolvedValueOnce(mockSessionCreateResponse); const config: ToolRouterCreateSessionConfig = { experimental: { assistivePrompt: {}, }, }; await toolRouter.create(userId, config); expect(mockClient.toolRouter.session.create).toHaveBeenCalledWith( { user_id: userId, toolkits: undefined, auth_configs: undefined, connected_accounts: undefined, tools: undefined, tags: undefined, manage_connections: createExpectedManageConnections(), workbench: undefined, experimental: undefined, }, undefined ); }); it('should create a session with experimental config combined with other options', async () => { mockClient.toolRouter.session.create.mockResolvedValueOnce(mockSessionCreateResponse); const config: ToolRouterCreateSessionConfig = { toolkits: ['gmail', 'slack'], experimental: { assistivePrompt: { userTimezone: 'Europe/London', }, }, }; await toolRouter.create(userId, config); expect(mockClient.toolRouter.session.create).toHaveBeenCalledWith( { user_id: userId, toolkits: { enable: ['gmail', 'slack'], }, auth_configs: undefined, connected_accounts: undefined, tools: undefined, tags: undefined, manage_connections: createExpectedManageConnections(), workbench: undefined, experimental: { assistive_prompt_config: { user_timezone: 'Europe/London', }, }, }, undefined ); }); it('should transform experimental assistive_prompt from API response to SDK format', async () => { const responseWithExperimental = { ...mockSessionCreateResponse, experimental: { assistive_prompt: 'You are a helpful assistant working in the America/New_York timezone.', }, }; mockClient.toolRouter.session.create.mockResolvedValueOnce(responseWithExperimental); const session = await toolRouter.create(userId, { experimental: { assistivePrompt: { userTimezone: 'America/New_York', }, }, }); expect(session.experimental.assistivePrompt).toEqual( 'You are a helpful assistant working in the America/New_York timezone.' ); expect(session.experimental.files).toBeDefined(); }); it('should handle API response without experimental field', async () => { mockClient.toolRouter.session.create.mockResolvedValueOnce(mockSessionCreateResponse); const session = await toolRouter.create(userId); expect(session.experimental).toBeDefined(); expect(session.experimental.assistivePrompt).toBeUndefined(); expect(session.experimental.files).toBeDefined(); }); }); describe('error handling', () => { it('should throw error if API call fails', async () => { const apiError = new Error('API error: Invalid session configuration'); mockClient.toolRouter.session.create.mockRejectedValueOnce(apiError); await expect(toolRouter.create(userId)).rejects.toThrow( 'API error: Invalid session configuration' ); expect(mockClient.toolRouter.session.create).toHaveBeenCalledTimes(1); }); it('should throw error for invalid configuration', async () => { const invalidConfig = { toolkits: 'invalid-toolkits', // Should be array or object } as any; await expect(toolRouter.create(userId, invalidConfig)).rejects.toThrow(); }); it('should pass through empty userId', async () => { mockClient.toolRouter.session.create.mockResolvedValueOnce(mockSessionCreateResponse); // The test validates that even empty userId is passed through // The API layer would handle the validation await toolRouter.create(''); expect(mockClient.toolRouter.session.create).toHaveBeenCalledWith( expect.objectContaining({ user_id: '', }), undefined ); }); }); describe('response handling', () => { it('should handle MCP type correctly', async () => { const responseWithHttp = { ...mockSessionCreateResponse, mcp: { type: 'http', url: 'https://mcp.example.com/session_123', }, }; mockClient.toolRouter.session.create.mockResolvedValueOnce(responseWithHttp); const session = await toolRouter.create(userId); expect(session.mcp.type).toBe('http'); }); it('should handle SSE MCP type correctly', async () => { const sseResponse = { ...mockSessionCreateResponse, mcp: { type: 'sse', url: 'https://mcp.example.com/sse/session_123', }, }; mockClient.toolRouter.session.create.mockResolvedValueOnce(sseResponse); const session = await toolRouter.create(userId); expect(session.mcp.type).toBe('sse'); expect(session.mcp.url).toBe('https://mcp.example.com/sse/session_123'); }); it('should return session with all required properties', async () => { mockClient.toolRouter.session.create.mockResolvedValueOnce(mockSessionCreateResponse); const session = await toolRouter.create(userId); expect(session).toHaveProperty('sessionId'); expect(session).toHaveProperty('mcp'); expect(session).toHaveProperty('tools'); expect(session).toHaveProperty('authorize'); expect(session).toHaveProperty('toolkits'); expect(typeof session.tools).toBe('function'); expect(typeof session.authorize).toBe('function'); expect(typeof session.toolkits).toBe('function'); }); }); }); describe('authorize function', () => { const userId = 'user_123'; const sessionId = 'session_123'; const toolkit = 'gmail'; beforeEach(async () => { mockClient.toolRouter.session.create.mockResolvedValueOnce(mockSessionCreateResponse); }); it('should authorize a toolkit without callbackUrl', async () => { mockClient.toolRouter.session.link.mockResolvedValueOnce(mockLinkResponse); const session = await toolRouter.create(userId); const connectionRequest = await session.authorize(toolkit); expect(mockClient.toolRouter.session.link).toHaveBeenCalledWith( sessionId, { toolkit, }, undefined ); expect(connectionRequest).toHaveProperty('id', 'conn_456'); expect(connectionRequest).toHaveProperty('status', ConnectedAccountStatuses.INITIATED); expect(connectionRequest).toHaveProperty('redirectUrl', 'https://composio.dev/auth/redirect'); }); it('should authorize a toolkit with callbackUrl', async () => { mockClient.toolRouter.session.link.mockResolvedValueOnce(mockLinkResponse); const session = await toolRouter.create(userId); const connectionRequest = await session.authorize(toolkit, { callbackUrl: 'https://myapp.com/callback', }); expect(mockClient.toolRouter.session.link).toHaveBeenCalledWith( sessionId, { toolkit, callback_url: 'https://myapp.com/callback', }, undefined ); expect(connectionRequest.id).toBe('conn_456'); expect(connectionRequest.redirectUrl).toBe('https://composio.dev/auth/redirect'); }); it('should authorize multiple toolkits in sequence', async () => { const slackLinkResponse = { connected_account_id: 'conn_789', redirect_url: 'https://composio.dev/auth/slack/redirect', }; mockClient.toolRouter.session.link .mockResolvedValueOnce(mockLinkResponse) .mockResolvedValueOnce(slackLinkResponse); const session = await toolRouter.create(userId); const gmailConnection = await session.authorize('gmail'); const slackConnection = await session.authorize('slack'); expect(mockClient.toolRouter.session.link).toHaveBeenCalledTimes(2); expect(gmailConnection.id).toBe('conn_456'); expect(slackConnection.id).toBe('conn_789'); }); it('should handle authorization errors', async () => { const authError = new Error('Authorization failed: Invalid toolkit'); mockClient.toolRouter.session.link.mockRejectedValueOnce(authError); const session = await toolRouter.create(userId); await expect(session.authorize(toolkit)).rejects.toThrow( 'Authorization failed: Invalid toolkit' ); expect(mockClient.toolRouter.session.link).toHaveBeenCalledWith( sessionId, { toolkit, }, undefined ); }); it('should handle network errors during authorization', async () => { const networkError = new Error('Network error'); mockClient.toolRouter.session.link.mockRejectedValueOnce(networkError); const session = await toolRouter.create(userId); await expect(session.authorize(toolkit)).rejects.toThrow('Network error'); }); it('should create ConnectionRequest with correct parameters', async () => { mockClient.toolRouter.session.link.mockResolvedValueOnce(mockLinkResponse); const session = await toolRouter.create(userId); const connectionRequest = await session.authorize(toolkit); // Verify the connection request has the expected methods expect(connectionRequest).toHaveProperty('waitForConnection'); expect(connectionRequest).toHaveProperty('toJSON'); expect(connectionRequest).toHaveProperty('toString'); expect(typeof connectionRequest.waitForConnection).toBe('function'); }); // authorize() validates experimental.aclConfigForShared at the SDK // boundary — same caps as link() (1000-entry list, 256-char user_id). it('forwards the experimental block to the wire', async () => { mockClient.toolRouter.session.link.mockResolvedValueOnce(mockLinkResponse); const session = await toolRouter.create(userId); await session.authorize(toolkit, { experimental: { accountType: 'SHARED', aclConfigForShared: { allowAllUsers: true, notAllowedUserIds: ['user_bob'], }, }, }); expect(mockClient.toolRouter.session.link).toHaveBeenCalledWith( sessionId, { toolkit, experimental: { account_type: 'SHARED', acl_config_for_shared: { allow_all_users: true, not_allowed_user_ids: ['user_bob'], }, }, }, undefined ); }); it('omits the experimental block entirely when not provided', async () => { mockClient.toolRouter.session.link.mockResolvedValueOnce(mockLinkResponse); const session = await toolRouter.create(userId); await session.authorize(toolkit); const body = mockClient.toolRouter.session.link.mock.calls[0][1]; expect('experimental' in body).toBe(false); }); it('throws ValidationError when allowedUserIds exceeds the 1000-entry cap', async () => { const session = await toolRouter.create(userId); const oversizedList = Array.from({ length: 1001 }, (_, i) => `user_${i}`); await expect( session.authorize(toolkit, { experimental: { accountType: 'SHARED', aclConfigForShared: { allowedUserIds: oversizedList }, }, }) ).rejects.toMatchObject({ name: 'ValidationError' }); expect(mockClient.toolRouter.session.link).not.toHaveBeenCalled(); }); it('throws ValidationError when a user_id exceeds the 256-char length cap', async () => { const session = await toolRouter.create(userId); const tooLongUserId = 'u'.repeat(257); await expect( session.authorize(toolkit, { experimental: { accountType: 'SHARED', aclConfigForShared: { allowedUserIds: [tooLongUserId] }, }, }) ).rejects.toMatchObject({ name: 'ValidationError' }); expect(mockClient.toolRouter.session.link).not.toHaveBeenCalled(); }); it('throws ValidationError when a user_id is empty', async () => { const session = await toolRouter.create(userId); await expect( session.authorize(toolkit, { experimental: { accountType: 'SHARED', aclConfigForShared: { notAllowedUserIds: [''] }, }, }) ).rejects.toMatchObject({ name: 'ValidationError' }); expect(mockClient.toolRouter.session.link).not.toHaveBeenCalled(); }); }); describe('toolkits function', () => { const userId = 'user_123'; const sessionId = 'session_123'; beforeEach(async () => { mockClient.toolRouter.session.create.mockResolvedValueOnce(mockSessionCreateResponse); }); it('should fetch toolkits without options', async () => { mockClient.toolRouter.session.toolkits.mockResolvedValueOnce(mockToolkitsResponse); const session = await toolRouter.create(userId); const result = await session.toolkits(); expect(mockClient.toolRouter.session.toolkits).toHaveBeenCalledWith( sessionId, { cursor: undefined, limit: undefined, toolkits: undefined, is_connected: undefined, }, undefined ); expect(result).toHaveProperty('items'); expect(result).toHaveProperty('cursor', 'cursor_789'); expect(result).toHaveProperty('totalPages', 2); expect(result.items).toHaveLength(3); }); it('should fetch toolkits with pagination options', async () => { mockClient.toolRouter.session.toolkits.mockResolvedValueOnce(mockToolkitsResponse); const session = await toolRouter.create(userId); const result = await session.toolkits({ limit: 10, cursor: 'cursor_abc', }); expect(mockClient.toolRouter.session.toolkits).toHaveBeenCalledWith( sessionId, { cursor: 'cursor_abc', limit: 10, toolkits: undefined, is_connected: undefined, }, undefined ); expect(result.items).toHaveLength(3); }); it('should fetch toolkits with toolkits filter option', async () => { mockClient.toolRouter.session.toolkits.mockResolvedValueOnce(mockToolkitsResponse); const session = await toolRouter.create(userId); const result = await session.toolkits({ toolkits: ['gmail', 'slack'], }); expect(mockClient.toolRouter.session.toolkits).toHaveBeenCalledWith( sessionId, { cursor: undefined, limit: undefined, toolkits: ['gmail', 'slack'], is_connected: undefined, }, undefined ); expect(result.items).toHaveLength(3); }); it('should fetch toolkits with isConnected filter option', async () => { mockClient.toolRouter.session.toolkits.mockResolvedValueOnce(mockToolkitsResponse); const session = await toolRouter.create(userId); const result = await session.toolkits({ isConnected: true, }); expect(mockClient.toolRouter.session.toolkits).toHaveBeenCalledWith( sessionId, { cursor: undefined, limit: undefined, toolkits: undefined, is_connected: true, }, undefined ); expect(result.items).toHaveLength(3); }); it('should fetch toolkits with both pagination and toolkits filter options', async () => { mockClient.toolRouter.session.toolkits.mockResolvedValueOnce(mockToolkitsResponse); const session = await toolRouter.create(userId); const result = await session.toolkits({ limit: 5, cursor: 'cursor_xyz', toolkits: ['github'], }); expect(mockClient.toolRouter.session.toolkits).toHaveBeenCalledWith( sessionId, { cursor: 'cursor_xyz', limit: 5, toolkits: ['github'], is_connected: undefined, }, undefined ); expect(result.items).toHaveLength(3); }); it('should fetch toolkits with all options combined', async () => { mockClient.toolRouter.session.toolkits.mockResolvedValueOnce(mockToolkitsResponse); const session = await toolRouter.create(userId); const result = await session.toolkits({ limit: 5, cursor: 'cursor_xyz', toolkits: ['github', 'gmail'], isConnected: false, }); expect(mockClient.toolRouter.session.toolkits).toHaveBeenCalledWith( sessionId, { cursor: 'cursor_xyz', limit: 5, toolkits: ['github', 'gmail'], is_connected: false, }, undefined ); expect(result.items).toHaveLength(3); }); it('should transform toolkit connection state correctly for active connection', async () => { mockClient.toolRouter.session.toolkits.mockResolvedValueOnce(mockToolkitsResponse); const session = await toolRouter.create(userId); const result = await session.toolkits(); const gmailToolkit = result.items[0]; expect(gmailToolkit.slug).toBe('gmail'); expect(gmailToolkit.name).toBe('Gmail'); expect(gmailToolkit.logo).toBe('https://example.com/gmail-logo.png'); expect(gmailToolkit.isNoAuth).toBe(false); expect(gmailToolkit.connection?.isActive).toBe(true); expect(gmailToolkit.connection?.authConfig).toEqual({ id: 'auth_config_123', mode: 'OAUTH2', isComposioManaged: true, }); expect(gmailToolkit.connection?.connectedAccount).toEqual({ id: 'conn_123', status: 'ACTIVE', }); }); it('should transform toolkit connection state correctly for initiated connection', async () => { mockClient.toolRouter.session.toolkits.mockResolvedValueOnce(mockToolkitsResponse); const session = await toolRouter.create(userId); const result = await session.toolkits(); const slackToolkit = result.items[1]; expect(slackToolkit.slug).toBe('slack'); expect(slackToolkit.connection?.isActive).toBe(false); expect(slackToolkit.connection?.connectedAccount?.status).toBe('INITIATED'); }); it('should transform toolkit connection state correctly for no connection', async () => { mockClient.toolRouter.session.toolkits.mockResolvedValueOnce(mockToolkitsResponse); const session = await toolRouter.create(userId); const result = await session.toolkits(); const githubToolkit = result.items[2]; expect(githubToolkit.slug).toBe('github'); expect(githubToolkit.connection?.isActive).toBe(false); expect(githubToolkit.connection?.authConfig).toBeNull(); expect(githubToolkit.connection?.connectedAccount).toBeUndefined(); }); it('should transform toolkit connection state correctly for no-auth toolkit', async () => { const noAuthToolkitsResponse = { items: [ { slug: 'codeinterpreter', name: 'Code Interpreter', meta: { logo: 'https://example.com/codeinterpreter-logo.png', }, is_no_auth: true, connected_account: null, }, ], next_cursor: undefined, total_pages: 1, }; mockClient.toolRouter.session.toolkits.mockResolvedValueOnce(noAuthToolkitsResponse); const session = await toolRouter.create(userId); const result = await session.toolkits(); const codeinterpreterToolkit = result.items[0]; expect(codeinterpreterToolkit.slug).toBe('codeinterpreter'); expect(codeinterpreterToolkit.name).toBe('Code Interpreter'); expect(codeinterpreterToolkit.isNoAuth).toBe(true); expect(codeinterpreterToolkit.connection).toBeUndefined(); }); it('should handle empty toolkits response', async () => { const emptyResponse = { items: [], next_cursor: undefined, total_pages: 0, }; mockClient.toolRouter.session.toolkits.mockResolvedValueOnce(emptyResponse); const session = await toolRouter.create(userId); const result = await session.toolkits(); expect(result.items).toHaveLength(0); expect(result.cursor).toBeUndefined(); expect(result.totalPages).toBe(0); }); it('should handle pagination through multiple pages', async () => { const firstPageResponse = { items: mockToolkitsResponse.items.slice(0, 2), next_cursor: 'cursor_page2', total_pages: 2, }; const secondPageResponse = { items: mockToolkitsResponse.items.slice(2), next_cursor: undefined, total_pages: 2, }; mockClient.toolRouter.session.toolkits .mockResolvedValueOnce(firstPageResponse) .mockResolvedValueOnce(secondPageResponse); const session = await toolRouter.create(userId); // Fetch first page const page1 = await session.toolkits({ limit: 2 }); expect(page1.items).toHaveLength(2); expect(page1.cursor).toBe('cursor_page2'); // Fetch second page const page2 = await session.toolkits({ limit: 2, cursor: page1.cursor, }); expect(page2.items).toHaveLength(1); expect(page2.cursor).toBeUndefined(); }); it('should handle API errors', async () => { const apiError = new Error('Failed to fetch toolkits'); mockClient.toolRouter.session.toolkits.mockRejectedValueOnce(apiError); const session = await toolRouter.create(userId); await expect(session.toolkits()).rejects.toThrow('Failed to fetch toolkits'); }); it('should handle malformed response data gracefully with undefined values', async () => { const malformedResponse = { items: [ { slug: 'gmail', // Missing required fields like name, meta, is_no_auth }, ], next_cursor: undefined, total_pages: 1, }; mockClient.toolRouter.session.toolkits.mockResolvedValueOnce(malformedResponse); const session = await toolRouter.create(userId); // The code handles malformed data gracefully with undefined values const result = await session.toolkits(); expect(result.items).toHaveLength(1); expect(result.items[0].slug).toBe('gmail'); expect(result.items[0].name).toBeUndefined(); expect(result.items[0].logo).toBeUndefined(); expect(result.items[0].isNoAuth).toBeUndefined(); }); it('should throw validation error for invalid options', async () => { const session = await toolRouter.create(userId); // Invalid options should throw a validation error await expect( session.toolkits({ limit: 'invalid' as unknown as number, // Invalid type }) ).rejects.toThrow(); }); }); describe('search function', () => { const userId = 'user_123'; const sessionId = 'session_123'; const mockSearchResponse = { success: true, error: null, results: [ { index: 1, use_case: 'send emails', primary_tool_slugs: ['GMAIL_SEND_EMAIL'], related_tool_slugs: [], toolkits: ['gmail'], }, ], tool_schemas: { GMAIL_SEND_EMAIL: { tool_slug: 'GMAIL_SEND_EMAIL', toolkit: 'gmail', description: 'Send an email', hasFullSchema: true, input_schema: {}, output_schema: {}, }, }, toolkit_connection_statuses: [ { toolkit: 'gmail', description: 'Gmail', has_active_connection: false, status_message: 'No connection', }, ], next_steps_guidance: ['Connect Gmail'], session: { id: 'trs_123', generate_id: false, instructions: 'Reuse session', }, time_info: { current_time_utc: '2025-03-09T12:00:00.000Z', current_time_utc_epoch_seconds: 1741521600, message: 'UTC', }, }; it('should call search with correct params and return camelCase response', async () => { mockClient.toolRouter.session.create.mockResolvedValueOnce(mockSessionCreateResponse); mockClient.toolRouter.session.search.mockResolvedValueOnce(mockSearchResponse); const session = await toolRouter.create(userId); const result = await session.search({ query: 'send emails' }); expect(mockClient.toolRouter.session.search).toHaveBeenCalledWith( sessionId, { queries: [{ use_case: 'send emails' }], }, undefined ); expect(result.success).toBe(true); expect(result.results[0].useCase).toBe('send emails'); expect(result.results[0].primaryToolSlugs).toEqual(['GMAIL_SEND_EMAIL']); expect(result.toolSchemas.GMAIL_SEND_EMAIL.toolSlug).toBe('GMAIL_SEND_EMAIL'); expect(result.nextStepsGuidance).toEqual(['Connect Gmail']); expect(result.session.generateId).toBe(false); expect(result.timeInfo.currentTimeUtcEpochSeconds).toBe(1741521600); }); it('should pass toolkits filter when provided', async () => { mockClient.toolRouter.session.create.mockResolvedValueOnce(mockSessionCreateResponse); mockClient.toolRouter.session.search.mockResolvedValueOnce(mockSearchResponse); const session = await toolRouter.create(userId); await session.search({ query: 'send emails', toolkits: ['gmail', 'slack'] }); expect(mockClient.toolRouter.session.search).toHaveBeenCalledWith( sessionId, { queries: [{ use_case: 'send emails' }], toolkits: ['gmail', 'slack'], }, undefined ); }); it('should pass inline custom tools to search', async () => { const grepTool = createCustomTool('GREP', { name: 'Grep', description: 'Search local text', preload: true, inputParams: z.object({ pattern: z.string() }), execute: vi.fn(async () => ({ matches: [] })), }); mockClient.toolRouter.session.create.mockResolvedValueOnce({ ...mockSessionCreateResponse, experimental: { custom_tools: [ { slug: 'LOCAL_GREP', original_slug: 'GREP', extends_toolkit: null, }, ], }, }); mockClient.toolRouter.session.search.mockResolvedValueOnce(mockSearchResponse); const session = await toolRouter.create(userId, { experimental: { customTools: [grepTool] }, }); await session.search({ query: 'search local text' }); expect(mockClient.toolRouter.session.search).toHaveBeenCalledWith( sessionId, { queries: [{ use_case: 'search local text' }], experimental: { custom_tools: [ expect.objectContaining({ slug: 'GREP', preload: true, }), ], }, }, undefined ); }); it('should propagate search API errors', async () => { mockClient.toolRouter.session.create.mockResolvedValueOnce(mockSessionCreateResponse); mockClient.toolRouter.session.search.mockRejectedValueOnce(new Error('Search failed')); const session = await toolRouter.create(userId); await expect(session.search({ query: 'send emails' })).rejects.toThrow('Search failed'); }); }); describe('execute function', () => { const userId = 'user_123'; const sessionId = 'session_123'; const mockExecuteResponse = { data: { tool_slug: 'GMAIL_SEND_EMAIL', id: 'msg_123' }, error: null, log_id: 'log_abc', }; it('should call execute with correct params and return camelCase response', async () => { mockClient.toolRouter.session.create.mockResolvedValueOnce(mockSessionCreateResponse); mockClient.toolRouter.session.execute.mockResolvedValueOnce(mockExecuteResponse); const session = await toolRouter.create(userId); const result = await session.execute('GMAIL_SEND_EMAIL', { to: 'user@example.com', subject: 'Hi', body: 'Hello', }); expect(mockClient.toolRouter.session.execute).toHaveBeenCalledWith( sessionId, { tool_slug: 'GMAIL_SEND_EMAIL', arguments: { to: 'user@example.com', subject: 'Hi', body: 'Hello' }, }, undefined ); expect(result.data).toEqual({ tool_slug: 'GMAIL_SEND_EMAIL', id: 'msg_123' }); expect(result.error).toBeNull(); expect(result.logId).toBe('log_abc'); }); it('should propagate execute API errors', async () => { mockClient.toolRouter.session.create.mockResolvedValueOnce(mockSessionCreateResponse); mockClient.toolRouter.session.execute.mockRejectedValueOnce(new Error('Execute failed')); const session = await toolRouter.create(userId); await expect(session.execute('GMAIL_SEND_EMAIL')).rejects.toThrow('Execute failed'); }); it('should default arguments to empty object when omitted', async () => { mockClient.toolRouter.session.create.mockResolvedValueOnce(mockSessionCreateResponse); mockClient.toolRouter.session.execute.mockResolvedValueOnce(mockExecuteResponse); const session = await toolRouter.create(userId); await session.execute('HACKERNEWS_GET_USER'); expect(mockClient.toolRouter.session.execute).toHaveBeenCalledWith( sessionId, { tool_slug: 'HACKERNEWS_GET_USER', arguments: {}, }, undefined ); }); it('should pass account option to session execute', async () => { mockClient.toolRouter.session.create.mockResolvedValueOnce(mockSessionCreateResponse); mockClient.toolRouter.session.execute.mockResolvedValueOnce(mockExecuteResponse); const session = await toolRouter.create(userId); await session.execute('GMAIL_SEND_EMAIL', { to: 'user@example.com' }, { account: 'work' }); expect(mockClient.toolRouter.session.execute).toHaveBeenCalledWith( sessionId, { tool_slug: 'GMAIL_SEND_EMAIL', arguments: { to: 'user@example.com' }, account: 'work', }, undefined ); }); it('should pass inline custom tools to remote execute when custom tools are bound', async () => { const grepTool = createCustomTool('GREP', { name: 'Grep', description: 'Search local text', inputParams: z.object({ pattern: z.string() }), execute: vi.fn(async () => ({ matches: [] })), }); mockClient.toolRouter.session.create.mockResolvedValueOnce({ ...mockSessionCreateResponse, experimental: { custom_tools: [ { slug: 'LOCAL_GREP', original_slug: 'GREP', extends_toolkit: null, }, ], }, }); mockClient.toolRouter.session.execute.mockResolvedValueOnce(mockExecuteResponse); const session = await toolRouter.create(userId, { experimental: { customTools: [grepTool] }, }); await session.execute('GMAIL_SEND_EMAIL', { to: 'user@example.com' }); expect(mockClient.toolRouter.session.execute).toHaveBeenCalledWith( sessionId, { tool_slug: 'GMAIL_SEND_EMAIL', arguments: { to: 'user@example.com' }, experimental: { custom_tools: [expect.objectContaining({ slug: 'GREP' })], }, }, undefined ); }); }); describe('tools function', () => { const userId = 'user_123'; const sessionId = 'session_123'; beforeEach(() => { // Reset the Tools mock before each test vi.clearAllMocks(); (Tools as any).mockImplementation(function () { return { getRawComposioTools: vi.fn().mockResolvedValue([{ slug: 'GMAIL_FETCH_EMAILS' }]), getRawToolRouterSessionTools: vi .fn() .mockResolvedValue([{ slug: 'COMPOSIO_SEARCH_TOOLS' }]), wrapToolsForToolRouter: vi.fn().mockReturnValue('mocked-wrapped-tools'), }; }); }); it('should fetch and wrap tools without modifiers', async () => { mockClient.toolRouter.session.create.mockResolvedValueOnce(mockSessionCreateResponse); const session = await toolRouter.create(userId); const tools = await session.tools(); expect(Tools).toHaveBeenCalledWith(mockClient, { provider: mockProvider, apiKey: 'test-api-key', }); const toolsInstance = (Tools as any).mock.results[0].value; expect(toolsInstance.getRawToolRouterSessionTools).toHaveBeenCalledWith( sessionId, undefined, undefined ); expect(toolsInstance.wrapToolsForToolRouter).toHaveBeenCalledWith( sessionId, [{ slug: 'COMPOSIO_SEARCH_TOOLS' }], undefined ); expect(tools).toBe('mocked-wrapped-tools'); }); it('should fetch and wrap tools with modifiers', async () => { mockClient.toolRouter.session.create.mockResolvedValueOnce(mockSessionCreateResponse); const modifiers = { modifySchema: vi.fn(tool => ({ ...tool, description: 'Modified description', })), beforeExecute: vi.fn(), }; const session = await toolRouter.create(userId); const tools = await session.tools(modifiers); const toolsInstance = (Tools as any).mock.results[0].value; expect(toolsInstance.getRawToolRouterSessionTools).toHaveBeenCalledWith( sessionId, { modifySchema: modifiers.modifySchema }, undefined ); expect(toolsInstance.wrapToolsForToolRouter).toHaveBeenCalledWith( sessionId, [{ slug: 'COMPOSIO_SEARCH_TOOLS' }], modifiers ); expect(tools).toBe('mocked-wrapped-tools'); }); it('should include preloaded tools returned by the session tools endpoint', async () => { mockClient.toolRouter.session.create.mockResolvedValueOnce({ ...mockSessionCreateResponse, config: { preload: { tools: ['GMAIL_FETCH_EMAILS'] }, }, }); (Tools as any).mockImplementation(function () { return { getRawComposioTools: vi.fn().mockResolvedValue([{ slug: 'GMAIL_FETCH_EMAILS' }]), getRawToolRouterSessionTools: vi .fn() .mockResolvedValue([ { slug: 'COMPOSIO_SEARCH_TOOLS' }, { slug: 'GMAIL_FETCH_EMAILS', toolkit: { slug: 'gmail', name: 'Gmail' } }, ]), wrapToolsForToolRouter: vi.fn().mockReturnValue('mocked-wrapped-tools'), }; }); const session = await toolRouter.create(userId, { toolkits: ['gmail'], preload: { tools: ['GMAIL_FETCH_EMAILS'] }, }); const tools = await session.tools(); const toolsInstance = (Tools as any).mock.results[0].value; expect(toolsInstance.wrapToolsForToolRouter).toHaveBeenCalledWith( sessionId, [ { slug: 'COMPOSIO_SEARCH_TOOLS' }, { slug: 'GMAIL_FETCH_EMAILS', toolkit: { slug: 'gmail', name: 'Gmail' } }, ], undefined ); expect(tools).toBe('mocked-wrapped-tools'); }); it('should include custom tools with preload enabled locally', async () => { const executeGrep = vi.fn(async ({ pattern }: { pattern: string }) => ({ matches: [pattern], })); const grepTool = createCustomTool('GREP', { name: 'Grep', description: 'Search local text', preload: true, inputParams: z.object({ pattern: z.string().describe('Pattern to search for'), }), execute: executeGrep, }); mockClient.toolRouter.session.create.mockResolvedValueOnce({ ...mockSessionCreateResponse, tool_router_tools: ['COMPOSIO_SEARCH_TOOLS'], experimental: { custom_tools: [ { slug: 'SERVER_GREP', original_slug: 'GREP', extends_toolkit: null, }, ], }, }); mockProvider.wrapTools.mockReturnValue('mocked-custom-tools'); const session = await toolRouter.create(userId, { experimental: { customTools: [grepTool] }, }); const tools = await session.tools(); expect(mockProvider.wrapTools).toHaveBeenCalledTimes(1); const wrappedTools = mockProvider.wrapTools.mock.calls[0][0] as Array<{ slug: string; description?: string; toolkit?: { slug: string; name: string }; }>; expect(wrappedTools.map(tool => tool.slug)).toEqual(['COMPOSIO_SEARCH_TOOLS', 'SERVER_GREP']); expect(wrappedTools[1].description).toContain(DIRECT_CUSTOM_TOOL_DESCRIPTION_PREFIX); expect(wrappedTools[1].toolkit).toEqual({ slug: 'custom', name: 'Custom' }); expect(tools).toBe('mocked-custom-tools'); const routingExecute = mockProvider.wrapTools.mock.calls[0][1] as ( toolSlug: string, input: Record ) => Promise; const result = await routingExecute('SERVER_GREP', { pattern: 'needle' }); expect(result).toEqual({ data: { matches: ['needle'] }, error: null, successful: true, }); expect(executeGrep).toHaveBeenCalledWith({ pattern: 'needle' }, expect.anything()); expect(mockClient.toolRouter.session.execute).not.toHaveBeenCalled(); }); it('should pass inline custom tools when routing remote provider executions', async () => { const grepTool = createCustomTool('GREP', { name: 'Grep', description: 'Search local text', inputParams: z.object({ pattern: z.string() }), execute: vi.fn(async () => ({ matches: [] })), }); const executeSessionTool = vi.fn().mockResolvedValue({ data: { ok: true }, error: null, successful: true, }); (Tools as any).mockImplementation(function () { return { getRawToolRouterSessionTools: vi .fn() .mockResolvedValue([ { slug: 'COMPOSIO_SEARCH_TOOLS' }, { slug: 'GMAIL_SEND_EMAIL', toolkit: { slug: 'gmail', name: 'Gmail' } }, ]), executeSessionTool, }; }); mockClient.toolRouter.session.create.mockResolvedValueOnce({ ...mockSessionCreateResponse, experimental: { custom_tools: [ { slug: 'LOCAL_GREP', original_slug: 'GREP', extends_toolkit: null, }, ], }, }); mockProvider.wrapTools.mockReturnValue('mocked-custom-tools'); const session = await toolRouter.create(userId, { experimental: { customTools: [grepTool] }, }); await session.tools(); const routingExecute = mockProvider.wrapTools.mock.calls[0][1] as ( toolSlug: string, input: Record ) => Promise; await routingExecute('GMAIL_SEND_EMAIL', { to: 'user@example.com' }); expect(executeSessionTool).toHaveBeenCalledWith( 'GMAIL_SEND_EMAIL', { sessionId, arguments: { to: 'user@example.com' } }, undefined, { slug: 'GMAIL_SEND_EMAIL', toolkit: { slug: 'gmail', name: 'Gmail' } }, { experimental: { custom_tools: [expect.objectContaining({ slug: 'GREP' })], }, } ); }); it('should pass inline custom tools when routing remote multi-execute batches', async () => { const grepTool = createCustomTool('GREP', { name: 'Grep', description: 'Search local text', inputParams: z.object({ pattern: z.string() }), execute: vi.fn(async () => ({ matches: [] })), }); const executeSessionTool = vi.fn().mockResolvedValue({ data: { results: [] }, error: null, successful: true, }); const multiExecuteTool = { slug: 'COMPOSIO_MULTI_EXECUTE_TOOL' }; (Tools as any).mockImplementation(function () { return { getRawToolRouterSessionTools: vi .fn() .mockResolvedValue([{ slug: 'COMPOSIO_SEARCH_TOOLS' }, multiExecuteTool]), executeSessionTool, }; }); mockClient.toolRouter.session.create.mockResolvedValueOnce({ ...mockSessionCreateResponse, experimental: { custom_tools: [ { slug: 'LOCAL_GREP', original_slug: 'GREP', extends_toolkit: null, }, ], }, }); mockProvider.wrapTools.mockReturnValue('mocked-custom-tools'); const session = await toolRouter.create(userId, { experimental: { customTools: [grepTool] }, }); await session.tools(); const routingExecute = mockProvider.wrapTools.mock.calls[0][1] as ( toolSlug: string, input: Record ) => Promise; const input = { tools: [{ tool_slug: 'GMAIL_SEND_EMAIL', arguments: { to: 'a@b.com' } }] }; await routingExecute('COMPOSIO_MULTI_EXECUTE_TOOL', input); expect(executeSessionTool).toHaveBeenCalledWith( 'COMPOSIO_MULTI_EXECUTE_TOOL', { sessionId, arguments: input }, undefined, multiExecuteTool, { experimental: { custom_tools: [expect.objectContaining({ slug: 'GREP' })], }, } ); }); it('should not expose custom tools unless SDK preload selects them', async () => { const grepTool = createCustomTool('GREP', { name: 'Grep', description: 'Search local text', inputParams: z.object({ pattern: z.string(), }), execute: vi.fn(async () => ({ matches: [] })), }); mockClient.toolRouter.session.create.mockResolvedValueOnce({ ...mockSessionCreateResponse, tool_router_tools: ['COMPOSIO_SEARCH_TOOLS'], experimental: { custom_tools: [ { slug: 'LOCAL_GREP', original_slug: 'GREP', extends_toolkit: null, }, ], }, }); mockProvider.wrapTools.mockReturnValue('mocked-custom-tools'); const session = await toolRouter.create(userId, { experimental: { customTools: [grepTool] }, }); await session.tools(); const wrappedTools = mockProvider.wrapTools.mock.calls[0][0] as Array<{ slug: string }>; expect(wrappedTools.map(tool => tool.slug)).toEqual(['COMPOSIO_SEARCH_TOOLS']); }); it('should expose custom tools by default for preload all', async () => { const grepTool = createCustomTool('GREP', { name: 'Grep', description: 'Search local text', inputParams: z.object({ pattern: z.string(), }), execute: vi.fn(async () => ({ matches: [] })), }); mockClient.toolRouter.session.create.mockResolvedValueOnce({ ...mockSessionCreateResponse, tool_router_tools: ['COMPOSIO_SEARCH_TOOLS'], experimental: { custom_tools: [ { slug: 'LOCAL_GREP', original_slug: 'GREP', extends_toolkit: null, }, ], }, }); mockProvider.wrapTools.mockReturnValue('mocked-custom-tools'); const session = await toolRouter.create(userId, { preload: { tools: 'all' }, experimental: { customTools: [grepTool] }, }); await session.tools(); const wrappedTools = mockProvider.wrapTools.mock.calls[0][0] as Array<{ slug: string }>; expect(wrappedTools.map(tool => tool.slug)).toEqual(['COMPOSIO_SEARCH_TOOLS', 'LOCAL_GREP']); }); it('should handle tools fetching errors', async () => { mockClient.toolRouter.session.create.mockResolvedValueOnce(mockSessionCreateResponse); (Tools as any).mockImplementation(function () { return { getRawComposioTools: vi.fn().mockRejectedValue(new Error('Failed to fetch tools')), getRawToolRouterSessionTools: vi .fn() .mockRejectedValue(new Error('Failed to fetch tools')), wrapToolsForToolRouter: vi.fn().mockReturnValue('mocked-wrapped-tools'), }; }); const session = await toolRouter.create(userId); await expect(session.tools()).rejects.toThrow('Failed to fetch tools'); }); it('should call tools function multiple times with different modifiers', async () => { mockClient.toolRouter.session.create.mockResolvedValueOnce(mockSessionCreateResponse); const modifier1 = { modifySchema: vi.fn() }; const modifier2 = { modifySchema: vi.fn() }; const session = await toolRouter.create(userId); // Each call to session.tools() creates a new Tools instance // So we need to check that Tools was called twice and each instance's methods were called await session.tools(modifier1); await session.tools(modifier2); expect(Tools).toHaveBeenCalledTimes(2); const firstToolsInstance = (Tools as any).mock.results[0].value; expect(firstToolsInstance.getRawToolRouterSessionTools).toHaveBeenCalledWith( sessionId, { modifySchema: modifier1.modifySchema }, undefined ); expect(firstToolsInstance.wrapToolsForToolRouter).toHaveBeenCalledWith( sessionId, [{ slug: 'COMPOSIO_SEARCH_TOOLS' }], modifier1 ); const secondToolsInstance = (Tools as any).mock.results[1].value; expect(secondToolsInstance.getRawToolRouterSessionTools).toHaveBeenCalledWith( sessionId, { modifySchema: modifier2.modifySchema }, undefined ); expect(secondToolsInstance.wrapToolsForToolRouter).toHaveBeenCalledWith( sessionId, [{ slug: 'COMPOSIO_SEARCH_TOOLS' }], modifier2 ); }); it('should use tool slugs from session response', async () => { const customResponse = { ...mockSessionCreateResponse, session_id: 'custom_session_123', tool_router_tools: ['CUSTOM_TOOL_1', 'CUSTOM_TOOL_2'], }; mockClient.toolRouter.session.create.mockResolvedValueOnce(customResponse); const newSession = await toolRouter.create(userId); await newSession.tools(); expect(Tools).toHaveBeenCalledWith(mockClient, { provider: mockProvider, apiKey: 'test-api-key', }); const toolsInstance = (Tools as any).mock.results[0].value; expect(toolsInstance.getRawToolRouterSessionTools).toHaveBeenCalledWith( 'custom_session_123', undefined, undefined ); expect(toolsInstance.wrapToolsForToolRouter).toHaveBeenCalledWith( 'custom_session_123', [{ slug: 'COMPOSIO_SEARCH_TOOLS' }], undefined ); }); it('should handle empty tool router tools array', async () => { const emptyToolsResponse = { ...mockSessionCreateResponse, session_id: 'empty_session_123', tool_router_tools: [], }; mockClient.toolRouter.session.create.mockResolvedValueOnce(emptyToolsResponse); const session = await toolRouter.create(userId); await session.tools(); expect(Tools).toHaveBeenCalledWith(mockClient, { provider: mockProvider, apiKey: 'test-api-key', }); const toolsInstance = (Tools as any).mock.results[0].value; expect(toolsInstance.getRawToolRouterSessionTools).toHaveBeenCalledWith( 'empty_session_123', undefined, undefined ); expect(toolsInstance.wrapToolsForToolRouter).toHaveBeenCalledWith( 'empty_session_123', [{ slug: 'COMPOSIO_SEARCH_TOOLS' }], undefined ); }); }); describe('integration tests', () => { const userId = 'user_123'; it('should create a complete session and use all functions', async () => { mockClient.toolRouter.session.create.mockResolvedValueOnce(mockSessionCreateResponse); mockClient.toolRouter.session.link.mockResolvedValueOnce(mockLinkResponse); mockClient.toolRouter.session.toolkits.mockResolvedValueOnce(mockToolkitsResponse); // Create session const session = await toolRouter.create(userId, { toolkits: ['gmail', 'slack'], manageConnections: true, }); expect(session.sessionId).toBe('session_123'); expect(session.mcp.url).toBe('https://mcp.example.com/session_123'); // Use authorize function const connection = await session.authorize('gmail'); expect(connection.id).toBe('conn_456'); // Use toolkits function const toolkits = await session.toolkits(); expect(toolkits.items).toHaveLength(3); // Use tools function const tools = await session.tools(); expect(tools).toBe('mocked-wrapped-tools'); // Verify all API calls were made expect(mockClient.toolRouter.session.create).toHaveBeenCalledTimes(1); expect(mockClient.toolRouter.session.link).toHaveBeenCalledTimes(1); expect(mockClient.toolRouter.session.toolkits).toHaveBeenCalledTimes(1); }); it('should handle multiple sessions independently', async () => { const session1Response = { ...mockSessionCreateResponse, session_id: 'session_1', }; const session2Response = { ...mockSessionCreateResponse, session_id: 'session_2', }; mockClient.toolRouter.session.create .mockResolvedValueOnce(session1Response) .mockResolvedValueOnce(session2Response); mockClient.toolRouter.session.toolkits.mockResolvedValue(mockToolkitsResponse); const session1 = await toolRouter.create('user_1'); const session2 = await toolRouter.create('user_2'); expect(session1.sessionId).toBe('session_1'); expect(session2.sessionId).toBe('session_2'); await session1.toolkits(); await session2.toolkits(); expect(mockClient.toolRouter.session.toolkits).toHaveBeenCalledWith( 'session_1', { cursor: undefined, limit: undefined, toolkits: undefined, is_connected: undefined, }, undefined ); expect(mockClient.toolRouter.session.toolkits).toHaveBeenCalledWith( 'session_2', { cursor: undefined, limit: undefined, toolkits: undefined, is_connected: undefined, }, undefined ); }); it('should handle rapid successive calls', async () => { mockClient.toolRouter.session.create.mockResolvedValue(mockSessionCreateResponse); mockClient.toolRouter.session.toolkits.mockResolvedValue(mockToolkitsResponse); const session = await toolRouter.create(userId); // Make multiple rapid calls const promises = [session.toolkits(), session.toolkits(), session.toolkits()]; const results = await Promise.all(promises); expect(results).toHaveLength(3); results.forEach(result => { expect(result.items).toHaveLength(3); }); expect(mockClient.toolRouter.session.toolkits).toHaveBeenCalledTimes(3); }); }); describe('delete method', () => { it('should delete a session by ID and return a camelCase response', async () => { mockClient.delete.mockResolvedValueOnce({ session_id: 'trs_delete_123', deleted: true, }); const result = await toolRouter.delete('trs_delete_123'); expect(mockClient.delete).toHaveBeenCalledWith( '/api/v3.1/tool_router/session/trs_delete_123', undefined ); expect(result).toEqual({ sessionId: 'trs_delete_123', deleted: true, }); }); it('should forward request options when deleting by ID', async () => { const requestOptions = { signal: new AbortController().signal }; mockClient.delete.mockResolvedValueOnce({ session_id: 'trs_delete_123', deleted: true, }); await toolRouter.delete('trs_delete_123', requestOptions); expect(mockClient.delete).toHaveBeenCalledWith( '/api/v3.1/tool_router/session/trs_delete_123', requestOptions ); }); }); describe('use method', () => { const sessionId = 'session_123'; it('should retrieve an existing session by ID', async () => { mockClient.toolRouter.session.retrieve.mockResolvedValueOnce(mockSessionRetrieveResponse); const session = await toolRouter.use(sessionId); expect(mockClient.toolRouter.session.retrieve).toHaveBeenCalledWith(sessionId, undefined); expect(mockClient.toolRouter.session.attach).not.toHaveBeenCalled(); expect(mockClient.post).not.toHaveBeenCalled(); expect(session).toHaveProperty('sessionId', 'session_123'); expect(session).toHaveProperty('mcp'); expect(session.mcp).toEqual({ type: 'http', url: 'https://mcp.example.com/session_123', headers: { 'x-api-key': 'test-api-key', }, }); expect(session).toHaveProperty('tools'); expect(session).toHaveProperty('authorize'); expect(session).toHaveProperty('toolkits'); expect(session).toHaveProperty('delete'); expect(session.preload.tools).toEqual(['GMAIL_FETCH_EMAILS']); expect(session.configVersion).toBe(7); }); it('should attach custom tools when provided', async () => { const grepTool = createCustomTool('GREP', { name: 'Grep', description: 'Search local text', inputParams: z.object({ pattern: z.string() }), execute: vi.fn(async () => ({ matches: [] })), }); mockClient.toolRouter.session.attach.mockResolvedValueOnce({ ...mockSessionRetrieveResponse, experimental: { custom_tools: [ { slug: 'SERVER_GREP', original_slug: 'GREP', extends_toolkit: null, }, ], }, }); const session = await toolRouter.use(sessionId, { customTools: [grepTool] }); expect(mockClient.toolRouter.session.attach).toHaveBeenCalledWith( sessionId, { experimental: { custom_tools: [expect.objectContaining({ slug: 'GREP' })], }, }, undefined ); expect(mockClient.toolRouter.session.attach.mock.calls[0][1].experimental).not.toHaveProperty( 'custom_toolkits' ); expect(mockClient.post).not.toHaveBeenCalled(); expect(mockClient.toolRouter.session.retrieve).not.toHaveBeenCalled(); expect(session.customTools()).toEqual([ expect.objectContaining({ slug: 'SERVER_GREP', name: 'Grep', }), ]); }); it('should return a session with correct session ID', async () => { const customResponse = { ...mockSessionRetrieveResponse, config: { ...mockSessionRetrieveResponse.config, user_id: 'custom_user_456', }, }; mockClient.toolRouter.session.retrieve.mockResolvedValueOnce(customResponse); const session = await toolRouter.use(sessionId); expect(session.sessionId).toBe('session_123'); // The tools function should be created with the session ID expect(session.tools).toBeDefined(); }); it('should return a session with working tools function', async () => { mockClient.toolRouter.session.retrieve.mockResolvedValueOnce(mockSessionRetrieveResponse); const session = await toolRouter.use(sessionId); const tools = await session.tools(); expect(tools).toBe('mocked-wrapped-tools'); expect(Tools).toHaveBeenCalled(); const toolsInstance = (Tools as any).mock.results[0].value; expect(toolsInstance.getRawToolRouterSessionTools).toHaveBeenCalledWith( sessionId, undefined, undefined ); expect(toolsInstance.wrapToolsForToolRouter).toHaveBeenCalledWith( sessionId, [{ slug: 'COMPOSIO_SEARCH_TOOLS' }], undefined ); }); it('should return a session with working authorize function', async () => { mockClient.toolRouter.session.retrieve.mockResolvedValueOnce(mockSessionRetrieveResponse); mockClient.toolRouter.session.link.mockResolvedValueOnce(mockLinkResponse); const session = await toolRouter.use(sessionId); const connectionRequest = await session.authorize('github'); expect(mockClient.toolRouter.session.link).toHaveBeenCalledWith( sessionId, { toolkit: 'github', }, undefined ); expect(connectionRequest).toHaveProperty('redirectUrl', 'https://composio.dev/auth/redirect'); expect(connectionRequest).toHaveProperty('status', ConnectedAccountStatuses.INITIATED); }); it('should return a session with working toolkits function', async () => { mockClient.toolRouter.session.retrieve.mockResolvedValueOnce(mockSessionRetrieveResponse); mockClient.toolRouter.session.toolkits.mockResolvedValueOnce(mockToolkitsResponse); const session = await toolRouter.use(sessionId); const toolkitsResult = await session.toolkits(); expect(mockClient.toolRouter.session.toolkits).toHaveBeenCalledWith( sessionId, { cursor: undefined, limit: undefined, toolkits: undefined, is_connected: undefined, }, undefined ); expect(toolkitsResult.items).toHaveLength(3); expect(toolkitsResult.items[0].slug).toBe('gmail'); }); it('should return a session with working delete function', async () => { mockClient.toolRouter.session.retrieve.mockResolvedValueOnce(mockSessionRetrieveResponse); mockClient.delete.mockResolvedValueOnce({ session_id: sessionId, deleted: true, }); const session = await toolRouter.use(sessionId); const result = await session.delete(); expect(mockClient.delete).toHaveBeenCalledWith( '/api/v3.1/tool_router/session/session_123', undefined ); expect(result).toEqual({ sessionId, deleted: true, }); }); it('should attach custom tools and expose SDK-preloaded custom schemas', async () => { const grepTool = createCustomTool('GREP', { name: 'Grep', description: 'Search local text', preload: true, inputParams: z.object({ pattern: z.string() }), execute: vi.fn(async () => ({ matches: [] })), }); mockClient.toolRouter.session.attach.mockResolvedValueOnce({ ...mockSessionRetrieveResponse, tool_router_tools: ['COMPOSIO_SEARCH_TOOLS'], experimental: { custom_tools: [ { slug: 'LOCAL_GREP', original_slug: 'GREP', extends_toolkit: null, }, ], }, }); mockProvider.wrapTools.mockReturnValue('mocked-custom-tools'); const session = await toolRouter.use(sessionId, { customTools: [grepTool] }); await session.tools(); expect(mockClient.toolRouter.session.attach).toHaveBeenCalledWith( sessionId, { experimental: { custom_tools: [expect.objectContaining({ slug: 'GREP', preload: true })], }, }, undefined ); expect(mockClient.post).not.toHaveBeenCalled(); expect(mockClient.toolRouter.session.retrieve).not.toHaveBeenCalled(); const wrappedTools = mockProvider.wrapTools.mock.calls[0][0] as Array<{ slug: string }>; expect(wrappedTools.map(tool => tool.slug)).toEqual(['COMPOSIO_SEARCH_TOOLS', 'LOCAL_GREP']); }); it('should apply preload all from an attached session to custom tools in use()', async () => { const grepTool = createCustomTool('GREP', { name: 'Grep', description: 'Search local text', inputParams: z.object({ pattern: z.string() }), execute: vi.fn(async () => ({ matches: [] })), }); mockClient.toolRouter.session.attach.mockResolvedValueOnce({ ...mockSessionRetrieveResponse, tool_router_tools: ['COMPOSIO_SEARCH_TOOLS'], config: { ...mockSessionRetrieveResponse.config, preload: { tools: 'all' }, }, experimental: { custom_tools: [ { slug: 'LOCAL_GREP', original_slug: 'GREP', extends_toolkit: null, }, ], }, }); mockProvider.wrapTools.mockReturnValue('mocked-custom-tools'); mockClient.toolRouter.session.search.mockResolvedValueOnce({ success: true, error: null, results: [], tool_schemas: {}, toolkit_connection_statuses: [], next_steps_guidance: [], session: { id: sessionId, generate_id: false, instructions: 'Reuse session', }, time_info: { current_time_utc: '2025-03-09T12:00:00.000Z', current_time_utc_epoch_seconds: 1741521600, message: 'UTC', }, }); const session = await toolRouter.use(sessionId, { customTools: [grepTool] }); await session.tools(); await session.search({ query: 'search local text' }); const wrappedTools = mockProvider.wrapTools.mock.calls[0][0] as Array<{ slug: string }>; expect(wrappedTools.map(tool => tool.slug)).toEqual(['COMPOSIO_SEARCH_TOOLS', 'LOCAL_GREP']); expect(mockClient.toolRouter.session.search).toHaveBeenCalledWith( sessionId, { queries: [{ use_case: 'search local text' }], experimental: { custom_tools: [expect.objectContaining({ slug: 'GREP', preload: true })], }, }, undefined ); }); it('should handle different session IDs', async () => { const session1Response = { ...mockSessionRetrieveResponse, session_id: 'session_1', }; const session2Response = { ...mockSessionRetrieveResponse, session_id: 'session_2', }; mockClient.toolRouter.session.retrieve .mockResolvedValueOnce(session1Response) .mockResolvedValueOnce(session2Response); const session1 = await toolRouter.use('session_1'); const session2 = await toolRouter.use('session_2'); expect(session1.sessionId).toBe('session_1'); expect(session2.sessionId).toBe('session_2'); expect(mockClient.toolRouter.session.retrieve).toHaveBeenCalledTimes(2); expect(mockClient.toolRouter.session.retrieve).toHaveBeenNthCalledWith( 1, 'session_1', undefined ); expect(mockClient.toolRouter.session.retrieve).toHaveBeenNthCalledWith( 2, 'session_2', undefined ); expect(mockClient.post).not.toHaveBeenCalled(); }); it('should handle MCP server type correctly', async () => { mockClient.toolRouter.session.retrieve.mockResolvedValueOnce(mockSessionRetrieveResponse); const session = await toolRouter.use(sessionId); expect(session.mcp.type).toBe('http'); expect(session.mcp.url).toBe('https://mcp.example.com/session_123'); }); it('should throw error if session retrieve fails', async () => { const error = new Error('Session not found'); mockClient.toolRouter.session.retrieve.mockRejectedValueOnce(error); await expect(toolRouter.use(sessionId)).rejects.toThrow('Session not found'); expect(mockClient.toolRouter.session.retrieve).toHaveBeenCalledWith(sessionId, undefined); expect(mockClient.post).not.toHaveBeenCalled(); }); it('should handle retrieve with different tool lists', async () => { const customResponse = { ...mockSessionRetrieveResponse, tool_router_tools: ['CUSTOM_TOOL_1', 'CUSTOM_TOOL_2'], }; mockClient.toolRouter.session.retrieve.mockResolvedValueOnce(customResponse); const session = await toolRouter.use(sessionId); expect(session).toBeDefined(); expect(session.sessionId).toBe('session_123'); }); it('should be independent from create method', async () => { mockClient.toolRouter.session.create.mockResolvedValueOnce(mockSessionCreateResponse); mockClient.toolRouter.session.retrieve.mockResolvedValueOnce(mockSessionRetrieveResponse); // Create a new session const createdSession = await toolRouter.create('user_123'); expect(createdSession.sessionId).toBe('session_123'); // Use an existing session const retrievedSession = await toolRouter.use('session_123'); expect(retrievedSession.sessionId).toBe('session_123'); // Both should have been called once expect(mockClient.toolRouter.session.create).toHaveBeenCalledTimes(1); expect(mockClient.toolRouter.session.retrieve).toHaveBeenCalledTimes(1); expect(mockClient.post).not.toHaveBeenCalled(); }); }); });