/**
* Integration Tests: Tool Execution Flow
*
* Tests tool discovery, execution, and response handling through MCP protocol.
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { setupTestMCPServer, type TestMCPServer } from '../helpers/test-server.js';
import { loadFixture } from '../helpers/fixtures.js';
describe('Tool Execution Flow', () => {
let testServer: TestMCPServer;
beforeEach(async () => {
testServer = await setupTestMCPServer();
});
afterEach(async () => {
await testServer.close();
vi.restoreAllMocks();
});
describe('Tool discovery', () => {
it('should list all 14 registered tools', async () => {
const tools = await testServer.listTools();
expect(tools).toHaveLength(14);
});
it('should include search_companies tool', async () => {
const tools = await testServer.listTools();
const searchCompanies = tools.find((t) => t.name === 'search_companies');
expect(searchCompanies).toBeDefined();
expect(searchCompanies?.description).toContain('Search');
expect(searchCompanies?.inputSchema).toBeDefined();
});
it('should include get_workspace_schema tool', async () => {
const tools = await testServer.listTools();
const getSchema = tools.find((t) => t.name === 'get_workspace_schema');
expect(getSchema).toBeDefined();
expect(getSchema?.inputSchema).toHaveProperty('properties');
});
it('should include all CRUD tools for companies', async () => {
const tools = await testServer.listTools();
const toolNames = tools.map((t) => t.name);
expect(toolNames).toContain('search_companies');
expect(toolNames).toContain('get_company');
expect(toolNames).toContain('create_company');
expect(toolNames).toContain('update_company');
expect(toolNames).toContain('manage_company_domains');
});
it('should include all CRUD tools for people', async () => {
const tools = await testServer.listTools();
const toolNames = tools.map((t) => t.name);
expect(toolNames).toContain('search_people');
expect(toolNames).toContain('get_person');
expect(toolNames).toContain('create_person');
expect(toolNames).toContain('update_person');
expect(toolNames).toContain('manage_person_emails');
});
});
describe('Tool execution', () => {
it('should execute search_companies through MCP', async () => {
const companyData = loadFixture('companies.json', 'acme-corp');
testServer.mockClient.mockResponse(
'POST',
'/objects/companies/records/query',
{
data: {
data: [companyData],
next_page: null,
},
}
);
const result = await testServer.callTool('search_companies', {
query: 'Acme',
});
expect(result.content).toHaveLength(1);
expect(result.content[0].type).toBe('text');
const parsed = JSON.parse(result.content[0].text as string);
expect(parsed.success).toBe(true);
expect(parsed.data.companies).toHaveLength(1);
});
it('should execute get_company through MCP', async () => {
const companyData = loadFixture('companies.json', 'acme-corp');
testServer.mockClient.mockResponse(
'GET',
'/objects/companies/records/company-acme-123',
{
data: { data: companyData },
}
);
const result = await testServer.callTool('get_company', {
record_id: 'company-acme-123',
});
expect(result.content).toHaveLength(1);
const parsed = JSON.parse(result.content[0].text as string);
expect(parsed.success).toBe(true);
expect(parsed.data.record_id).toBe('company-acme-123');
});
it('should execute create_company through MCP', async () => {
const companyData = loadFixture('companies.json', 'acme-corp');
testServer.mockClient.mockResponse('POST', '/objects/companies/records', {
data: { data: companyData },
});
const result = await testServer.callTool('create_company', {
name: 'Acme Corporation',
domains: ['acme.com'],
});
expect(result.content).toHaveLength(1);
const parsed = JSON.parse(result.content[0].text as string);
expect(parsed.success).toBe(true);
expect(parsed.data.name).toBe('Acme Corporation');
});
});
describe('Response formatting', () => {
it('should return CallToolResult with content array', async () => {
testServer.mockClient.mockResponse(
'POST',
'/objects/companies/records/query',
{
data: { data: [], next_page: null },
}
);
const result = await testServer.callTool('search_companies', {
query: 'test',
});
expect(result).toHaveProperty('content');
expect(Array.isArray(result.content)).toBe(true);
expect(result.content[0]).toHaveProperty('type');
expect(result.content[0]).toHaveProperty('text');
});
it('should format success responses with success:true', async () => {
const companyData = loadFixture('companies.json', 'acme-corp');
testServer.mockClient.mockResponse('GET', '/objects/companies/records/test', {
data: { data: companyData },
});
const result = await testServer.callTool('get_company', {
record_id: 'test',
});
const parsed = JSON.parse(result.content[0].text as string);
expect(parsed.success).toBe(true);
expect(parsed.data).toBeDefined();
});
it('should format error responses with success:false', async () => {
const errorData = loadFixture('api-errors.json', 'not-found');
testServer.mockClient.mockResponse(
'GET',
'/objects/companies/records/nonexistent',
{
error: {
statusCode: errorData.error.status,
message: errorData.error.message,
response: { message: errorData.error.message },
},
}
);
const result = await testServer.callTool('get_company', {
record_id: 'nonexistent',
});
const parsed = JSON.parse(result.content[0].text as string);
expect(parsed.success).toBe(false);
expect(parsed.error).toBeDefined();
expect(parsed.error.type).toBe('not_found_error');
});
});
describe('Argument validation', () => {
it('should validate required arguments', async () => {
const result = await testServer.callTool('get_company', {
record_id: '', // Invalid empty string
});
const parsed = JSON.parse(result.content[0].text as string);
expect(parsed.success).toBe(false);
expect(parsed.error.type).toBe('validation_error');
expect(parsed.error.field).toBe('record_id');
});
it('should accept empty arguments for search tools (lists all records)', async () => {
testServer.mockClient.mockResponse(
'POST',
'/objects/companies/records/query',
{
data: { data: [], next_page: null },
}
);
const result = await testServer.callTool('search_companies', {});
const parsed = JSON.parse(result.content[0].text as string);
expect(parsed.success).toBe(true);
expect(parsed.data.filters.query).toBeNull();
});
it('should accept valid arguments', async () => {
testServer.mockClient.mockResponse(
'POST',
'/objects/companies/records/query',
{
data: { data: [], next_page: null },
}
);
const result = await testServer.callTool('search_companies', {
query: 'Acme',
limit: 10,
});
const parsed = JSON.parse(result.content[0].text as string);
expect(parsed.success).toBe(true);
});
});
describe('Error propagation', () => {
it('should propagate API errors through MCP', async () => {
const authError = loadFixture('api-errors.json', 'authentication');
testServer.mockClient.mockResponse(
'POST',
'/objects/companies/records/query',
{
error: {
statusCode: authError.error.status,
message: authError.error.message,
},
}
);
const result = await testServer.callTool('search_companies', {
query: 'test',
});
const parsed = JSON.parse(result.content[0].text as string);
expect(parsed.success).toBe(false);
expect(parsed.error.type).toBe('authentication_error');
});
it('should handle network errors gracefully', async () => {
testServer.mockClient.mockResponse(
'POST',
'/objects/companies/records/query',
{
error: new Error('Network error'),
}
);
const result = await testServer.callTool('search_companies', {
query: 'test',
});
const parsed = JSON.parse(result.content[0].text as string);
expect(parsed.success).toBe(false);
expect(parsed.error).toBeDefined();
});
});
describe('Record exploration features', () => {
it('should fetch person with notes using include_notes parameter', async () => {
const personData = loadFixture('people.json', 'john-doe');
testServer.mockClient.mockResponse(
'GET',
'/objects/people/records/person-john-123',
{
data: { data: personData },
}
);
testServer.mockClient.mockResponse(
'GET',
'/notes?parent_object=people&parent_record_id=person-john-123',
{
data: {
data: [
{
id: { note_id: 'note-1' },
title: 'Meeting Notes',
content_plaintext: 'Great discussion',
content_markdown: '# Meeting Notes',
created_at: '2024-11-15T10:00:00Z',
created_by_actor: { type: 'user', id: 'user-1' },
},
],
},
}
);
const result = await testServer.callTool('get_person', {
record_id: 'person-john-123',
include_notes: true,
});
const parsed = JSON.parse(result.content[0].text as string);
expect(parsed.success).toBe(true);
expect(parsed.data.notes).toHaveLength(1);
expect(parsed.data.notes[0].id).toBe('note-1');
expect(parsed.data.notes[0].title).toBe('Meeting Notes');
});
it('should fetch company with all related data using include_all parameter', async () => {
const companyData = loadFixture('companies.json', 'acme-corp');
testServer.mockClient.mockResponse(
'GET',
'/objects/companies/records/company-acme-123',
{
data: { data: companyData },
}
);
testServer.mockClient.mockResponse(
'GET',
'/notes?parent_object=companies&parent_record_id=company-acme-123',
{
data: { data: [{ id: { note_id: 'note-1' }, title: 'Note', content_plaintext: '', content_markdown: '', created_at: '2024-11-15T10:00:00Z', created_by_actor: {} }] },
}
);
testServer.mockClient.mockResponse(
'GET',
'/tasks?linked_object=companies&linked_record_id=company-acme-123',
{
data: { data: [{ id: { task_id: 'task-1' }, content_plaintext: 'Task', deadline_at: null, is_completed: false, assignees: [], linked_records: [], created_at: '2024-11-15T10:00:00Z' }] },
}
);
testServer.mockClient.mockResponse(
'GET',
'/threads?record_id=company-acme-123&object=companies',
{
data: { data: [{ id: { thread_id: 'thread-1' }, comments: [], created_at: '2024-11-15T10:00:00Z' }] },
}
);
testServer.mockClient.mockResponse(
'GET',
'/meetings?linked_object=companies&linked_record_id=company-acme-123',
{
data: { data: [{ id: { meeting_id: 'meeting-1' }, title: 'Meeting', description: '', start: {}, end: {}, participants: [], linked_records: [], created_at: '2024-11-15T10:00:00Z' }] },
}
);
testServer.mockClient.mockResponse(
'GET',
'/objects/companies/records/company-acme-123/entries',
{
data: { data: [{ id: { list_id: 'list-1', entry_id: 'entry-1' }, created_at: '2024-11-15T10:00:00Z', entry_values: {} }] },
}
);
const result = await testServer.callTool('get_company', {
record_id: 'company-acme-123',
include_all: true,
});
const parsed = JSON.parse(result.content[0].text as string);
expect(parsed.success).toBe(true);
expect(parsed.data.notes).toHaveLength(1);
expect(parsed.data.tasks).toHaveLength(1);
expect(parsed.data.comment_threads).toHaveLength(1);
expect(parsed.data.meetings).toHaveLength(1);
expect(parsed.data.list_entries).toHaveLength(1);
});
it('should maintain backward compatibility when no include parameters provided', async () => {
const personData = loadFixture('people.json', 'john-doe');
testServer.mockClient.mockResponse(
'GET',
'/objects/people/records/person-john-123',
{
data: { data: personData },
}
);
const result = await testServer.callTool('get_person', {
record_id: 'person-john-123',
});
const parsed = JSON.parse(result.content[0].text as string);
expect(parsed.success).toBe(true);
expect(parsed.data.record_id).toBe('person-john-123');
expect(parsed.data.notes).toBeUndefined();
expect(parsed.data.tasks).toBeUndefined();
});
});
});