Skip to main content
Glama
ContractTests.test.ts23.3 kB
/** * MCP Tool Contract Tests * Comprehensive testing of MCP tool contracts and API validation */ import { ContractTestFramework, setupContractTesting, contractTestUtils, ToolContract, ContractTestResult, APIContractValidationResult } from '../helpers/ContractTestFramework'; import { CastPlanUltimateAutomationServer } from '../../index'; import { testUtils } from '../setup'; describe('MCP Tool Contract Tests', () => { const getFramework = setupContractTesting(); let server: CastPlanUltimateAutomationServer; beforeAll(async () => { // Initialize test server server = new CastPlanUltimateAutomationServer({ projectRoot: process.cwd(), enableAI: false, // Disable AI for contract testing enableCache: false }); // Initialize contract framework await ContractTestFramework.initialize(server as any); }); afterAll(async () => { if (server) { await testUtils.cleanup(() => server.close()); } }); describe('Contract Registration and Validation', () => { test('should load all standard MCP tool contracts', async () => { const framework = getFramework(); expect(framework).toBeDefined(); // Contract framework should have loaded standard contracts const validationResult = await ContractTestFramework.validateMCPToolContracts(); expect(validationResult.toolsValidated).toBeGreaterThan(0); expect(validationResult.results).toBeDefined(); expect(Array.isArray(validationResult.results)).toBe(true); }); test('should validate contract completeness for all tools', async () => { const testContracts: ToolContract[] = [ { name: 'test_tool', description: 'Test tool for contract validation', inputSchema: require('zod').z.object({ testField: require('zod').z.string() }), outputSchema: require('zod').z.object({ success: require('zod').z.boolean(), message: require('zod').z.string() }), sideEffects: 'read', errorCodes: ['TEST_ERROR'] } ]; for (const contract of testContracts) { const issues = contractTestUtils.validateContractCompleteness(contract); expect(issues.length).toBe(0); } }); test('should detect incomplete contracts', async () => { const incompleteContract: ToolContract = { name: 'incomplete_tool', description: 'Short', // Too short inputSchema: require('zod').z.object({}), outputSchema: require('zod').z.object({}), // Missing required fields sideEffects: 'write', // Should have permissions // Missing errorCodes } as any; const issues = contractTestUtils.validateContractCompleteness(incompleteContract); expect(issues.length).toBeGreaterThan(0); expect(issues).toContain('Description too short or missing'); expect(issues).toContain('Write operations should specify required permissions'); expect(issues).toContain('Error codes not specified'); }); }); describe('BMAD Tool Contract Validation', () => { test('should validate bmad_parse_specification contract', async () => { const testCases = [ { input: { content: '# Project Specification\\n\\nThis is a test specification.', format: 'markdown', generateTasks: true, autoAssign: false }, shouldFail: false }, { input: { content: '' // Empty content should fail }, shouldFail: true }, { input: { content: 'Valid content', format: 'invalid_format' // Invalid format }, shouldFail: true }, { input: { content: 'Valid content', generateTasks: 'not_boolean' // Wrong type }, shouldFail: true } ]; // Note: This would require actual server implementation // For now, we test the contract structure const framework = getFramework(); expect(framework).toBeDefined(); // Test contract structure validation const mockTool = { name: 'bmad_parse_specification', description: 'Parse business specifications and generate tasks', inputSchema: { type: 'object', properties: { content: { type: 'string', minLength: 1 }, format: { type: 'string', enum: ['markdown', 'yaml', 'plain'] }, generateTasks: { type: 'boolean' }, autoAssign: { type: 'boolean' } }, required: ['content'] } }; // This would be replaced with actual contract validation expect(mockTool.name).toBe('bmad_parse_specification'); expect(mockTool.inputSchema.properties.content).toBeDefined(); expect(mockTool.inputSchema.required).toContain('content'); }); test('should validate bmad_update_task_status contract', async () => { const testCases = [ { input: { taskId: 'task-123', status: 'completed', notes: 'Task completed successfully' }, shouldFail: false }, { input: { taskId: '', // Empty taskId should fail status: 'completed' }, shouldFail: true }, { input: { taskId: 'task-123', status: 'invalid_status' // Invalid status }, shouldFail: true } ]; // Test contract structure validation const mockTool = { name: 'bmad_update_task_status', description: 'Update task status', inputSchema: { type: 'object', properties: { taskId: { type: 'string', minLength: 1 }, status: { type: 'string', enum: ['pending', 'in_progress', 'completed', 'blocked'] }, notes: { type: 'string' } }, required: ['taskId', 'status'] } }; expect(mockTool.name).toBe('bmad_update_task_status'); expect(mockTool.inputSchema.properties.taskId).toBeDefined(); expect(mockTool.inputSchema.properties.status.enum).toContain('completed'); }); }); describe('Documentation Tool Contract Validation', () => { test('should validate docs_reference contract', async () => { const testCases = [ { input: { workType: 'backend', description: 'Implement user authentication', filePaths: ['src/auth/controller.ts', 'src/auth/service.ts'] }, shouldFail: false }, { input: { workType: '', // Empty workType should fail description: 'Test description' }, shouldFail: true }, { input: { workType: 'frontend', description: '' // Empty description should fail }, shouldFail: true } ]; const mockTool = { name: 'docs_reference', description: 'Find relevant documentation for development work', inputSchema: { type: 'object', properties: { workType: { type: 'string', minLength: 1 }, description: { type: 'string', minLength: 1 }, filePaths: { type: 'array', items: { type: 'string' } } }, required: ['workType', 'description'] } }; expect(mockTool.name).toBe('docs_reference'); expect(mockTool.inputSchema.required).toContain('workType'); expect(mockTool.inputSchema.required).toContain('description'); }); test('should validate docs_search contract with performance considerations', async () => { const performanceTestCases = contractTestUtils.createPerformanceTestCases('docs_search'); const testCases = [ { input: { query: 'authentication', maxResults: 10, documentTypes: ['api', 'guide'] }, shouldFail: false }, { input: { query: 'a', // Very short query maxResults: 1000 // Very high limit }, shouldFail: false // Should be handled gracefully }, { input: { query: '', // Empty query should fail }, shouldFail: true }, { input: { query: 'test', maxResults: 101 // Exceeds maximum }, shouldFail: true } ]; const mockTool = { name: 'docs_search', description: 'Search through project documentation', inputSchema: { type: 'object', properties: { query: { type: 'string', minLength: 1, maxLength: 1000 }, maxResults: { type: 'number', minimum: 1, maximum: 100 }, documentTypes: { type: 'array', items: { type: 'string' }, maxItems: 20 } }, required: ['query'] } }; expect(mockTool.inputSchema.properties.query.maxLength).toBeDefined(); expect(mockTool.inputSchema.properties.maxResults.maximum).toBe(100); expect(mockTool.inputSchema.properties.documentTypes.maxItems).toBeDefined(); }); }); describe('Hooks Tool Contract Validation', () => { test('should validate hooks_trigger contract with security considerations', async () => { const testCases = [ { input: { eventType: 'file_changed', data: { filePath: 'src/test.ts', changeType: 'modified' }, target: 'documentation' }, shouldFail: false }, { input: { eventType: '', // Empty eventType should fail data: {} }, shouldFail: true }, { input: { eventType: 'dangerous_event', data: { command: 'rm -rf /', // Dangerous data should be handled safely unsafeData: '<script>alert("xss")</script>' } }, shouldFail: false // Should be accepted but handled safely } ]; const mockTool = { name: 'hooks_trigger', description: 'Manually trigger hook events', inputSchema: { type: 'object', properties: { eventType: { type: 'string', minLength: 1, maxLength: 100 }, data: { type: 'object' }, target: { type: 'string', maxLength: 100 } }, required: ['eventType', 'data'] } }; expect(mockTool.inputSchema.properties.eventType.maxLength).toBeDefined(); expect(mockTool.inputSchema.properties.target?.maxLength).toBeDefined(); }); test('should validate hooks_setup_git contract with file system permissions', async () => { const testCases = [ { input: { projectRoot: '/valid/project/path', hooks: ['pre-commit', 'post-commit'], overwrite: false }, shouldFail: false }, { input: { projectRoot: '', // Empty path should fail hooks: ['pre-commit'] }, shouldFail: true }, { input: { projectRoot: '/valid/path', hooks: [], // Empty hooks array should fail }, shouldFail: true }, { input: { projectRoot: '/valid/path', hooks: ['invalid-hook'] // Invalid hook type }, shouldFail: true } ]; const mockTool = { name: 'hooks_setup_git', description: 'Setup Git hooks', inputSchema: { type: 'object', properties: { projectRoot: { type: 'string', minLength: 1 }, hooks: { type: 'array', items: { type: 'string', enum: ['pre-commit', 'post-commit', 'pre-push'] }, minItems: 1, maxItems: 10 }, overwrite: { type: 'boolean' } }, required: ['projectRoot', 'hooks'] } }; expect(mockTool.inputSchema.properties.hooks.minItems).toBe(1); expect(mockTool.inputSchema.properties.hooks.items.enum).toContain('pre-commit'); }); }); describe('Enhanced Tool Contract Validation', () => { test('should validate initialize_documentation_system contract', async () => { const testCases = [ { input: { projectRoot: '/project/root', enableAI: true, timeZone: process.env.TEST_TIMEZONE || 'UTC', locale: process.env.TEST_LOCALE || 'en-US' }, shouldFail: false }, { input: { projectRoot: '', // Empty project root should fail }, shouldFail: true }, { input: { projectRoot: '/valid/path', timeZone: 'Invalid/TimeZone' // Invalid timezone should be handled }, shouldFail: false // Should be handled gracefully with fallback } ]; const mockTool = { name: 'initialize_documentation_system', description: 'Initialize the enhanced documentation system', inputSchema: { type: 'object', properties: { projectRoot: { type: 'string', minLength: 1 }, enableAI: { type: 'boolean' }, timeZone: { type: 'string', maxLength: 50 }, locale: { type: 'string', maxLength: 10 } }, required: ['projectRoot'] } }; expect(mockTool.inputSchema.required).toContain('projectRoot'); expect(mockTool.inputSchema.properties.timeZone?.maxLength).toBeDefined(); }); test('should validate track_document_work contract with array limits', async () => { const testCases = [ { input: { workType: 'backend', workDescription: 'Implement authentication system', filePaths: ['src/auth.ts', 'src/user.ts'], expectedDocuments: ['API docs', 'User guide'] }, shouldFail: false }, { input: { workType: 'frontend', workDescription: 'Update UI components', filePaths: new Array(1000).fill('file.ts') // Very large array }, shouldFail: true // Should fail due to array size limit }, { input: { workType: '', // Empty workType workDescription: 'Description', filePaths: ['file.ts'] }, shouldFail: true } ]; const mockTool = { name: 'track_document_work', description: 'Track work-document relationships', inputSchema: { type: 'object', properties: { workType: { type: 'string', minLength: 1, maxLength: 50 }, workDescription: { type: 'string', minLength: 1, maxLength: 2000 }, filePaths: { type: 'array', items: { type: 'string', maxLength: 500 }, maxItems: 100 }, expectedDocuments: { type: 'array', items: { type: 'string', maxLength: 200 }, maxItems: 50 } }, required: ['workType', 'workDescription', 'filePaths'] } }; expect(mockTool.inputSchema.properties.filePaths.maxItems).toBe(100); expect(mockTool.inputSchema.properties.expectedDocuments?.maxItems).toBe(50); }); }); describe('Contract Performance and Security Validation', () => { test('should validate performance characteristics of all tools', async () => { const performanceIssues: string[] = []; // Mock tools with performance issues const toolsToTest = [ { name: 'performance_test_tool', inputSchema: { type: 'object', properties: { unlimitedString: { type: 'string' }, // No maxLength unlimitedArray: { type: 'array', items: { type: 'string' } }, // No maxItems deepNesting: { type: 'object', properties: { level1: { type: 'object', properties: { level2: { type: 'object', properties: { level3: { type: 'string' } } } } } } } } } } ]; for (const tool of toolsToTest) { const schema = tool.inputSchema; if (schema.properties) { for (const [propName, propSchema] of Object.entries(schema.properties)) { if (typeof propSchema === 'object') { if (propSchema.type === 'string' && !propSchema.maxLength) { performanceIssues.push(`String property '${propName}' has no length limit`); } if (propSchema.type === 'array' && !propSchema.maxItems) { performanceIssues.push(`Array property '${propName}' has no item limit`); } } } } } expect(performanceIssues.length).toBeGreaterThan(0); // Should detect issues expect(performanceIssues).toContain("String property 'unlimitedString' has no length limit"); expect(performanceIssues).toContain("Array property 'unlimitedArray' has no item limit"); }); test('should validate security considerations in tool contracts', async () => { const securityIssues: string[] = []; // Mock tools with security considerations const securityTestCases = [ { toolName: 'file_write_tool', sideEffects: 'write' as const, requiredPermissions: undefined, // Missing permissions inputSchema: { type: 'object', properties: { filePath: { type: 'string' }, // No path validation content: { type: 'string' } // No content filtering } } }, { toolName: 'system_command_tool', sideEffects: 'system' as const, requiredPermissions: ['system_exec'], inputSchema: { type: 'object', properties: { command: { type: 'string' }, // Dangerous without validation args: { type: 'array', items: { type: 'string' } } } } } ]; for (const testCase of securityTestCases) { if (testCase.sideEffects === 'write' && !testCase.requiredPermissions) { securityIssues.push(`Tool ${testCase.toolName} performs write operations but doesn't specify required permissions`); } if (testCase.sideEffects === 'system') { securityIssues.push(`Tool ${testCase.toolName} has system-level side effects - requires extra security validation`); } // Check for dangerous input parameters const schema = testCase.inputSchema; if (schema.properties) { if ('filePath' in schema.properties) { securityIssues.push(`Tool ${testCase.toolName} accepts file paths - path traversal prevention needed`); } if ('command' in schema.properties) { securityIssues.push(`Tool ${testCase.toolName} accepts commands - command injection prevention needed`); } } } expect(securityIssues.length).toBeGreaterThan(0); expect(securityIssues.some(issue => issue.includes('write operations'))).toBe(true); expect(securityIssues.some(issue => issue.includes('system-level side effects'))).toBe(true); }); }); describe('Contract Test Report Generation', () => { test('should generate comprehensive contract test report', async () => { const report = await ContractTestFramework.generateContractTestReport(); expect(report.timestamp).toBeDefined(); expect(new Date(report.timestamp).getTime()).toBeGreaterThan(0); expect(report.summary).toBeDefined(); expect(typeof report.summary.toolsValidated).toBe('number'); expect(typeof report.summary.contractsPassed).toBe('number'); expect(typeof report.summary.contractsFailed).toBe('number'); expect(typeof report.summary.overallSuccess).toBe('boolean'); expect(Array.isArray(report.summary.results)).toBe(true); expect(Array.isArray(report.recommendations)).toBe(true); expect(Array.isArray(report.nextSteps)).toBe(true); // Report should contain meaningful recommendations expect(report.recommendations.length).toBeGreaterThan(0); expect(report.nextSteps.length).toBeGreaterThan(0); // Should recommend CI/CD integration expect(report.recommendations.some(r => r.includes('CI/CD'))).toBe(true); }); test('should provide actionable recommendations based on test results', async () => { // Mock test results with various issues const mockResults: ContractTestResult[] = [ { tool: 'fast_tool', passed: true, errors: [], warnings: [], performance: { validationTimeMs: 50, memoryUsageBytes: 1024 } }, { tool: 'slow_tool', passed: true, errors: [], warnings: ['Performance warning'], performance: { validationTimeMs: 150, memoryUsageBytes: 2048 } }, { tool: 'failed_tool', passed: false, errors: ['Schema validation failed', 'Missing required field'], warnings: [], performance: { validationTimeMs: 200, memoryUsageBytes: 5 * 1024 * 1024 } } ]; // This would be part of the framework's recommendation generation const failedTools = mockResults.filter(r => !r.passed); const slowTools = mockResults.filter(r => r.performance.validationTimeMs > 100); const memoryIntensiveTools = mockResults.filter(r => r.performance.memoryUsageBytes > 1024 * 1024); expect(failedTools.length).toBe(1); expect(slowTools.length).toBe(2); // slow_tool and failed_tool expect(memoryIntensiveTools.length).toBe(1); // Recommendations should be specific and actionable const expectedRecommendations = [ 'Fix contract validation for failed_tool', 'Optimize validation performance for slow tools', 'Review memory usage for memory-intensive tools', 'Implement contract testing in CI/CD pipeline' ]; // Each recommendation should be actionable expectedRecommendations.forEach(recommendation => { expect(recommendation).not.toContain('TODO'); expect(recommendation).not.toContain('maybe'); expect(recommendation.length).toBeGreaterThan(10); }); }); }); });

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/Ghostseller/CastPlan_mcp'

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