Skip to main content
Glama

mcp-adr-analysis-server

by tosin2013
research-integration.test.ts22.8 kB
/** * Unit tests for research-integration.ts * Tests research directory monitoring, topic extraction, impact evaluation, and update suggestions */ import { jest } from '@jest/globals'; import { McpAdrError } from '../../src/types/index.js'; // Pragmatic mocking approach to avoid TypeScript complexity jest.unstable_mockModule('fs/promises', () => ({ access: jest.fn(), readdir: jest.fn(), stat: jest.fn(), readFile: jest.fn(), })); jest.unstable_mockModule('path', () => ({ resolve: jest.fn(), join: jest.fn(), extname: jest.fn(), relative: jest.fn(), })); jest.unstable_mockModule('../../src/utils/adr-discovery.js', () => ({ discoverAdrsInDirectory: jest.fn(), })); const { monitorResearchDirectory, extractResearchTopics, evaluateResearchImpact, generateAdrUpdateSuggestions, createResearchTemplate, promptForActionConfirmation, } = await import('../../src/utils/research-integration.js'); const fs = await import('fs/promises'); const path = await import('path'); const { discoverAdrsInDirectory } = await import('../../src/utils/adr-discovery.js'); describe('research-integration', () => { beforeEach(() => { jest.clearAllMocks(); }); describe('monitorResearchDirectory', () => { it('should monitor research directory with files', async () => { const mockStats = { mtime: new Date('2024-01-01T12:00:00Z'), size: 1024, }; (path.resolve as jest.MockedFunction<typeof path.resolve>).mockReturnValue( '/project/docs/research' ); (fs.access as jest.MockedFunction<typeof fs.access>).mockResolvedValue(undefined); (fs.readdir as jest.MockedFunction<typeof fs.readdir>).mockResolvedValue([ { name: 'research1.md', isDirectory: () => false, isFile: () => true }, { name: 'research2.txt', isDirectory: () => false, isFile: () => true }, ] as any); (path.join as jest.MockedFunction<typeof path.join>).mockImplementation((...args) => args.join('/') ); (path.extname as jest.MockedFunction<typeof path.extname>).mockImplementation(filename => filename.includes('.md') ? '.md' : '.txt' ); (fs.stat as jest.MockedFunction<typeof fs.stat>).mockResolvedValue(mockStats as any); (fs.readFile as any).mockImplementation((filepath: any) => { if (filepath.toString().includes('research1.md')) { return Promise.resolve('# Research 1\nContent for research 1'); } return Promise.resolve('Research 2 text content'); }); (path.relative as jest.MockedFunction<typeof path.relative>).mockImplementation((from, to) => to.replace(from, '').replace(/^\//, '') ); const result = await monitorResearchDirectory('docs/research'); expect(result).toHaveProperty('monitoringPrompt'); expect(result).toHaveProperty('instructions'); expect(result).toHaveProperty('actualData'); // Verify prompt contains file content expect(result.monitoringPrompt).toContain('Files Found**: 2 research files'); expect(result.monitoringPrompt).toContain('research1.md'); expect(result.monitoringPrompt).toContain('research2.txt'); expect(result.monitoringPrompt).toContain('Research 1'); // Verify instructions expect(result.instructions).toContain('Files Found**: 2 research files'); expect(result.instructions).toContain('Readable Files**: 2 files'); // Verify actual data expect(result.actualData.researchFiles).toHaveLength(2); expect(result.actualData.summary.totalFiles).toBe(2); expect(result.actualData.summary.readableFiles).toBe(2); expect(result.actualData.summary.directoryExists).toBe(true); }); it('should handle empty research directory', async () => { (path.resolve as jest.MockedFunction<typeof path.resolve>).mockReturnValue( '/project/docs/research' ); (fs.access as jest.MockedFunction<typeof fs.access>).mockRejectedValue( new Error('Directory not found') ); const result = await monitorResearchDirectory('docs/research'); expect(result.monitoringPrompt).toContain('No research files found'); expect(result.monitoringPrompt).toContain('mkdir -p docs/research'); expect(result.instructions).toContain('Files Found**: 0 research files'); expect(result.actualData.summary.totalFiles).toBe(0); expect(result.actualData.summary.directoryExists).toBe(false); }); it('should use default research path when not specified', async () => { (path.resolve as jest.MockedFunction<typeof path.resolve>).mockReturnValue( '/project/docs/research' ); (fs.access as jest.MockedFunction<typeof fs.access>).mockRejectedValue( new Error('Directory not found') ); await monitorResearchDirectory(); expect(path.resolve).toHaveBeenCalledWith(process.cwd(), 'docs/research'); }); it('should handle nested directories', async () => { const mockStats = { mtime: new Date('2024-01-01T12:00:00Z'), size: 512 }; (path.resolve as jest.MockedFunction<typeof path.resolve>).mockReturnValue( '/project/docs/research' ); (fs.access as jest.MockedFunction<typeof fs.access>).mockResolvedValue(undefined); (fs.readdir as jest.MockedFunction<typeof fs.readdir>).mockImplementation(dirPath => { if (dirPath === '/project/docs/research') { return Promise.resolve([ { name: 'subdir', isDirectory: () => true, isFile: () => false }, ] as any); } return Promise.resolve([ { name: 'nested.md', isDirectory: () => false, isFile: () => true }, ] as any); }); (path.join as jest.MockedFunction<typeof path.join>).mockImplementation((...args) => args.join('/') ); (path.extname as jest.MockedFunction<typeof path.extname>).mockReturnValue('.md'); (fs.stat as jest.MockedFunction<typeof fs.stat>).mockResolvedValue(mockStats as any); (fs.readFile as jest.MockedFunction<typeof fs.readFile>).mockResolvedValue( 'Nested file content' ); (path.relative as jest.MockedFunction<typeof path.relative>).mockReturnValue( 'subdir/nested.md' ); const result = await monitorResearchDirectory('docs/research'); expect(result.actualData.researchFiles).toHaveLength(1); expect(result.actualData.researchFiles[0].filename).toBe('nested.md'); }); it('should throw McpAdrError on monitoring failure', async () => { const monitoringError = new Error('File system error'); (path.resolve as jest.MockedFunction<typeof path.resolve>).mockImplementation(() => { throw monitoringError; }); await expect(monitorResearchDirectory()).rejects.toThrow(McpAdrError); await expect(monitorResearchDirectory()).rejects.toThrow( 'Failed to monitor research directory: File system error' ); }); }); describe('extractResearchTopics', () => { it('should extract topics from research files', async () => { // Set up proper mocks for the file system operations (path.resolve as jest.MockedFunction<typeof path.resolve>).mockReturnValue( '/project/docs/research' ); (fs.access as jest.MockedFunction<typeof fs.access>).mockResolvedValue(undefined); (fs.readdir as jest.MockedFunction<typeof fs.readdir>).mockResolvedValue([ { name: 'architecture.md', isDirectory: () => false, isFile: () => true }, ] as any); (path.join as jest.MockedFunction<typeof path.join>).mockImplementation((...args) => args.join('/') ); (path.extname as jest.MockedFunction<typeof path.extname>).mockReturnValue('.md'); (fs.stat as jest.MockedFunction<typeof fs.stat>).mockResolvedValue({ mtime: new Date('2024-01-01T12:00:00Z'), size: 1024, } as any); (fs.readFile as jest.MockedFunction<typeof fs.readFile>).mockResolvedValue( '# Architecture Research\nKey findings about microservices' ); (path.relative as jest.MockedFunction<typeof path.relative>).mockReturnValue( 'docs/research/architecture.md' ); const existingTopics = ['Topic 1', 'Topic 2']; const result = await extractResearchTopics('docs/research', existingTopics); expect(result).toHaveProperty('extractionPrompt'); expect(result).toHaveProperty('instructions'); expect(result).toHaveProperty('actualData'); expect(result.extractionPrompt).toContain('architecture.md'); expect(result.extractionPrompt).toContain('Key findings about microservices'); expect(result.extractionPrompt).toContain('Current Topics (2)'); expect(result.extractionPrompt).toContain('Topic 1'); expect(result.instructions).toContain('Text-Based Files**: 1 files'); expect(result.instructions).toContain('Existing Topics**: 2 topics'); }); it('should handle no existing topics', async () => { // Set up mocks for empty directory (path.resolve as jest.MockedFunction<typeof path.resolve>).mockReturnValue( '/project/docs/research' ); (fs.access as jest.MockedFunction<typeof fs.access>).mockRejectedValue( new Error('Directory not found') ); const result = await extractResearchTopics('docs/research'); expect(result.extractionPrompt).toContain('No existing topics provided'); expect(result.instructions).toContain('Existing Topics**: 0 topics'); }); it('should throw McpAdrError on extraction failure', async () => { const extractionError = new Error('File system error'); (path.resolve as jest.MockedFunction<typeof path.resolve>).mockImplementation(() => { throw extractionError; }); await expect(extractResearchTopics()).rejects.toThrow(McpAdrError); await expect(extractResearchTopics()).rejects.toThrow( 'Failed to extract research topics: Failed to monitor research directory: File system error' ); }); }); describe('evaluateResearchImpact', () => { it('should evaluate research impact on ADRs', async () => { const mockResearchTopics = [ { id: 'topic-1', title: 'Microservices Architecture', category: 'architecture', description: 'Research on microservices patterns', sourceFiles: ['research1.md'], keyFindings: ['Improved scalability', 'Increased complexity'], evidence: ['Performance tests', 'Case studies'], confidence: 0.8, relevanceScore: 0.9, lastUpdated: '2024-01-01T12:00:00Z', tags: ['architecture', 'scalability'], }, ]; const mockDiscoveryResult = { adrs: [ { title: 'Use Monolithic Architecture', filename: 'ADR-001-monolith.md', status: 'Accepted', content: 'We will use a monolithic architecture for simplicity.', path: '/test/ADR-001-monolith.md', metadata: { number: 1 }, }, ], totalAdrs: 1, summary: { byStatus: { Accepted: 1 }, byCategory: {} }, directory: 'docs/adrs', recommendations: [], }; ( discoverAdrsInDirectory as jest.MockedFunction<typeof discoverAdrsInDirectory> ).mockResolvedValue(mockDiscoveryResult as any); const result = await evaluateResearchImpact(mockResearchTopics, 'docs/adrs'); expect(discoverAdrsInDirectory).toHaveBeenCalledWith('docs/adrs', true, process.cwd()); expect(result).toHaveProperty('evaluationPrompt'); expect(result).toHaveProperty('instructions'); expect(result).toHaveProperty('actualData'); expect(result.evaluationPrompt).toContain('Use Monolithic Architecture'); expect(result.evaluationPrompt).toContain('Microservices Architecture'); expect(result.evaluationPrompt).toContain('Relevance Score**: 0.9'); expect(result.evaluationPrompt).toContain('Confidence**: 0.8'); expect(result.instructions).toContain('Research Topics**: 1 topics'); expect(result.instructions).toContain('ADRs Found**: 1 files'); expect(result.instructions).toContain('High Relevance Topics**: 1 topics'); }); it('should handle no ADRs found', async () => { const mockResearchTopics = [ { id: 'topic-1', title: 'Test Topic', category: 'test', description: 'Test description', sourceFiles: ['test.md'], keyFindings: ['Finding 1'], evidence: ['Evidence 1'], confidence: 0.5, relevanceScore: 0.3, lastUpdated: '2024-01-01T12:00:00Z', tags: ['test'], }, ]; const mockDiscoveryResult = { adrs: [], totalAdrs: 0, summary: { byStatus: {}, byCategory: {} }, directory: 'docs/adrs', recommendations: [], }; ( discoverAdrsInDirectory as jest.MockedFunction<typeof discoverAdrsInDirectory> ).mockResolvedValue(mockDiscoveryResult as any); const result = await evaluateResearchImpact(mockResearchTopics, 'docs/adrs'); expect(result.evaluationPrompt).toContain('No ADRs found in the specified directory'); expect(result.instructions).toContain('High Relevance Topics**: 0 topics'); }); it('should throw McpAdrError on evaluation failure', async () => { const evaluationError = new Error('ADR discovery failed'); ( discoverAdrsInDirectory as jest.MockedFunction<typeof discoverAdrsInDirectory> ).mockRejectedValue(evaluationError); const mockTopics = [ { id: 'topic-1', title: 'Test Topic', category: 'test', description: 'Test', sourceFiles: ['test.md'], keyFindings: ['Test'], evidence: ['Test'], confidence: 0.5, relevanceScore: 0.5, lastUpdated: '2024-01-01T12:00:00Z', tags: ['test'], }, ]; await expect(evaluateResearchImpact(mockTopics)).rejects.toThrow(McpAdrError); await expect(evaluateResearchImpact(mockTopics)).rejects.toThrow( 'Failed to evaluate research impact: ADR discovery failed' ); }); }); describe('generateAdrUpdateSuggestions', () => { it('should generate update suggestions for found ADR', async () => { const mockDiscoveryResult = { adrs: [ { title: 'Database Selection', filename: 'ADR-001-database.md', status: 'Accepted', content: 'We will use PostgreSQL for our database needs.', path: '/test/ADR-001-database.md', metadata: { number: 1 }, }, ], totalAdrs: 1, summary: { byStatus: { Accepted: 1 }, byCategory: {} }, directory: 'docs/adrs', recommendations: [], }; const mockResearchFindings = [ { finding: 'NoSQL databases show better performance for our use case', evidence: ['Performance benchmarks', 'Scalability studies'], impact: 'High - may require architecture change', }, ]; ( discoverAdrsInDirectory as jest.MockedFunction<typeof discoverAdrsInDirectory> ).mockResolvedValue(mockDiscoveryResult as any); const result = await generateAdrUpdateSuggestions( '001', mockResearchFindings, 'content', 'docs/adrs' ); expect(result).toHaveProperty('updatePrompt'); expect(result).toHaveProperty('instructions'); expect(result).toHaveProperty('actualData'); expect(result.updatePrompt).toContain('Database Selection'); expect(result.updatePrompt).toContain('✅ FOUND TARGET ADR'); expect(result.updatePrompt).toContain('NoSQL databases show better performance'); expect(result.updatePrompt).toContain('Update Type**: content'); expect(result.instructions).toContain('✅ Found - Database Selection'); expect(result.instructions).toContain('Research Findings**: 1 findings'); expect(result.actualData.summary.targetFound).toBe(true); expect(result.actualData.summary.updateType).toBe('content'); }); it('should handle ADR not found scenario', async () => { const mockDiscoveryResult = { adrs: [ { title: 'Different ADR', filename: 'ADR-002-other.md', status: 'Accepted', content: 'Different content', path: '/test/ADR-002-other.md', metadata: { number: 2 }, }, ], totalAdrs: 1, summary: { byStatus: { Accepted: 1 }, byCategory: {} }, directory: 'docs/adrs', recommendations: [], }; const mockResearchFindings = [ { finding: 'Test finding', evidence: ['Test evidence'], impact: 'Low', }, ]; ( discoverAdrsInDirectory as jest.MockedFunction<typeof discoverAdrsInDirectory> ).mockResolvedValue(mockDiscoveryResult as any); const result = await generateAdrUpdateSuggestions( '999', mockResearchFindings, 'status', 'docs/adrs' ); expect(result.updatePrompt).toContain('❌ TARGET ADR NOT FOUND'); expect(result.updatePrompt).toContain('Target ADR ID**: 999'); expect(result.instructions).toContain('❌ Not found - will need identification'); expect(result.actualData.summary.targetFound).toBe(false); }); it('should handle different update types', async () => { const mockDiscoveryResult = { adrs: [ { title: 'Test ADR', filename: 'ADR-test.md', status: 'Accepted', content: 'Test content', path: '/test/ADR-test.md', }, ], totalAdrs: 1, summary: { byStatus: { Accepted: 1 }, byCategory: {} }, directory: 'docs/adrs', recommendations: [], }; const mockFindings = [{ finding: 'Test', evidence: ['Test'], impact: 'Test' }]; ( discoverAdrsInDirectory as jest.MockedFunction<typeof discoverAdrsInDirectory> ).mockResolvedValue(mockDiscoveryResult as any); const updateTypes = [ 'content', 'status', 'consequences', 'alternatives', 'deprecation', ] as const; for (const updateType of updateTypes) { const result = await generateAdrUpdateSuggestions('test', mockFindings, updateType); expect(result.updatePrompt).toContain(`Update Type**: ${updateType}`); } }); it('should throw McpAdrError on update generation failure', async () => { const updateError = new Error('Update generation failed'); ( discoverAdrsInDirectory as jest.MockedFunction<typeof discoverAdrsInDirectory> ).mockRejectedValue(updateError); const mockFindings = [{ finding: 'Test', evidence: ['Test'], impact: 'Test' }]; await expect(generateAdrUpdateSuggestions('test', mockFindings, 'content')).rejects.toThrow( McpAdrError ); await expect(generateAdrUpdateSuggestions('test', mockFindings, 'content')).rejects.toThrow( 'Failed to generate ADR update suggestions: Update generation failed' ); }); }); describe('createResearchTemplate', () => { it('should create research template with title and category', () => { const template = createResearchTemplate('API Design Research', 'architecture'); expect(template).toContain('# API Design Research'); expect(template).toContain('**Category**: architecture'); expect(template).toContain('**Status**: In Progress'); expect(template).toContain('## Research Question'); expect(template).toContain('## Key Findings'); expect(template).toContain('## Implications'); expect(template).toContain('## Recommendations'); expect(template).toContain('## Related ADRs'); expect(template).toContain('## Next Steps'); expect(template).toContain('## References'); }); it('should use default category when not specified', () => { const template = createResearchTemplate('Test Research'); expect(template).toContain('# Test Research'); expect(template).toContain('**Category**: general'); }); it('should include current date', () => { const template = createResearchTemplate('Date Test'); const currentDate = new Date().toISOString().split('T')[0]; expect(template).toContain(`**Date**: ${currentDate}`); }); }); describe('promptForActionConfirmation', () => { it('should create confirmation prompt for different impact levels', () => { const impactLevels = ['low', 'medium', 'high', 'critical'] as const; for (const impact of impactLevels) { const result = promptForActionConfirmation('Test Action', 'Test details', impact); expect(result).toHaveProperty('confirmationPrompt'); expect(result).toHaveProperty('instructions'); expect(result.confirmationPrompt).toContain('Test Action'); expect(result.confirmationPrompt).toContain('Test details'); expect(result.confirmationPrompt).toContain(impact.toUpperCase()); // Check risk assessment icons if (impact === 'critical') { expect(result.confirmationPrompt).toContain('🔴 **CRITICAL**'); } else if (impact === 'high') { expect(result.confirmationPrompt).toContain('🟠 **HIGH**'); } else if (impact === 'medium') { expect(result.confirmationPrompt).toContain('🟡 **MEDIUM**'); } else { expect(result.confirmationPrompt).toContain('🟢 **LOW**'); } } }); it('should include response options', () => { const result = promptForActionConfirmation('Test', 'Details', 'medium'); expect(result.confirmationPrompt).toContain('**APPROVED**'); expect(result.confirmationPrompt).toContain('**REJECTED**'); expect(result.confirmationPrompt).toContain('**MODIFIED**'); expect(result.confirmationPrompt).toContain('**DEFERRED**'); }); it('should include confirmation requirements', () => { const result = promptForActionConfirmation('Test', 'Details', 'high'); expect(result.confirmationPrompt).toContain('Understanding'); expect(result.confirmationPrompt).toContain('Authorization'); expect(result.confirmationPrompt).toContain('Impact Assessment'); expect(result.confirmationPrompt).toContain('Timing'); expect(result.confirmationPrompt).toContain('Resources'); }); }); });

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/tosin2013/mcp-adr-analysis-server'

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