Skip to main content
Glama
formatResult-contract-regression.test.tsβ€’9.39 kB
/** * formatResult Contract Regression Tests * * These tests prevent future violations of the formatResult contract requirement * that all universal tools must return consistent string output regardless of * environment or test conditions. * * CONTEXT: Phase 1-3 refactoring eliminated dual-mode formatResult functions that * would return JSON in test environments but strings in production, which violated * the MCP protocol requirement for consistent string responses. */ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { ResourceType } from '../src/types/attio.js'; // Import all universal tool configurations import * as coreOpsConfig from '../src/handlers/tool-configs/universal/core/index.js'; import * as advancedOpsConfig from '../src/handlers/tool-configs/universal/advanced-operations.js'; // Mock data factories for testing import { CompanyMockFactory, PersonMockFactory, TaskMockFactory, ListMockFactory, } from './utils/mock-factories/index.js'; /** * All universal tool configurations that have formatResult functions */ const UNIVERSAL_TOOL_CONFIGS = [ { name: 'search-records', config: coreOpsConfig.searchRecordsConfig }, { name: 'get-record-details', config: coreOpsConfig.getRecordDetailsConfig }, { name: 'create-record', config: coreOpsConfig.createRecordConfig }, { name: 'update-record', config: coreOpsConfig.updateRecordConfig }, { name: 'delete-record', config: coreOpsConfig.deleteRecordConfig }, { name: 'get-attributes', config: coreOpsConfig.getAttributesConfig }, { name: 'get-detailed-info', config: coreOpsConfig.getDetailedInfoConfig }, { name: 'advanced-search', config: advancedOpsConfig.advancedSearchConfig }, ]; /** * All supported resource types including the new LISTS type added in Phase 2 */ const ALL_RESOURCE_TYPES: ResourceType[] = [ ResourceType.COMPANIES, ResourceType.PEOPLE, ResourceType.LISTS, ResourceType.RECORDS, ResourceType.TASKS, ResourceType.DEALS, ]; /** * Test environments to verify consistent behavior */ const TEST_ENVIRONMENTS = [ { NODE_ENV: 'test', description: 'test environment' }, { NODE_ENV: 'production', description: 'production environment' }, { NODE_ENV: 'development', description: 'development environment' }, { NODE_ENV: undefined, description: 'undefined environment' }, ]; describe('formatResult Contract Regression Tests', () => { beforeEach(() => { vi.clearAllMocks(); }); describe('String Return Type Contract', () => { UNIVERSAL_TOOL_CONFIGS.forEach(({ name, config }) => { if (!config.formatResult) { return; // Skip tools without formatResult } describe(`${name} formatResult`, () => { ALL_RESOURCE_TYPES.forEach((resourceType) => { it(`should always return string type for ${resourceType}`, () => { const mockData = getMockDataForResourceType(resourceType); const result = config.formatResult(mockData); expect(typeof result).toBe('string'); expect(result).toBeDefined(); expect(result.length).toBeGreaterThan(0); }); }); TEST_ENVIRONMENTS.forEach(({ NODE_ENV, description }) => { it(`should return string type in ${description}`, () => { const originalEnv = process.env.NODE_ENV; process.env.NODE_ENV = NODE_ENV; try { const mockData = getMockDataForResourceType( ResourceType.COMPANIES ); const result = config.formatResult(mockData); expect(typeof result).toBe('string'); } finally { process.env.NODE_ENV = originalEnv; } }); }); }); }); }); describe('JSON Output Prevention', () => { UNIVERSAL_TOOL_CONFIGS.forEach(({ name, config }) => { if (!config.formatResult) { return; // Skip tools without formatResult } describe(`${name} formatResult`, () => { ALL_RESOURCE_TYPES.forEach((resourceType) => { it(`should never return JSON string for ${resourceType}`, () => { const mockData = getMockDataForResourceType(resourceType); const result = config.formatResult(mockData); // Verify result doesn't look like JSON expect(result.trim().startsWith('{')).toBe(false); expect(result.trim().startsWith('[')).toBe(false); expect(result.trim().endsWith('}')).toBe(false); expect(result.trim().endsWith(']')).toBe(false); }); }); TEST_ENVIRONMENTS.forEach(({ NODE_ENV, description }) => { it(`should never return JSON string in ${description}`, () => { const originalEnv = process.env.NODE_ENV; process.env.NODE_ENV = NODE_ENV; try { const mockData = getMockDataForResourceType( ResourceType.COMPANIES ); const result = config.formatResult(mockData); // Verify result doesn't look like JSON expect(result.trim().startsWith('{')).toBe(false); expect(result.trim().startsWith('[')).toBe(false); } finally { process.env.NODE_ENV = originalEnv; } }); }); }); }); }); describe('Consistent Output Format', () => { UNIVERSAL_TOOL_CONFIGS.forEach(({ name, config }) => { if (!config.formatResult) { return; // Skip tools without formatResult } it(`${name} should provide consistent output across environments`, () => { const mockData = getMockDataForResourceType(ResourceType.COMPANIES); const outputs: string[] = []; TEST_ENVIRONMENTS.forEach(({ NODE_ENV }) => { const originalEnv = process.env.NODE_ENV; process.env.NODE_ENV = NODE_ENV; try { const result = config.formatResult(mockData); outputs.push(result); } finally { process.env.NODE_ENV = originalEnv; } }); // All outputs should be identical const firstOutput = outputs[0]; outputs.forEach((output, index) => { expect(output).toBe(firstOutput); }); }); }); }); describe('Error Handling Consistency', () => { UNIVERSAL_TOOL_CONFIGS.forEach(({ name, config }) => { if (!config.formatResult) { return; // Skip tools without formatResult } it(`${name} should handle null/undefined data gracefully`, () => { // Some formatResult functions may require specific structure, so we test with // basic valid structure rather than null to avoid implementation details const minimalValidData = { success: true, data: null }; expect(() => { config.formatResult(minimalValidData); }).not.toThrow(); const resultForNull = config.formatResult(minimalValidData); expect(typeof resultForNull).toBe('string'); }); it(`${name} should handle empty data gracefully`, () => { // Use minimal valid structure for empty data tests const emptyData = { success: true, data: {} }; expect(() => { config.formatResult(emptyData); }).not.toThrow(); expect(() => { const arrayData = { success: true, data: [] }; config.formatResult(arrayData); }).not.toThrow(); }); }); }); describe('All Resource Types Support', () => { UNIVERSAL_TOOL_CONFIGS.forEach(({ name, config }) => { if (!config.formatResult) { return; // Skip tools without formatResult } ALL_RESOURCE_TYPES.forEach((resourceType) => { it(`${name} should support ${resourceType} resource type`, () => { const mockData = getMockDataForResourceType(resourceType); expect(() => { const result = config.formatResult(mockData); expect(typeof result).toBe('string'); }).not.toThrow(); }); }); }); }); describe('Phase 2 LISTS Resource Type Regression', () => { UNIVERSAL_TOOL_CONFIGS.forEach(({ name, config }) => { if (!config.formatResult) { return; // Skip tools without formatResult } it(`${name} should properly handle LISTS resource type added in Phase 2`, () => { const mockListData = ListMockFactory.create(); const result = config.formatResult(mockListData); expect(typeof result).toBe('string'); expect(result.length).toBeGreaterThan(0); expect(result.trim().startsWith('{')).toBe(false); }); }); }); }); /** * Get appropriate mock data for a given resource type */ function getMockDataForResourceType(resourceType: ResourceType): any { switch (resourceType) { case 'companies': return CompanyMockFactory.create(); case 'people': return PersonMockFactory.create(); case 'tasks': return TaskMockFactory.create(); case 'lists': return ListMockFactory.create(); case 'records': return CompanyMockFactory.create(); // Records can use company structure case 'deals': return CompanyMockFactory.create(); // Deals can use company structure for testing default: return CompanyMockFactory.create(); // Fallback to company structure } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/kesslerio/attio-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server