Skip to main content
Glama
MockFactories.ts24.8 kB
/** * Advanced Mock Factories for Comprehensive Testing * * Creates realistic mocks for all services and dependencies */ import { BMADService } from '../../services/BMADService.js'; import { DocumentationService } from '../../services/DocumentationService.js'; import { HooksService } from '../../services/HooksService.js'; import { DateTimeService } from '../../services/DateTimeService.js'; import { DocumentLifecycleService } from '../../services/DocumentLifecycleService.js'; import { WorkDocumentConnectionService } from '../../services/WorkDocumentConnectionService.js'; import { DocumentTreeService } from '../../services/DocumentTreeService.js'; import { AIAnalysisService } from '../../services/AIAnalysisService.js'; import { TestDataFactory, MockLoggerFactory } from './TestUtils.js'; import { MockI18nServiceFactory, TEST_LOCALES, LocaleTestUtils } from './I18nTestUtils.js'; import type { I18nTestConfig, MockI18nService } from './I18nTestUtils.js'; export class ServiceMockFactory { // BMAD Service Mock static createBMADServiceMock(): jest.Mocked<BMADService> { const mockTasks = [ TestDataFactory.createMockTask({ id: '1', title: 'Frontend Development' }), TestDataFactory.createMockTask({ id: '2', title: 'Backend API Implementation' }) ]; const mockAgents = [ TestDataFactory.createMockAgent({ id: '1', name: 'Frontend Dev', role: 'frontend' }), TestDataFactory.createMockAgent({ id: '2', name: 'Backend Dev', role: 'backend' }) ]; const mockAssignments = [ { id: '1', taskId: '1', agentId: '1', assignedAt: new Date().toISOString() }, { id: '2', taskId: '2', agentId: '2', assignedAt: new Date().toISOString() } ]; return { parseSpecification: jest.fn().mockResolvedValue({ tasks: mockTasks, agents: mockAgents, assignments: mockAssignments.slice(0, 1), // Only one auto-assignment success: true }), getTasks: jest.fn().mockResolvedValue(mockTasks), getAgents: jest.fn().mockResolvedValue(mockAgents), getAssignments: jest.fn().mockResolvedValue(mockAssignments), updateTaskStatus: jest.fn().mockImplementation(async (taskId: string, status: string) => { const task = mockTasks.find(t => t.id === taskId); if (task) { task.status = status; task.updatedAt = new Date().toISOString(); return true; } return false; }), assignTaskToAgent: jest.fn().mockResolvedValue(true), createTask: jest.fn().mockImplementation(async (taskData: any) => { const newTask = TestDataFactory.createMockTask(taskData); mockTasks.push(newTask); return newTask; }), deleteTask: jest.fn().mockImplementation(async (taskId: string) => { const index = mockTasks.findIndex(t => t.id === taskId); if (index > -1) { mockTasks.splice(index, 1); return true; } return false; }) } as jest.Mocked<BMADService>; } // Documentation Service Mock static createDocumentationServiceMock(): jest.Mocked<DocumentationService> { const mockChangeHistory = [ { id: '1', filePath: '/docs/api.md', changeType: 'updated', timestamp: new Date().toISOString(), description: 'Updated API documentation' } ]; return { processDocumentationRequest: jest.fn().mockImplementation(async (request: any) => { const { action, files, context, category, workType } = request; switch (action) { case 'reference': return { relevantDocs: files.map((file: string) => ({ path: `/docs/${path.basename(file, path.extname(file))}.md`, relevance: 0.8, lastUpdated: new Date().toISOString() })), recommendations: ['Update API documentation', 'Add code examples'], category: category || 'general', workType: workType || 'implement' }; case 'update': mockChangeHistory.push({ id: String(mockChangeHistory.length + 1), filePath: files[0], changeType: 'updated', timestamp: new Date().toISOString(), description: context }); return { updatedDocs: files, success: true, changes: mockChangeHistory.slice(-1) }; default: return { success: false, error: 'Unknown action' }; } }), getChangeHistory: jest.fn().mockResolvedValue(mockChangeHistory), searchDocumentation: jest.fn().mockImplementation(async (query: string) => { return [ { path: '/docs/search-result.md', title: `Documentation for ${query}`, excerpt: `This document contains information about ${query}...`, relevance: 0.9 } ]; }), validateDocumentationStructure: jest.fn().mockResolvedValue({ isValid: true, issues: [], recommendations: ['Consider adding more examples'], score: 0.85 }) } as jest.Mocked<DocumentationService>; } // Hooks Service Mock static createHooksServiceMock(): jest.Mocked<HooksService> { const mockEventHistory = [ TestDataFactory.createMockHookEvent({ type: 'pre-work', data: { workType: 'frontend' } }), TestDataFactory.createMockHookEvent({ type: 'file-change', data: { filePath: '/src/index.ts' } }) ]; const mockActiveWatchers = [ { id: '1', pattern: '**/*.ts', active: true }, { id: '2', pattern: '**/*.md', active: true } ]; return { processHookRequest: jest.fn().mockImplementation(async (request: any) => { const { event } = request; mockEventHistory.push(event); return { success: true, event: event, processedAt: new Date().toISOString(), triggeredActions: ['documentation-update', 'validation'] }; }), getEventHistory: jest.fn().mockResolvedValue(mockEventHistory), getActiveWatchers: jest.fn().mockResolvedValue(mockActiveWatchers), setupGitHooks: jest.fn().mockResolvedValue(true), startFileWatching: jest.fn().mockImplementation(async (patterns?: string[]) => { const newWatchers = (patterns || ['**/*']).map((pattern, index) => ({ id: String(mockActiveWatchers.length + index + 1), pattern, active: true })); mockActiveWatchers.push(...newWatchers); return true; }), stopFileWatching: jest.fn().mockImplementation(async () => { mockActiveWatchers.forEach(watcher => watcher.active = false); return true; }), getConfig: jest.fn().mockResolvedValue({ watchPatterns: ['**/*.ts', '**/*.md'], gitHooksEnabled: true, eventProcessing: true }) } as jest.Mocked<HooksService>; } // DateTime Service Mock - with configurable locale/timezone support static createDateTimeServiceMock(config: I18nTestConfig = TEST_LOCALES['UTC']): jest.Mocked<DateTimeService> { const fixedDate = new Date('2025-01-30T10:00:00.000Z'); return { getCurrentTimestamp: jest.fn().mockReturnValue(fixedDate.toISOString()), // Locale-aware date formatting getCurrentKoreanDate: jest.fn().mockImplementation(() => { if (config.locale.startsWith('ko')) { return '2025년 1월 30일 19:00'; } else if (config.locale.startsWith('ja')) { return '2025年1月30日 19:00'; } else if (config.locale.startsWith('zh')) { return '2025年1月30日 19:00'; } else { return 'January 30, 2025 19:00'; } }), formatDate: jest.fn().mockImplementation((date: Date | string, format?: string) => { const dateObj = typeof date === 'string' ? new Date(date) : date; if (format === 'korean' || config.locale.startsWith('ko')) { const localDate = LocaleTestUtils.formatDate(dateObj, config); return localDate.replace(/(\d{4})-(\d{2})-(\d{2})/, '$1년 $2월 $3일'); } else if (format === 'japanese' || config.locale.startsWith('ja')) { const localDate = LocaleTestUtils.formatDate(dateObj, config); return localDate.replace(/(\d{4})-(\d{2})-(\d{2})/, '$1年$2月$3日'); } else if (format && format.includes('custom')) { return LocaleTestUtils.formatDate(dateObj, config); } // Default format in specified timezone return new Intl.DateTimeFormat('en-CA', { timeZone: config.timezone, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }).format(dateObj).replace(',', ''); }), parseDate: jest.fn().mockImplementation((dateString: string, format?: string) => { try { return new Date(dateString); } catch { return null; } }), addDays: jest.fn().mockImplementation((date: Date, days: number) => { const newDate = new Date(date); newDate.setDate(newDate.getDate() + days); return newDate; }), getDaysBetween: jest.fn().mockImplementation((startDate: Date, endDate: Date) => { const diffTime = endDate.getTime() - startDate.getTime(); return Math.floor(diffTime / (1000 * 60 * 60 * 24)); }), // Business day logic should work consistently across timezones isBusinessDay: jest.fn().mockImplementation((date: Date) => { // Convert to local timezone date const localDate = new Date(date.toLocaleString('en-US', { timeZone: config.timezone })); const dayOfWeek = localDate.getDay(); // 0 = Sunday, 6 = Saturday return dayOfWeek >= 1 && dayOfWeek <= 5; // Monday-Friday }), getNextBusinessDay: jest.fn().mockImplementation((date: Date) => { let nextDay = new Date(date); do { nextDay.setDate(nextDay.getDate() + 1); const localNextDay = new Date(nextDay.toLocaleString('en-US', { timeZone: config.timezone })); const dayOfWeek = localNextDay.getDay(); if (dayOfWeek >= 1 && dayOfWeek <= 5) { break; } } while (true); return nextDay; }), getBusinessDaysBetween: jest.fn().mockImplementation((startDate: Date, endDate: Date) => { let count = 0; const current = new Date(startDate); current.setDate(current.getDate() + 1); // Start from next day while (current < endDate) { const localCurrent = new Date(current.toLocaleString('en-US', { timeZone: config.timezone })); const dayOfWeek = localCurrent.getDay(); if (dayOfWeek >= 1 && dayOfWeek <= 5) { count++; } current.setDate(current.getDate() + 1); } return endDate < startDate ? -count : count; }), formatRelativeTime: jest.fn().mockImplementation((date: Date) => { const now = new Date(); const diffMs = now.getTime() - date.getTime(); const diffMinutes = Math.floor(diffMs / (1000 * 60)); if (diffMinutes < 1) return 'just now'; if (diffMinutes < 60) return `${diffMinutes} minutes ago`; const diffHours = Math.floor(diffMinutes / 60); if (diffHours < 24) return `${diffHours} hours ago`; const diffDays = Math.floor(diffHours / 24); if (diffDays < 7) return `${diffDays} days ago`; const diffWeeks = Math.floor(diffDays / 7); return `${diffWeeks} weeks ago`; }), getTimezone: jest.fn().mockReturnValue(config.timezone) } as jest.Mocked<DateTimeService>; } // Document Lifecycle Service Mock static createDocumentLifecycleServiceMock(): jest.Mocked<DocumentLifecycleService> { const mockDocuments = [ TestDataFactory.createMockDocument({ id: '1', title: 'API Documentation', state: 'published' }), TestDataFactory.createMockDocument({ id: '2', title: 'User Guide', state: 'draft' }), TestDataFactory.createMockDocument({ id: '3', title: 'Architecture Overview', state: 'review' }) ]; return { initialize: jest.fn().mockResolvedValue(undefined), getAllDocuments: jest.fn().mockResolvedValue(mockDocuments), getDocumentById: jest.fn().mockImplementation(async (id: string) => { return mockDocuments.find(doc => doc.id === id) || null; }), createDocument: jest.fn().mockImplementation(async (docData: any) => { const newDoc = TestDataFactory.createMockDocument(docData); mockDocuments.push(newDoc); return newDoc; }), updateDocumentState: jest.fn().mockImplementation(async (id: string, state: string) => { const doc = mockDocuments.find(d => d.id === id); if (doc) { doc.state = state; doc.lastModified = new Date().toISOString(); return true; } return false; }), scheduleReview: jest.fn().mockResolvedValue(true), getDocumentsByState: jest.fn().mockImplementation(async (state: string) => { return mockDocuments.filter(doc => doc.state === state); }), deleteDocument: jest.fn().mockImplementation(async (id: string) => { const index = mockDocuments.findIndex(doc => doc.id === id); if (index > -1) { mockDocuments.splice(index, 1); return true; } return false; }) } as jest.Mocked<DocumentLifecycleService>; } // Work Document Connection Service Mock static createWorkDocumentConnectionServiceMock(): jest.Mocked<WorkDocumentConnectionService> { const mockConnections = [ TestDataFactory.createMockConnection({ id: '1', workType: 'frontend' }), TestDataFactory.createMockConnection({ id: '2', workType: 'backend' }) ]; return { initialize: jest.fn().mockResolvedValue(undefined), getAllConnections: jest.fn().mockResolvedValue(mockConnections), createConnection: jest.fn().mockImplementation(async (connectionData: any) => { const newConnection = TestDataFactory.createMockConnection(connectionData); mockConnections.push(newConnection); return newConnection; }), updateConnectionStrength: jest.fn().mockImplementation(async (id: string, strength: number) => { const connection = mockConnections.find(c => c.id === id); if (connection) { connection.connectionStrength = strength; return true; } return false; }), getConnectionsByWorkType: jest.fn().mockImplementation(async (workType: string) => { return mockConnections.filter(conn => conn.workType === workType); }), deleteConnection: jest.fn().mockImplementation(async (id: string) => { const index = mockConnections.findIndex(conn => conn.id === id); if (index > -1) { mockConnections.splice(index, 1); return true; } return false; }) } as jest.Mocked<WorkDocumentConnectionService>; } // Document Tree Service Mock static createDocumentTreeServiceMock(): jest.Mocked<DocumentTreeService> { const mockTreeNodes = [ TestDataFactory.createMockTreeNode({ id: '1', treeType: 'master', depth: 0 }), TestDataFactory.createMockTreeNode({ id: '2', treeType: 'component', depth: 1, parentId: '1' }), TestDataFactory.createMockTreeNode({ id: '3', treeType: 'category', depth: 1, parentId: '1' }) ]; return { initialize: jest.fn().mockResolvedValue(undefined), buildTree: jest.fn().mockImplementation(async (documents: any[]) => { return mockTreeNodes.slice(0, documents.length); }), getTreeNodes: jest.fn().mockResolvedValue(mockTreeNodes), createTreeNode: jest.fn().mockImplementation(async (nodeData: any) => { const newNode = TestDataFactory.createMockTreeNode(nodeData); mockTreeNodes.push(newNode); return newNode; }), updateTreeStructure: jest.fn().mockResolvedValue(true), deleteTreeNode: jest.fn().mockImplementation(async (id: string) => { const index = mockTreeNodes.findIndex(node => node.id === id); if (index > -1) { mockTreeNodes.splice(index, 1); return true; } return false; }) } as jest.Mocked<DocumentTreeService>; } // AI Analysis Service Mock static createAIAnalysisServiceMock(provider: 'openai' | 'anthropic' = 'openai'): jest.Mocked<AIAnalysisService> { return { analyzeQuality: jest.fn().mockImplementation(async (documentPath: string) => { return { score: 0.85, insights: [ 'Document structure is well-organized', 'Good use of headings and sections', 'Could benefit from more code examples' ], suggestions: [ 'Add more practical examples', 'Include troubleshooting section', 'Consider adding diagrams' ] }; }), detectDuplicates: jest.fn().mockImplementation(async (documentPath: string) => { return { score: 0.1, // Low score means few duplicates insights: [ 'Minimal content duplication detected', 'Some common patterns in code examples' ], suggestions: [ 'Consider extracting common examples to shared section' ] }; }), calculateRelevance: jest.fn().mockImplementation(async (context: string, workDescription: string) => { return { score: 0.78, insights: [ 'Good alignment with current work', 'Relevant to project requirements', 'Matches technical stack' ], suggestions: [ 'Update after implementation', 'Add performance considerations' ] }; }), getProvider: jest.fn().mockReturnValue(provider), isEnabled: jest.fn().mockReturnValue(true) } as jest.Mocked<AIAnalysisService>; } // I18n Service Mock - for internationalization testing static createI18nServiceMock(config: I18nTestConfig = TEST_LOCALES['en-US']): jest.Mocked<MockI18nService> { return MockI18nServiceFactory.create(config); } /** * Create a complete service suite with configurable locale/timezone */ static createServiceSuite(config: I18nTestConfig = TEST_LOCALES['en-US']) { return { dateTimeService: this.createDateTimeServiceMock(config), i18nService: this.createI18nServiceMock(config), lifecycleService: this.createDocumentLifecycleServiceMock(), connectionService: this.createWorkDocumentConnectionServiceMock(), treeService: this.createDocumentTreeServiceMock(), aiService: this.createAIAnalysisServiceMock(), bmadService: this.createBMADServiceMock(), documentationService: this.createDocumentationServiceMock(), hooksService: this.createHooksServiceMock(), logger: MockLoggerFactory.create() }; } /** * Create service suites for multiple locale configurations */ static createMultiLocaleServiceSuites( locales: (keyof typeof TEST_LOCALES)[] = ['en-US', 'ko-KR', 'de-DE', 'ja-JP'] ) { return locales.reduce((suites, localeKey) => { suites[localeKey] = this.createServiceSuite(TEST_LOCALES[localeKey]); return suites; }, {} as Record<string, ReturnType<typeof ServiceMockFactory.createServiceSuite>>); } } // Server Mock Factory export class ServerMockFactory { static createMCPServerMock() { return { setRequestHandler: jest.fn(), connect: jest.fn().mockResolvedValue(undefined) }; } static createTransportMock() { return { start: jest.fn().mockResolvedValue(undefined), close: jest.fn().mockResolvedValue(undefined) }; } } // Configuration Mock Factory export class ConfigMockFactory { static createUltimateAutomationConfig(overrides: any = {}) { // Support configurable locale/timezone with sensible defaults const i18nConfig = overrides.i18n || TEST_LOCALES['en-US']; return { projectRoot: '/test/project', databasePath: ':memory:', services: { bmad: true, documentation: true, hooks: true, enhanced: true, ...overrides.services }, ai: { enabled: false, provider: 'openai', apiKey: undefined, model: undefined, ...overrides.ai }, // Use configurable i18n settings, with fallback to original values for backward compatibility timeZone: overrides.timeZone || i18nConfig.timezone || 'UTC', locale: overrides.locale || i18nConfig.locale || 'en-US', // Additional i18n configuration i18n: { timezone: i18nConfig.timezone || 'UTC', locale: i18nConfig.locale || 'en-US', dateFormat: i18nConfig.dateFormat || 'yyyy-MM-dd', currency: i18nConfig.currency || 'USD', rtl: i18nConfig.rtl || false, ...overrides.i18n }, logLevel: 'error', logFile: undefined, cacheEnabled: true, maxConcurrentOperations: 5, watchMode: false, watchPatterns: undefined, watchIgnored: undefined, ...overrides }; } /** * Create configuration for specific locale/timezone testing */ static createLocaleSpecificConfig( localeKey: keyof typeof TEST_LOCALES, overrides: any = {} ) { const localeConfig = TEST_LOCALES[localeKey]; return this.createUltimateAutomationConfig({ i18n: localeConfig, timeZone: localeConfig.timezone, locale: localeConfig.locale, ...overrides }); } /** * Create configuration for cross-timezone testing */ static createCrossTimezoneConfigs( timezones: string[] = ['UTC', 'America/New_York', 'Asia/Seoul', 'Europe/London'] ) { return timezones.map(timezone => this.createUltimateAutomationConfig({ i18n: { ...TEST_LOCALES['en-US'], timezone }, timeZone: timezone })); } /** * Create configurations for comprehensive locale testing */ static createMultiLocaleConfigs( locales: (keyof typeof TEST_LOCALES)[] = ['en-US', 'ko-KR', 'de-DE', 'ja-JP'] ) { return locales.map(localeKey => this.createLocaleSpecificConfig(localeKey)); } } // File System Mock Factory export class FileSystemMockFactory { static createNodeFileSystemAdapterMock() { const mockFiles = new Map<string, string>(); return { readFile: jest.fn().mockImplementation(async (filePath: string) => { const content = mockFiles.get(filePath); if (content === undefined) { throw new Error(`ENOENT: no such file or directory, open '${filePath}'`); } return content; }), writeFile: jest.fn().mockImplementation(async (filePath: string, content: string) => { mockFiles.set(filePath, content); }), exists: jest.fn().mockImplementation(async (filePath: string) => { return mockFiles.has(filePath); }), mkdir: jest.fn().mockResolvedValue(undefined), readdir: jest.fn().mockImplementation(async (dirPath: string) => { const files = Array.from(mockFiles.keys()) .filter(path => path.startsWith(dirPath)) .map(path => path.replace(dirPath + '/', '')); return [...new Set(files)]; }), stat: jest.fn().mockImplementation(async (filePath: string) => { if (!mockFiles.has(filePath)) { throw new Error(`ENOENT: no such file or directory, stat '${filePath}'`); } return { isFile: () => true, isDirectory: () => false, size: mockFiles.get(filePath)?.length || 0, mtime: new Date() }; }), // Helper for tests _setMockFile: (filePath: string, content: string) => { mockFiles.set(filePath, content); }, _clearMockFiles: () => { mockFiles.clear(); } }; } }

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