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

3645 lines
118 KiB
TypeScript

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<unknown, unknown, MockProvider>;
let mockClient: ReturnType<typeof createMockClient>;
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<string, unknown>
) => Promise<unknown>;
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<string, unknown>
) => Promise<unknown>;
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<string, unknown>
) => Promise<unknown>;
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();
});
});
});