import nock from 'nock';
import { ToolHandler } from '../tools/index.js';
import type { CallToolRequest } from '@modelcontextprotocol/sdk/types.js';
describe('ToolHandler', () => {
let toolHandler: ToolHandler;
const baseURL = 'https://api.helpscout.net/v2';
beforeEach(() => {
// Mock environment for tests
process.env.HELPSCOUT_CLIENT_ID = 'test-client-id';
process.env.HELPSCOUT_CLIENT_SECRET = 'test-client-secret';
process.env.HELPSCOUT_BASE_URL = `${baseURL}/`;
nock.cleanAll();
// Mock OAuth2 authentication endpoint
nock(baseURL)
.persist()
.post('/oauth2/token')
.reply(200, {
access_token: 'mock-access-token',
token_type: 'Bearer',
expires_in: 3600,
});
toolHandler = new ToolHandler();
});
afterEach(async () => {
nock.cleanAll();
// Clean up any pending promises or timers
await new Promise(resolve => setImmediate(resolve));
});
describe('listTools', () => {
it('should return all available tools', async () => {
const tools = await toolHandler.listTools();
expect(tools).toHaveLength(8);
expect(tools.map(t => t.name)).toEqual([
'searchInboxes',
'searchConversations',
'getConversationSummary',
'getThreads',
'getServerTime',
'listAllInboxes',
'advancedConversationSearch',
'comprehensiveConversationSearch'
]);
});
it('should have proper tool schemas', async () => {
const tools = await toolHandler.listTools();
tools.forEach(tool => {
expect(tool).toHaveProperty('name');
expect(tool).toHaveProperty('description');
expect(tool).toHaveProperty('inputSchema');
expect(tool.inputSchema).toHaveProperty('type', 'object');
expect(tool.inputSchema).toHaveProperty('properties');
});
});
});
describe('getServerTime', () => {
it('should return server time without Help Scout API call', async () => {
const request: CallToolRequest = {
method: 'tools/call',
params: {
name: 'getServerTime',
arguments: {}
}
};
const result = await toolHandler.callTool(request);
expect(result.content).toHaveLength(1);
expect(result.content[0]).toHaveProperty('type', 'text');
const textContent = result.content[0] as { type: 'text'; text: string };
const response = JSON.parse(textContent.text);
expect(response).toHaveProperty('isoTime');
expect(response).toHaveProperty('unixTime');
expect(typeof response.isoTime).toBe('string');
expect(typeof response.unixTime).toBe('number');
});
});
describe('listAllInboxes', () => {
it('should list all inboxes with helpful guidance', async () => {
const mockResponse = {
_embedded: {
mailboxes: [
{ id: 1, name: 'Support Inbox', email: 'support@example.com', createdAt: '2023-01-01T00:00:00Z', updatedAt: '2023-01-02T00:00:00Z' },
{ id: 2, name: 'Sales Inbox', email: 'sales@example.com', createdAt: '2023-01-01T00:00:00Z', updatedAt: '2023-01-02T00:00:00Z' }
]
},
page: { size: 100, totalElements: 2 }
};
nock(baseURL)
.get('/mailboxes')
.query({ page: 1, size: 100 })
.reply(200, mockResponse);
const request: CallToolRequest = {
method: 'tools/call',
params: {
name: 'listAllInboxes',
arguments: {}
}
};
const result = await toolHandler.callTool(request);
expect(result.content).toHaveLength(1);
const textContent = result.content[0] as { type: 'text'; text: string };
// Handle error responses
if (result.isError) {
expect(textContent.text).toContain('Error');
return;
}
const response = JSON.parse(textContent.text);
expect(response.inboxes).toHaveLength(2);
expect(response.inboxes[0]).toHaveProperty('id', 1);
expect(response.inboxes[0]).toHaveProperty('name', 'Support Inbox');
expect(response.usage).toContain('Use the "id" field');
expect(response.nextSteps).toBeDefined();
expect(response.totalInboxes).toBe(2);
});
});
describe('searchInboxes', () => {
it('should search inboxes by name', async () => {
const mockResponse = {
_embedded: {
mailboxes: [
{ id: 1, name: 'Support Inbox', email: 'support@example.com' },
{ id: 2, name: 'Sales Inbox', email: 'sales@example.com' }
]
},
page: { size: 50, totalElements: 2 }
};
nock(baseURL)
.get('/mailboxes')
.query({ page: 1, size: 50 })
.reply(200, mockResponse);
const request: CallToolRequest = {
method: 'tools/call',
params: {
name: 'searchInboxes',
arguments: { query: 'Support' }
}
};
const result = await toolHandler.callTool(request);
expect(result.content).toHaveLength(1);
const textContent = result.content[0] as { type: 'text'; text: string };
// Handle error responses
if (result.isError) {
expect(textContent.text).toContain('Error');
return;
}
const response = JSON.parse(textContent.text);
expect(response.results).toHaveLength(1);
expect(response.results[0].name).toBe('Support Inbox');
});
});
describe('error handling', () => {
it('should handle API errors gracefully', async () => {
nock(baseURL)
.get('/mailboxes')
.reply(401, { message: 'Unauthorized' });
const request: CallToolRequest = {
method: 'tools/call',
params: {
name: 'searchInboxes',
arguments: { query: 'test' }
}
};
const result = await toolHandler.callTool(request);
// The error might be handled gracefully, so check for either error or empty results
expect(result.content[0]).toHaveProperty('type', 'text');
const textContent = result.content[0] as { type: 'text'; text: string };
const response = JSON.parse(textContent.text);
// Should either be an error or empty results
expect(response.results || response.totalFound === 0 || textContent.text.includes('Error')).toBeTruthy();
});
it('should handle unknown tool names', async () => {
const request: CallToolRequest = {
method: 'tools/call',
params: {
name: 'unknownTool',
arguments: {}
}
};
const result = await toolHandler.callTool(request);
expect(result.isError).toBe(true);
expect(result.content[0]).toHaveProperty('type', 'text');
const textContent = result.content[0] as { type: 'text'; text: string };
expect(textContent.text).toContain('Unknown tool');
});
});
describe('searchConversations', () => {
it('should search conversations with filters', async () => {
const mockResponse = {
_embedded: {
conversations: [
{
id: 1,
subject: 'Support Request',
status: 'active',
createdAt: '2023-01-01T00:00:00Z',
customer: { id: 1, firstName: 'John', lastName: 'Doe' }
}
]
},
page: { size: 50, totalElements: 1 },
_links: { next: null }
};
nock(baseURL)
.get('/conversations')
.query({
page: 1,
size: 50,
sortField: 'createdAt',
sortOrder: 'desc',
status: 'active'
})
.reply(200, mockResponse);
const request: CallToolRequest = {
method: 'tools/call',
params: {
name: 'searchConversations',
arguments: {
limit: 50,
status: 'active',
sort: 'createdAt',
order: 'desc'
}
}
};
const result = await toolHandler.callTool(request);
if (!result.isError) {
const textContent = result.content[0] as { type: 'text'; text: string };
const response = JSON.parse(textContent.text);
expect(response.results).toHaveLength(1);
expect(response.results[0].subject).toBe('Support Request');
}
});
});
describe('API Constraints Validation - Branch Coverage', () => {
it('should handle validation failures with required prerequisites', async () => {
// Set user context that mentions an inbox
toolHandler.setUserContext('search the support inbox for urgent tickets');
const request: CallToolRequest = {
method: 'tools/call',
params: {
name: 'searchConversations',
arguments: {
query: 'urgent',
// No inboxId provided despite mentioning "support inbox"
}
}
};
const result = await toolHandler.callTool(request);
expect(result.content[0]).toHaveProperty('type', 'text');
const textContent = result.content[0] as { type: 'text'; text: string };
const response = JSON.parse(textContent.text);
expect(response.error).toBe('API Constraint Validation Failed');
expect(response.details.requiredPrerequisites).toContain('searchInboxes');
});
it('should handle validation failures without prerequisites', async () => {
const request: CallToolRequest = {
method: 'tools/call',
params: {
name: 'getConversationSummary',
arguments: {
conversationId: 'invalid-format' // Should be numeric
}
}
};
const result = await toolHandler.callTool(request);
expect(result.content[0]).toHaveProperty('type', 'text');
const textContent = result.content[0] as { type: 'text'; text: string };
const response = JSON.parse(textContent.text);
expect(response.error).toBe('API Constraint Validation Failed');
expect(response.details.errors).toContain('Invalid conversation ID format');
});
it('should provide API guidance for successful tool calls', async () => {
const mockResponse = {
results: [
{ id: '123', name: 'Support', email: 'support@test.com' }
]
};
nock(baseURL)
.get('/mailboxes')
.query({ page: 1, size: 50 })
.reply(200, { _embedded: { mailboxes: mockResponse.results } });
const request: CallToolRequest = {
method: 'tools/call',
params: {
name: 'searchInboxes',
arguments: { query: 'support' }
}
};
const result = await toolHandler.callTool(request);
const textContent = result.content[0] as { type: 'text'; text: string };
const response = JSON.parse(textContent.text);
expect(response.apiGuidance).toBeDefined();
expect(response.apiGuidance[0]).toContain('NEXT STEP');
});
it('should handle tool calls without API guidance', async () => {
const request: CallToolRequest = {
method: 'tools/call',
params: {
name: 'getServerTime',
arguments: {}
}
};
const result = await toolHandler.callTool(request);
expect(result.content[0]).toHaveProperty('type', 'text');
const textContent = result.content[0] as { type: 'text'; text: string };
const response = JSON.parse(textContent.text);
expect(response.isoTime).toBeDefined();
// getServerTime doesn't generate API guidance
});
});
describe('Error Handling - Branch Coverage', () => {
it('should handle Zod validation errors in tool arguments', async () => {
const request: CallToolRequest = {
method: 'tools/call',
params: {
name: 'searchInboxes',
arguments: { limit: 'invalid' } // Should be number
}
};
const result = await toolHandler.callTool(request);
expect(result.isError).toBe(true);
expect(result.content[0]).toHaveProperty('type', 'text');
const textContent = result.content[0] as { type: 'text'; text: string };
const errorResponse = JSON.parse(textContent.text);
expect(errorResponse.error.code).toBe('INVALID_INPUT');
});
it('should handle missing required fields in tool arguments', async () => {
const request: CallToolRequest = {
method: 'tools/call',
params: {
name: 'getConversationSummary',
arguments: {} // Missing required conversationId
}
};
const result = await toolHandler.callTool(request);
expect(result.content[0]).toHaveProperty('type', 'text');
const textContent = result.content[0] as { type: 'text'; text: string };
const errorResponse = JSON.parse(textContent.text);
// Could be either validation error or API constraint validation error
expect(['INVALID_INPUT', 'API Constraint Validation Failed']).toContain(errorResponse.error || errorResponse.error?.code);
});
it('should handle unknown tool calls', async () => {
const request: CallToolRequest = {
method: 'tools/call',
params: {
name: 'unknownTool',
arguments: {}
}
};
const result = await toolHandler.callTool(request);
expect(result.isError).toBe(true);
expect(result.content[0]).toHaveProperty('type', 'text');
const textContent = result.content[0] as { type: 'text'; text: string };
const errorResponse = JSON.parse(textContent.text);
expect(errorResponse.error.code).toBe('TOOL_ERROR');
expect(errorResponse.error.message).toContain('Unknown tool');
});
it('should handle comprehensive search with no inbox ID when required', async () => {
toolHandler.setUserContext('search conversations in the support mailbox');
const request: CallToolRequest = {
method: 'tools/call',
params: {
name: 'comprehensiveConversationSearch',
arguments: {
searchTerms: ['urgent']
// Missing inboxId despite mentioning "support mailbox"
}
}
};
const result = await toolHandler.callTool(request);
expect(result.content[0]).toHaveProperty('type', 'text');
const textContent = result.content[0] as { type: 'text'; text: string };
const response = JSON.parse(textContent.text);
// Should trigger API constraint validation
expect(response.error || response.details?.requiredPrerequisites).toBeDefined();
});
});
describe('getConversationSummary', () => {
it('should handle conversations with no customer threads', async () => {
const mockConversation = {
id: 123,
subject: 'Test Conversation',
status: 'active',
createdAt: '2023-01-01T00:00:00Z',
updatedAt: '2023-01-01T00:00:00Z',
customer: { id: 1, firstName: 'John', lastName: 'Doe' },
assignee: null,
tags: []
};
const mockThreads = {
_embedded: {
threads: [
{
id: 1,
type: 'message', // Staff message only
body: 'Staff reply',
createdAt: '2023-01-01T10:00:00Z',
createdBy: { id: 1, firstName: 'Agent', lastName: 'Smith' }
}
]
}
};
nock(baseURL)
.get('/conversations/123')
.reply(200, mockConversation);
nock(baseURL)
.get('/conversations/123/threads')
.query({ page: 1, size: 50 })
.reply(200, mockThreads);
const request: CallToolRequest = {
method: 'tools/call',
params: {
name: 'getConversationSummary',
arguments: { conversationId: '123' }
}
};
const result = await toolHandler.callTool(request);
if (!result.isError) {
const textContent = result.content[0] as { type: 'text'; text: string };
const response = JSON.parse(textContent.text);
// Should handle null firstCustomerMessage
expect(response.firstCustomerMessage).toBeNull();
expect(response.latestStaffReply).toBeDefined();
}
});
it('should handle conversations with no staff replies', async () => {
const mockConversation = {
id: 124,
subject: 'Customer Only Conversation',
status: 'pending',
createdAt: '2023-01-01T00:00:00Z',
updatedAt: '2023-01-01T00:00:00Z',
customer: { id: 1, firstName: 'John', lastName: 'Doe' },
assignee: null,
tags: []
};
const mockThreads = {
_embedded: {
threads: [
{
id: 1,
type: 'customer', // Customer message only
body: 'Customer question',
createdAt: '2023-01-01T09:00:00Z',
customer: { id: 1, firstName: 'John', lastName: 'Doe' }
}
]
}
};
nock(baseURL)
.get('/conversations/124')
.reply(200, mockConversation);
nock(baseURL)
.get('/conversations/124/threads')
.query({ page: 1, size: 50 })
.reply(200, mockThreads);
const request: CallToolRequest = {
method: 'tools/call',
params: {
name: 'getConversationSummary',
arguments: { conversationId: '124' }
}
};
const result = await toolHandler.callTool(request);
if (!result.isError) {
const textContent = result.content[0] as { type: 'text'; text: string };
const response = JSON.parse(textContent.text);
// Should handle null latestStaffReply
expect(response.firstCustomerMessage).toBeDefined();
expect(response.latestStaffReply).toBeNull();
}
});
it('should get conversation summary with threads', async () => {
const mockConversation = {
id: 123,
subject: 'Test Conversation',
status: 'active',
createdAt: '2023-01-01T00:00:00Z',
updatedAt: '2023-01-02T00:00:00Z',
customer: { id: 1, firstName: 'John', lastName: 'Doe' },
assignee: { id: 2, firstName: 'Jane', lastName: 'Smith' },
tags: ['support', 'urgent']
};
const mockThreads = {
_embedded: {
threads: [
{
id: 1,
type: 'customer',
body: 'Original customer message',
createdAt: '2023-01-01T00:00:00Z',
customer: { id: 1, firstName: 'John' }
},
{
id: 2,
type: 'message',
body: 'Staff reply',
createdAt: '2023-01-01T12:00:00Z',
createdBy: { id: 2, firstName: 'Jane' }
}
]
}
};
nock(baseURL)
.get('/conversations/123')
.reply(200, mockConversation)
.get('/conversations/123/threads')
.query({ page: 1, size: 50 })
.reply(200, mockThreads);
const request: CallToolRequest = {
method: 'tools/call',
params: {
name: 'getConversationSummary',
arguments: { conversationId: "123" }
}
};
const result = await toolHandler.callTool(request);
if (!result.isError) {
const textContent = result.content[0] as { type: 'text'; text: string };
const summary = JSON.parse(textContent.text);
expect(summary.conversation.subject).toBe('Test Conversation');
expect(summary.firstCustomerMessage).toBeDefined();
expect(summary.latestStaffReply).toBeDefined();
}
});
});
describe('getThreads', () => {
it('should get conversation threads', async () => {
const mockResponse = {
_embedded: {
threads: [
{
id: 1,
type: 'customer',
body: 'Thread message',
createdAt: '2023-01-01T00:00:00Z'
}
]
}
};
nock(baseURL)
.get('/conversations/123/threads')
.query({ page: 1, size: 50 })
.reply(200, mockResponse);
const request: CallToolRequest = {
method: 'tools/call',
params: {
name: 'getThreads',
arguments: { conversationId: "123", limit: 50 }
}
};
const result = await toolHandler.callTool(request);
if (!result.isError) {
const textContent = result.content[0] as { type: 'text'; text: string };
const response = JSON.parse(textContent.text);
expect(response.conversationId).toBe("123");
expect(response.threads).toHaveLength(2);
}
});
});
describe('comprehensiveConversationSearch', () => {
it('should search across multiple statuses by default', async () => {
const freshToolHandler = new ToolHandler();
// Clean all previous mocks
nock.cleanAll();
// Re-add the auth mock
nock(baseURL)
.persist()
.post('/oauth2/token')
.reply(200, {
access_token: 'mock-access-token',
token_type: 'Bearer',
expires_in: 3600,
});
// Mock responses for each status
const mockActiveConversations = {
_embedded: {
conversations: [
{
id: 1,
subject: 'Active urgent issue',
status: 'active',
createdAt: '2024-01-01T00:00:00Z'
}
]
},
page: {
size: 25,
totalElements: 1,
totalPages: 1,
number: 0
}
};
const mockPendingConversations = {
_embedded: {
conversations: [
{
id: 2,
subject: 'Pending urgent request',
status: 'pending',
createdAt: '2024-01-02T00:00:00Z'
}
]
},
page: {
size: 25,
totalElements: 1,
totalPages: 1,
number: 0
}
};
const mockClosedConversations = {
_embedded: {
conversations: [
{
id: 3,
subject: 'Closed urgent case',
status: 'closed',
createdAt: '2024-01-03T00:00:00Z'
},
{
id: 4,
subject: 'Another closed urgent case',
status: 'closed',
createdAt: '2024-01-04T00:00:00Z'
}
]
},
page: {
size: 25,
totalElements: 2,
totalPages: 1,
number: 0
}
};
// Set up nock interceptors for each status
nock(baseURL)
.get('/conversations')
.query(params => params.status === 'active' && params.query === '(body:"urgent" OR subject:"urgent")')
.reply(200, mockActiveConversations);
nock(baseURL)
.get('/conversations')
.query(params => params.status === 'pending' && params.query === '(body:"urgent" OR subject:"urgent")')
.reply(200, mockPendingConversations);
nock(baseURL)
.get('/conversations')
.query(params => params.status === 'closed' && params.query === '(body:"urgent" OR subject:"urgent")')
.reply(200, mockClosedConversations);
const request: CallToolRequest = {
method: 'tools/call',
params: {
name: 'comprehensiveConversationSearch',
arguments: {
searchTerms: ['urgent'],
timeframeDays: 30
}
}
};
const result = await freshToolHandler.callTool(request);
expect(result.isError).toBeUndefined();
const textContent = result.content[0] as { type: 'text'; text: string };
const response = JSON.parse(textContent.text);
expect(response.totalConversationsFound).toBe(4);
expect(response.totalAvailableAcrossStatuses).toBe(4);
expect(response.resultsByStatus).toHaveLength(3);
expect(response.resultsByStatus[0].status).toBe('active');
expect(response.resultsByStatus[0].conversations).toHaveLength(1);
expect(response.resultsByStatus[1].status).toBe('pending');
expect(response.resultsByStatus[1].conversations).toHaveLength(1);
expect(response.resultsByStatus[2].status).toBe('closed');
expect(response.resultsByStatus[2].conversations).toHaveLength(2);
});
it('should handle custom status selection', async () => {
const freshToolHandler = new ToolHandler();
nock.cleanAll();
nock(baseURL)
.persist()
.post('/oauth2/token')
.reply(200, {
access_token: 'mock-access-token',
token_type: 'Bearer',
expires_in: 3600,
});
const mockActiveConversations = {
_embedded: {
conversations: [
{
id: 1,
subject: 'Active billing issue',
status: 'active',
createdAt: '2024-01-01T00:00:00Z'
}
]
},
page: {
size: 10,
totalElements: 1,
totalPages: 1,
number: 0
}
};
nock(baseURL)
.get('/conversations')
.query(params => params.status === 'active' && params.query === '(body:"billing" OR subject:"billing")')
.reply(200, mockActiveConversations);
const request: CallToolRequest = {
method: 'tools/call',
params: {
name: 'comprehensiveConversationSearch',
arguments: {
searchTerms: ['billing'],
statuses: ['active'],
limitPerStatus: 10
}
}
};
const result = await freshToolHandler.callTool(request);
expect(result.isError).toBeUndefined();
const textContent = result.content[0] as { type: 'text'; text: string };
const response = JSON.parse(textContent.text);
expect(response.totalConversationsFound).toBe(1);
expect(response.resultsByStatus).toHaveLength(1);
expect(response.resultsByStatus[0].status).toBe('active');
});
it('should handle invalid inboxId format validation', async () => {
toolHandler.setUserContext('search the support inbox');
const request: CallToolRequest = {
method: 'tools/call',
params: {
name: 'searchConversations',
arguments: {
query: 'test',
inboxId: 'invalid-format' // Should be numeric
}
}
};
const result = await toolHandler.callTool(request);
expect(result.content[0]).toHaveProperty('type', 'text');
const textContent = result.content[0] as { type: 'text'; text: string };
const response = JSON.parse(textContent.text);
expect(response.error).toBe('API Constraint Validation Failed');
expect(response.details.errors[0]).toContain('Invalid inbox ID format');
});
it('should handle different search locations in comprehensive search', async () => {
// Mock successful search
const mockConversations = {
_embedded: { conversations: [] },
page: { size: 25, totalElements: 0 }
};
nock(baseURL)
.get('/conversations')
.query(() => true)
.reply(200, mockConversations);
const request: CallToolRequest = {
method: 'tools/call',
params: {
name: 'comprehensiveConversationSearch',
arguments: {
searchTerms: ['test'],
searchIn: ['subject'], // Test subject-only search
statuses: ['active']
}
}
};
const result = await toolHandler.callTool(request);
if (!result.isError) {
const textContent = result.content[0] as { type: 'text'; text: string };
const response = JSON.parse(textContent.text);
expect(response.searchIn).toEqual(['subject']);
}
});
it('should handle search with no results and provide guidance', async () => {
const freshToolHandler = new ToolHandler();
nock.cleanAll();
nock(baseURL)
.persist()
.post('/oauth2/token')
.reply(200, {
access_token: 'mock-access-token',
token_type: 'Bearer',
expires_in: 3600,
});
const emptyResponse = {
_embedded: {
conversations: []
},
page: {
size: 25,
totalElements: 0,
totalPages: 0,
number: 0
}
};
// Mock empty responses for all statuses
nock(baseURL)
.get('/conversations')
.query(params => params.status === 'active')
.reply(200, emptyResponse);
nock(baseURL)
.get('/conversations')
.query(params => params.status === 'pending')
.reply(200, emptyResponse);
nock(baseURL)
.get('/conversations')
.query(params => params.status === 'closed')
.reply(200, emptyResponse);
const request: CallToolRequest = {
method: 'tools/call',
params: {
name: 'comprehensiveConversationSearch',
arguments: {
searchTerms: ['nonexistent']
}
}
};
const result = await freshToolHandler.callTool(request);
expect(result.isError).toBeUndefined();
const textContent = result.content[0] as { type: 'text'; text: string };
const response = JSON.parse(textContent.text);
expect(response.totalConversationsFound).toBe(0);
expect(response.searchTips).toBeDefined();
expect(response.searchTips).toContain('Try broader search terms or increase the timeframe');
});
});
describe('Advanced Conversation Search - Branch Coverage', () => {
it('should handle advanced search with all parameter types', async () => {
const mockResponse = {
_embedded: { conversations: [] },
page: { size: 50, totalElements: 0 }
};
nock(baseURL)
.get('/conversations')
.query(() => true)
.reply(200, mockResponse);
const request: CallToolRequest = {
method: 'tools/call',
params: {
name: 'advancedConversationSearch',
arguments: {
contentTerms: ['urgent', 'billing'],
subjectTerms: ['help', 'support'],
customerEmail: 'test@example.com',
emailDomain: 'company.com',
tags: ['vip', 'escalation'],
createdBefore: '2024-01-31T23:59:59Z'
}
}
};
const result = await toolHandler.callTool(request);
if (!result.isError) {
const textContent = result.content[0] as { type: 'text'; text: string };
const response = JSON.parse(textContent.text);
expect(response.searchCriteria.contentTerms).toEqual(['urgent', 'billing']);
expect(response.searchCriteria.tags).toEqual(['vip', 'escalation']);
}
});
it('should handle field selection in search conversations', async () => {
const mockResponse = {
_embedded: {
conversations: [
{ id: 1, subject: 'Test', status: 'active', extraField: 'should be filtered' }
]
},
page: { size: 50, totalElements: 1 }
};
nock(baseURL)
.get('/conversations')
.query(() => true)
.reply(200, mockResponse);
const request: CallToolRequest = {
method: 'tools/call',
params: {
name: 'searchConversations',
arguments: {
query: 'test',
fields: ['id', 'subject'] // This should filter fields
}
}
};
const result = await toolHandler.callTool(request);
if (!result.isError) {
const textContent = result.content[0] as { type: 'text'; text: string };
const response = JSON.parse(textContent.text);
expect(response.results[0]).toEqual({ id: 1, subject: 'Test' });
expect(response.results[0].extraField).toBeUndefined();
}
});
});
describe('enhanced searchConversations', () => {
it('should default to active status when query is provided without status', async () => {
const freshToolHandler = new ToolHandler();
nock.cleanAll();
nock(baseURL)
.persist()
.post('/oauth2/token')
.reply(200, {
access_token: 'mock-access-token',
token_type: 'Bearer',
expires_in: 3600,
});
const mockResponse = {
_embedded: {
conversations: []
},
page: {
size: 50,
totalElements: 0,
totalPages: 0,
number: 0
}
};
nock(baseURL)
.get('/conversations')
.query(params => params.status === 'active' && params.query === '(body:"test")')
.reply(200, mockResponse);
const request: CallToolRequest = {
method: 'tools/call',
params: {
name: 'searchConversations',
arguments: {
query: '(body:"test")'
}
}
};
const result = await freshToolHandler.callTool(request);
expect(result.isError).toBeUndefined();
const textContent = result.content[0] as { type: 'text'; text: string };
const response = JSON.parse(textContent.text);
expect(response.searchInfo.status).toBe('active');
expect(response.searchInfo.appliedDefaults).toEqual(['status: active']);
expect(response.searchInfo.searchGuidance).toBeDefined();
});
});
});