data[$identifier->toString()])) { $items[] = $this->data[$identifier->toString()]; } else { $items[] = CacheItem::miss($identifier instanceof CacheKey ? $identifier : CacheKey::fromString($identifier->toString())); } } return CacheResult::fromItems(...$items); } public function set(CacheItem ...$items): bool { foreach ($items as $item) { $this->data[$item->key->toString()] = $item; } return true; } public function has(CacheIdentifier ...$identifiers): array { $result = []; foreach ($identifiers as $identifier) { $key = (string) $identifier; $result[$key] = isset($this->data[$key]); } return $result; } public function forget(CacheIdentifier ...$identifiers): bool { foreach ($identifiers as $identifier) { $key = (string) $identifier; unset($this->data[$key]); } return true; } public function clear(): bool { $this->data = []; return true; } public function remember(CacheKey $key, callable $callback, ?Duration $ttl = null): CacheItem { $keyStr = $key->toString(); if (isset($this->data[$keyStr])) { return $this->data[$keyStr]; } $value = $callback(); $item = CacheItem::forSetting($key, $value, $ttl); $this->data[$keyStr] = $item; return $item; } }; } function createTestClock(): Clock { return new class implements Clock { public function now(): \DateTimeImmutable { return new \DateTimeImmutable(); } public function fromTimestamp(Timestamp $timestamp): \DateTimeImmutable { return $timestamp->toDateTime(); } public function fromString(string $dateTime, ?string $format = null): \DateTimeImmutable { return new \DateTimeImmutable($dateTime); } public function today(): \DateTimeImmutable { return new \DateTimeImmutable('today'); } public function yesterday(): \DateTimeImmutable { return new \DateTimeImmutable('yesterday'); } public function tomorrow(): \DateTimeImmutable { return new \DateTimeImmutable('tomorrow'); } public function time(): Timestamp { return Timestamp::now(); } }; } describe('ErrorHandler ErrorAggregator Integration', function () { it('processes ErrorHandlerContext through ErrorAggregator', function () { // Setup ErrorAggregator $storage = new InMemoryErrorStorage(); $cache = createTestCache(); $clock = createTestClock(); $alertQueue = new InMemoryQueue(); $errorAggregator = new ErrorAggregator( storage: $storage, cache: $cache, clock: $clock, alertQueue: $alertQueue ); // Create ErrorHandlerContext (simulates what ErrorHandler does) $exception = FrameworkException::create( DatabaseErrorCode::QUERY_FAILED, 'Test database query failed' ); $exceptionContext = ExceptionContext::empty() ->withOperation('test_operation', 'TestComponent') ->withData([ 'test_key' => 'test_value', 'original_exception' => $exception, // Store exception for message extraction 'exception_message' => $exception->getMessage() ]); $requestContext = RequestContext::fromGlobals(); $systemContext = SystemContext::current(); $errorHandlerContext = ErrorHandlerContext::create( $exceptionContext, $requestContext, $systemContext, ['http_status' => 500] ); // Process through ErrorAggregator (simulates ErrorHandler calling it) $errorAggregator->processError($errorHandlerContext); // Verify error was stored $recentEvents = $errorAggregator->getRecentEvents(10); expect($recentEvents)->toHaveCount(1); $event = $recentEvents[0]; expect($event->errorMessage)->toBe('Test database query failed'); expect($event->severity)->toBe(ErrorSeverity::ERROR); expect($event->component)->toBe('TestComponent'); expect($event->operation)->toBe('test_operation'); // Verify pattern was created $activePatterns = $errorAggregator->getActivePatterns(10); expect($activePatterns)->toHaveCount(1); $pattern = $activePatterns[0]; expect($pattern->occurrenceCount)->toBe(1); expect($pattern->severity)->toBe(ErrorSeverity::ERROR); expect($pattern->component)->toBe('TestComponent'); }); it('creates error patterns from multiple identical errors', function () { $storage = new InMemoryErrorStorage(); $cache = createTestCache(); $clock = createTestClock(); $alertQueue = new InMemoryQueue(); $errorAggregator = new ErrorAggregator( storage: $storage, cache: $cache, clock: $clock, alertQueue: $alertQueue ); // Process same error 3 times for ($i = 0; $i < 3; $i++) { $exception = FrameworkException::create( DatabaseErrorCode::QUERY_FAILED, 'Repeated database error' ); $exceptionContext = ExceptionContext::empty() ->withOperation('query_execution', 'DatabaseManager') ->withData([ 'query' => 'SELECT * FROM users', 'original_exception' => $exception, 'exception_message' => $exception->getMessage() ]); $requestContext = RequestContext::fromGlobals(); $systemContext = SystemContext::current(); $errorHandlerContext = ErrorHandlerContext::create( $exceptionContext, $requestContext, $systemContext, ['http_status' => 500] ); $errorAggregator->processError($errorHandlerContext); } // Verify all 3 events were stored $recentEvents = $errorAggregator->getRecentEvents(10); expect($recentEvents)->toHaveCount(3); // Verify single pattern with 3 occurrences (same fingerprint) $activePatterns = $errorAggregator->getActivePatterns(10); expect($activePatterns)->toHaveCount(1); $pattern = $activePatterns[0]; expect($pattern->occurrenceCount)->toBe(3); expect($pattern->component)->toBe('DatabaseManager'); expect($pattern->operation)->toBe('query_execution'); }); it('handles ErrorAggregator being null (optional dependency)', function () { $errorAggregator = null; // Simulate the nullable call pattern used in ErrorHandler $exception = FrameworkException::create( DatabaseErrorCode::QUERY_FAILED, 'Test error' ); $exceptionContext = ExceptionContext::empty(); $requestContext = RequestContext::fromGlobals(); $systemContext = SystemContext::current(); $errorHandlerContext = ErrorHandlerContext::create( $exceptionContext, $requestContext, $systemContext, [] ); // This should not throw an error $errorAggregator?->processError($errorHandlerContext); // If we get here, the null-safe operator worked correctly expect(true)->toBeTrue(); }); it('processes errors with different severities correctly', function () { $storage = new InMemoryErrorStorage(); $cache = createTestCache(); $clock = createTestClock(); $alertQueue = new InMemoryQueue(); $errorAggregator = new ErrorAggregator( storage: $storage, cache: $cache, clock: $clock, alertQueue: $alertQueue ); // Process errors with different severities $errorCodes = [ SystemErrorCode::RESOURCE_EXHAUSTED, // CRITICAL DatabaseErrorCode::QUERY_FAILED, // ERROR CacheErrorCode::READ_FAILED, // WARNING ]; foreach ($errorCodes as $errorCode) { $exception = FrameworkException::create($errorCode, 'Test message'); $exceptionContext = ExceptionContext::empty() ->withOperation('test_op', 'TestComponent') ->withData([ 'original_exception' => $exception, 'exception_message' => $exception->getMessage() ]); $requestContext = RequestContext::fromGlobals(); $systemContext = SystemContext::current(); $errorHandlerContext = ErrorHandlerContext::create( $exceptionContext, $requestContext, $systemContext, [] ); $errorAggregator->processError($errorHandlerContext); } // Verify all events were stored $recentEvents = $errorAggregator->getRecentEvents(10); expect($recentEvents)->toHaveCount(3); // Verify patterns reflect correct severities $activePatterns = $errorAggregator->getActivePatterns(10); expect($activePatterns)->toHaveCount(3); $severities = array_map(fn($p) => $p->severity, $activePatterns); expect($severities)->toContain(ErrorSeverity::CRITICAL); expect($severities)->toContain(ErrorSeverity::ERROR); expect($severities)->toContain(ErrorSeverity::WARNING); }); });