/** * Agentic QE v3 - Performance Profiler Service Unit Tests */ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { PerformanceProfilerService } from '../../../../src/domains/chaos-resilience/services/performance-profiler'; import { MemoryBackend, StoreOptions } from '../../../../src/kernel/interfaces'; import { FaultType, CircuitBreakerTestOptions, } from '../../../../src/domains/chaos-resilience/interfaces'; /** * Mock MemoryBackend for testing */ function createMockMemory(): MemoryBackend { const store = new Map(); return { async initialize() {}, async dispose() {}, async set(key: string, value: T, _options?: StoreOptions): Promise { store.set(key, value); }, async get(key: string): Promise { return store.get(key) as T | undefined; }, async delete(key: string): Promise { return store.delete(key); }, async has(key: string): Promise { return store.has(key); }, async search(_pattern: string, _limit?: number): Promise { return []; }, async vectorSearch(_embedding: number[], _k: number): Promise<[]> { return []; }, async storeVector(_key: string, _embedding: number[], _metadata?: unknown): Promise {}, }; } describe('PerformanceProfilerService', () => { let service: PerformanceProfilerService; let mockMemory: MemoryBackend; beforeEach(() => { mockMemory = createMockMemory(); // Use shorter timeouts for faster tests service = new PerformanceProfilerService(mockMemory, { defaultTimeout: 3000, healthCheckInterval: 100, recoveryCheckDelay: 50, }); }); describe('testRecovery', () => { it('should test service recovery and return result', async () => { const result = await service.testRecovery( 'api-service', 'latency', 5000 // Expected recovery within 5 seconds ); expect(result.success).toBe(true); if (result.success) { expect(result.value.service).toBe('api-service'); expect(result.value.faultType).toBe('latency'); expect(result.value.expectedRecoveryTime).toBe(5000); expect(typeof result.value.passed).toBe('boolean'); expect(result.value.timeline.length).toBeGreaterThan(0); } }); it('should track recovery timeline events', async () => { const result = await service.testRecovery( 'api-service', 'process-kill', 10000 ); expect(result.success).toBe(true); if (result.success) { // Should have initial health check const initialEvent = result.value.timeline.find( e => e.event.includes('Initial health check') ); expect(initialEvent).toBeDefined(); // Should have fault injection event const faultEvent = result.value.timeline.find( e => e.event.includes('Injecting fault') ); expect(faultEvent).toBeDefined(); } }); it('should test with different fault types', async () => { const faultTypes: FaultType[] = [ 'latency', 'error', 'timeout', 'cpu-stress', 'memory-stress', ]; for (const faultType of faultTypes) { const result = await service.testRecovery('test-service', faultType, 5000); expect(result.success).toBe(true); if (result.success) { expect(result.value.faultType).toBe(faultType); } } }); it('should fail if initial health check fails', async () => { // Create service with simulateRandomFailures enabled to test failure scenarios const failureService = new PerformanceProfilerService(mockMemory, { defaultTimeout: 3000, healthCheckInterval: 100, recoveryCheckDelay: 50, simulateRandomFailures: true, // Enable random failures for this test }); // Mock Math.random to force health check failure const originalRandom = Math.random; Math.random = () => 0.01; // Will make health check fail (< 0.05) try { const result = await failureService.testRecovery('unhealthy-service', 'latency', 5000); expect(result.success).toBe(true); if (result.success) { expect(result.value.passed).toBe(false); expect(result.value.timeline[0].status).toBe('unhealthy'); } } finally { Math.random = originalRandom; } }); it('should record recovery time', async () => { const result = await service.testRecovery('api-service', 'latency', 10000); expect(result.success).toBe(true); if (result.success) { expect(result.value.recoveryTime).toBeDefined(); expect(result.value.recoveryTime).toBeGreaterThanOrEqual(0); } }); }); describe('testFailover', () => { it('should test failover between primary and secondary', async () => { const result = await service.testFailover('primary-db', 'secondary-db'); expect(result.success).toBe(true); if (result.success) { expect(result.value.primaryService).toBe('primary-db'); expect(result.value.secondaryService).toBe('secondary-db'); expect(typeof result.value.failoverTime).toBe('number'); expect(typeof result.value.dataLoss).toBe('boolean'); expect(result.value.steps.length).toBeGreaterThan(0); } }); it('should track failover steps in order', async () => { const result = await service.testFailover('primary', 'secondary'); expect(result.success).toBe(true); if (result.success) { // Verify steps are ordered for (let i = 0; i < result.value.steps.length - 1; i++) { expect(result.value.steps[i].order).toBeLessThan( result.value.steps[i + 1].order ); } } }); it('should verify health of both services', async () => { const result = await service.testFailover('primary', 'secondary'); expect(result.success).toBe(true); if (result.success) { const healthSteps = result.value.steps.filter( s => s.action.includes('health') ); expect(healthSteps.length).toBeGreaterThanOrEqual(2); } }); it('should capture service state before failover', async () => { const result = await service.testFailover('primary', 'secondary'); expect(result.success).toBe(true); if (result.success) { // When both services are healthy, state capture is performed if (result.value.steps.length > 3) { const captureStep = result.value.steps.find( s => s.action.includes('Capture') && s.action.includes('state') ); expect(captureStep).toBeDefined(); } } }); it('should verify data integrity after failover', async () => { const result = await service.testFailover('primary', 'secondary'); expect(result.success).toBe(true); if (result.success) { // When both services are healthy, data integrity check is performed // If services are unhealthy, the test exits early if (result.value.steps.length > 3) { const integrityStep = result.value.steps.find( s => s.action.toLowerCase().includes('data integrity') ); expect(integrityStep).toBeDefined(); } } }); it('should restore primary after test', async () => { const result = await service.testFailover('primary', 'secondary'); expect(result.success).toBe(true); if (result.success) { // Verify steps are present - the restore step is the last one when health checks pass expect(result.value.steps.length).toBeGreaterThan(0); // Check that each step has required properties result.value.steps.forEach(step => { expect(step.order).toBeDefined(); expect(step.action).toBeDefined(); expect(typeof step.duration).toBe('number'); expect(typeof step.success).toBe('boolean'); }); } }); it('should report failover time', async () => { const result = await service.testFailover('primary', 'secondary'); expect(result.success).toBe(true); if (result.success) { expect(result.value.failoverTime).toBeGreaterThanOrEqual(0); } }); it('should detect data loss', async () => { const result = await service.testFailover('primary', 'secondary'); expect(result.success).toBe(true); if (result.success) { expect(typeof result.value.dataLoss).toBe('boolean'); } }); }); describe('testCircuitBreaker', () => { it('should test circuit breaker behavior', async () => { const result = await service.testCircuitBreaker('api-service'); expect(result.success).toBe(true); if (result.success) { expect(result.value.service).toBe('api-service'); expect(typeof result.value.opened).toBe('boolean'); expect(typeof result.value.openedAfterErrors).toBe('number'); expect(typeof result.value.closedAfterRecovery).toBe('boolean'); expect(['correct', 'incorrect']).toContain(result.value.halfOpenBehavior); expect(typeof result.value.passed).toBe('boolean'); } }); it('should accept custom options', async () => { // Use fast service with short timeouts const fastService = new PerformanceProfilerService(mockMemory, { defaultTimeout: 2000, healthCheckInterval: 100, }); const options: CircuitBreakerTestOptions = { errorThreshold: 3, timeout: 2000, halfOpenRequests: 2, }; const result = await fastService.testCircuitBreaker('api-service', options); expect(result.success).toBe(true); }); it('should use default options when none provided', async () => { const result = await service.testCircuitBreaker('api-service'); expect(result.success).toBe(true); }); it('should track errors needed to open circuit', async () => { const result = await service.testCircuitBreaker('api-service', { errorThreshold: 5, }); expect(result.success).toBe(true); if (result.success) { expect(result.value.openedAfterErrors).toBeGreaterThanOrEqual(0); } }); it('should test half-open behavior', async () => { const result = await service.testCircuitBreaker('api-service', { halfOpenRequests: 3, }); expect(result.success).toBe(true); if (result.success) { expect(result.value.halfOpenBehavior).toBeDefined(); } }); it('should verify circuit can close after recovery', async () => { const result = await service.testCircuitBreaker('api-service'); expect(result.success).toBe(true); if (result.success) { expect(typeof result.value.closedAfterRecovery).toBe('boolean'); } }); }); describe('testRateLimiting', () => { it('should test rate limiting behavior', async () => { const expectedLimit = 100; const result = await service.testRateLimiting('api-service', expectedLimit); expect(result.success).toBe(true); if (result.success) { expect(result.value.service).toBe('api-service'); expect(result.value.expectedLimit).toBe(expectedLimit); expect(typeof result.value.actualLimit).toBe('number'); expect(typeof result.value.passed).toBe('boolean'); expect(result.value.responseWhenLimited).toBeDefined(); } }); it('should detect actual rate limit', async () => { const result = await service.testRateLimiting('api-service', 50); expect(result.success).toBe(true); if (result.success) { expect(result.value.actualLimit).toBeGreaterThan(0); } }); it('should capture rate limit response', async () => { const result = await service.testRateLimiting('api-service', 50); expect(result.success).toBe(true); if (result.success) { const response = result.value.responseWhenLimited; expect(response.statusCode).toBeDefined(); // When rate limited, status should be 429 if (result.value.passed) { expect(response.statusCode).toBe(429); } } }); it('should report retryAfter when available', async () => { const result = await service.testRateLimiting('api-service', 50); expect(result.success).toBe(true); if (result.success && result.value.responseWhenLimited.statusCode === 429) { // retryAfter is included in the simulated 429 response expect(result.value.responseWhenLimited.retryAfter).toBeDefined(); } }); it('should validate rate limit within tolerance', async () => { const result = await service.testRateLimiting('api-service', 100); expect(result.success).toBe(true); if (result.success) { // The pass/fail should consider tolerance expect(typeof result.value.passed).toBe('boolean'); } }); it('should test with different expected limits', async () => { const limits = [10, 50, 100, 200]; for (const limit of limits) { const result = await service.testRateLimiting('api-service', limit); expect(result.success).toBe(true); if (result.success) { expect(result.value.expectedLimit).toBe(limit); } } }); }); describe('configuration', () => { it('should use default configuration when none provided', () => { const defaultService = new PerformanceProfilerService(mockMemory); expect(defaultService).toBeDefined(); }); it('should merge provided configuration with defaults', () => { const customService = new PerformanceProfilerService(mockMemory, { defaultTimeout: 120000, healthCheckInterval: 500, maxRetries: 5, }); expect(customService).toBeDefined(); }); it('should respect custom timeout', async () => { const fastService = new PerformanceProfilerService(mockMemory, { defaultTimeout: 1000, healthCheckInterval: 100, recoveryCheckDelay: 50, }); const result = await fastService.testRecovery('api-service', 'latency', 500); expect(result.success).toBe(true); }); }); describe('error handling', () => { it('should handle recovery test errors gracefully', async () => { // Service should handle internal errors const result = await service.testRecovery('api-service', 'latency', 5000); expect(result.success).toBe(true); }); it('should handle failover test errors gracefully', async () => { const result = await service.testFailover('primary', 'secondary'); expect(result.success).toBe(true); }); it('should handle circuit breaker test errors gracefully', async () => { const result = await service.testCircuitBreaker('api-service'); expect(result.success).toBe(true); }); it('should handle rate limit test errors gracefully', async () => { const result = await service.testRateLimiting('api-service', 100); expect(result.success).toBe(true); }); }); describe('result storage', () => { it('should store recovery test results in memory', async () => { await service.testRecovery('api-service', 'latency', 5000); // Verify result was stored (check memory has resilience key) const keys = await mockMemory.search('resilience:recovery:*'); // Our mock doesn't implement search, but the store should have been called }); it('should store failover test results in memory', async () => { await service.testFailover('primary', 'secondary'); // Result should be stored }); it('should store circuit breaker test results in memory', async () => { await service.testCircuitBreaker('api-service'); // Result should be stored }); it('should store rate limit test results in memory', async () => { await service.testRateLimiting('api-service', 100); // Result should be stored }); }); });