mockConnection = new class () { private array $data = []; private int $lastInsertId = 0; private int $rowCount = 0; public function prepare(string $sql): object { return new class ($sql, $this) { public function __construct( private string $sql, private object $connection ) { } public function execute(array $params = []): bool { // Simulate different SQL operations if (str_contains($this->sql, 'INSERT INTO queue_workers')) { $this->connection->rowCount = 1; $this->connection->data['workers'][$params['id']] = $params; } elseif (str_contains($this->sql, 'UPDATE queue_workers')) { if (str_contains($this->sql, 'is_active = 0')) { // Deregister operation if (isset($this->connection->data['workers'][$params['id']])) { $this->connection->data['workers'][$params['id']]['is_active'] = 0; $this->connection->rowCount = 1; } } else { // Heartbeat update if (isset($this->connection->data['workers'][$params['id']])) { $worker = &$this->connection->data['workers'][$params['id']]; $worker['cpu_usage'] = $params['cpu_usage']; $worker['memory_usage_bytes'] = $params['memory_usage_bytes']; $worker['current_jobs'] = $params['current_jobs']; $worker['last_heartbeat'] = date('Y-m-d H:i:s'); $this->connection->rowCount = 1; } } } elseif (str_contains($this->sql, 'INSERT INTO worker_health_checks')) { $this->connection->data['health_checks'][] = $params; $this->connection->rowCount = 1; } return true; } public function fetch(): array|false { if (str_contains($this->sql, 'SELECT * FROM queue_workers WHERE id = :id')) { $id = func_get_args()[0]['id'] ?? null; return $this->connection->data['workers'][$id] ?? false; } if (str_contains($this->sql, 'SELECT * FROM queue_workers') && str_contains($this->sql, 'is_active = 1')) { // Return first active worker for testing foreach ($this->connection->data['workers'] ?? [] as $worker) { if ($worker['is_active']) { return $worker; } } return false; } // Statistics query if (str_contains($this->sql, 'COUNT(*) as total_workers')) { return [ 'total_workers' => count($this->connection->data['workers'] ?? []), 'active_workers' => count(array_filter($this->connection->data['workers'] ?? [], fn ($w) => $w['is_active'])), 'healthy_workers' => count(array_filter($this->connection->data['workers'] ?? [], fn ($w) => $w['is_active'])), 'total_capacity' => 100, 'current_load' => 50, 'avg_cpu_usage' => 25.5, 'avg_memory_usage' => 1024 * 1024 * 512, // 512MB 'unique_hosts' => 2, ]; } return false; } public function fetchAll(): array { if (str_contains($this->sql, 'worker_health_checks') && str_contains($this->sql, 'GROUP BY')) { return [ [ 'check_date' => '2024-01-01', 'check_hour' => '14', 'avg_score' => 85.5, 'total_checks' => 10, 'healthy_count' => 8, 'warning_count' => 2, 'critical_count' => 0, ], ]; } return []; } public function rowCount(): int { return $this->connection->rowCount; } }; } public function setWorkerData(array $workers): void { $this->data['workers'] = $workers; } }; // Mock logger for testing $this->mockLogger = new class () { public array $logs = []; public function info(string $message, array $context = []): void { $this->logs[] = ['level' => 'info', 'message' => $message, 'context' => $context]; } public function debug(string $message, array $context = []): void { $this->logs[] = ['level' => 'debug', 'message' => $message, 'context' => $context]; } public function error(string $message, array $context = []): void { $this->logs[] = ['level' => 'error', 'message' => $message, 'context' => $context]; } public function warning(string $message, array $context = []): void { $this->logs[] = ['level' => 'warning', 'message' => $message, 'context' => $context]; } }; $this->workerRegistry = new WorkerRegistry($this->mockConnection, $this->mockLogger); $this->healthCheckService = new WorkerHealthCheckService($this->workerRegistry, $this->mockConnection, $this->mockLogger); }); describe('WorkerId Value Object', function () { it('can generate unique worker IDs', function () { $id1 = WorkerId::generate(); $id2 = WorkerId::generate(); expect($id1->toString())->not()->toBe($id2->toString()); expect($id1->getValue())->not()->toBeEmpty(); }); it('can create deterministic IDs for host and process', function () { $id1 = WorkerId::forHost('localhost', 1234); $id2 = WorkerId::forHost('localhost', 1234); expect($id1->toString())->toBe($id2->toString()); expect($id1->getValue())->toHaveLength(16); }); it('can create from string and validate', function () { $original = 'worker_123_test'; $id = WorkerId::fromString($original); expect($id->toString())->toBe($original); expect($id->getValue())->toBe($original); expect((string) $id)->toBe($original); }); it('throws exception for empty worker ID', function () { expect(fn () => WorkerId::fromString('')) ->toThrow(\InvalidArgumentException::class, 'WorkerId cannot be empty'); }); it('can compare worker IDs for equality', function () { $id1 = WorkerId::fromString('worker_123'); $id2 = WorkerId::fromString('worker_123'); $id3 = WorkerId::fromString('worker_456'); expect($id1->equals($id2))->toBeTrue(); expect($id1->equals($id3))->toBeFalse(); }); it('supports JSON serialization', function () { $id = WorkerId::fromString('worker_test_123'); $json = json_encode($id); expect($json)->toBe('"worker_test_123"'); }); }); describe('Worker Entity', function () { it('can register a new worker with valid parameters', function () { $queues = [QueueName::default(), QueueName::high()]; $capabilities = ['pdf_processing', 'email_sending']; $worker = Worker::register( hostname: 'worker-01', processId: 1234, queues: $queues, maxJobs: 5, capabilities: $capabilities ); expect($worker->hostname)->toBe('worker-01'); expect($worker->processId)->toBe(1234); expect($worker->queues)->toBe($queues); expect($worker->maxJobs)->toBe(5); expect($worker->isActive)->toBeTrue(); expect($worker->capabilities)->toBe($capabilities); expect($worker->currentJobs)->toBe(0); expect($worker->version)->toBe('1.0.0'); expect($worker->registeredAt)->toBeInstanceOf(\DateTimeImmutable::class); expect($worker->lastHeartbeat)->toBeInstanceOf(\DateTimeImmutable::class); }); it('throws exception when no queues provided', function () { expect(fn () => new Worker( id: WorkerId::generate(), hostname: 'test', processId: 1234, queues: [], // Empty queues maxJobs: 5, registeredAt: new \DateTimeImmutable() ))->toThrow(\InvalidArgumentException::class, 'Worker must handle at least one queue'); }); it('validates job constraints during construction', function () { $baseWorker = fn ($maxJobs, $currentJobs) => new Worker( id: WorkerId::generate(), hostname: 'test', processId: 1234, queues: [QueueName::default()], maxJobs: $maxJobs, registeredAt: new \DateTimeImmutable(), currentJobs: $currentJobs ); // Invalid max jobs expect(fn () => $baseWorker(0, 0)) ->toThrow(\InvalidArgumentException::class, 'Max jobs must be greater than 0'); expect(fn () => $baseWorker(-1, 0)) ->toThrow(\InvalidArgumentException::class, 'Max jobs must be greater than 0'); // Invalid current jobs expect(fn () => $baseWorker(5, -1)) ->toThrow(\InvalidArgumentException::class, 'Current jobs cannot be negative'); expect(fn () => $baseWorker(5, 10)) ->toThrow(\InvalidArgumentException::class, 'Current jobs cannot exceed max jobs'); }); it('can update heartbeat with new metrics', function () { $worker = Worker::register('worker-01', 1234, [QueueName::default()]); $newCpuUsage = new Percentage(75.5); $newMemoryUsage = Byte::fromMegabytes(512); $newCurrentJobs = 3; $updatedWorker = $worker->updateHeartbeat($newCpuUsage, $newMemoryUsage, $newCurrentJobs); expect($updatedWorker->cpuUsage)->toBe($newCpuUsage); expect($updatedWorker->memoryUsage)->toBe($newMemoryUsage); expect($updatedWorker->currentJobs)->toBe($newCurrentJobs); expect($updatedWorker->isActive)->toBeTrue(); expect($updatedWorker->lastHeartbeat->getTimestamp())->toBeGreaterThan($worker->lastHeartbeat->getTimestamp()); }); it('can mark worker as inactive', function () { $worker = Worker::register('worker-01', 1234, [QueueName::default()]); $inactiveWorker = $worker->markInactive(); expect($inactiveWorker->isActive)->toBeFalse(); expect($inactiveWorker->id)->toBe($worker->id); expect($inactiveWorker->hostname)->toBe($worker->hostname); }); it('correctly determines worker availability for jobs', function () { $queues = [QueueName::default()]; // Healthy active worker with capacity $healthyWorker = new Worker( id: WorkerId::generate(), hostname: 'healthy-worker', processId: 1234, queues: $queues, maxJobs: 5, registeredAt: new \DateTimeImmutable(), lastHeartbeat: new \DateTimeImmutable(), isActive: true, cpuUsage: new Percentage(50), memoryUsage: Byte::fromMegabytes(500), currentJobs: 2 ); expect($healthyWorker->isAvailableForJobs())->toBeTrue(); // Inactive worker $inactiveWorker = $healthyWorker->markInactive(); expect($inactiveWorker->isAvailableForJobs())->toBeFalse(); // Worker at max capacity $maxCapacityWorker = new Worker( id: WorkerId::generate(), hostname: 'maxed-worker', processId: 1234, queues: $queues, maxJobs: 5, registeredAt: new \DateTimeImmutable(), lastHeartbeat: new \DateTimeImmutable(), isActive: true, currentJobs: 5 // At max capacity ); expect($maxCapacityWorker->isAvailableForJobs())->toBeFalse(); // Unhealthy worker (high CPU) $unhealthyWorker = new Worker( id: WorkerId::generate(), hostname: 'unhealthy-worker', processId: 1234, queues: $queues, maxJobs: 5, registeredAt: new \DateTimeImmutable(), lastHeartbeat: new \DateTimeImmutable(), isActive: true, cpuUsage: new Percentage(95), // Too high currentJobs: 2 ); expect($unhealthyWorker->isAvailableForJobs())->toBeFalse(); }); it('can check queue handling capability', function () { $defaultQueue = QueueName::default(); $highQueue = QueueName::high(); $lowQueue = QueueName::low(); $worker = Worker::register('worker-01', 1234, [$defaultQueue, $highQueue]); expect($worker->handlesQueue($defaultQueue))->toBeTrue(); expect($worker->handlesQueue($highQueue))->toBeTrue(); expect($worker->handlesQueue($lowQueue))->toBeFalse(); }); it('determines health status based on multiple factors', function () { $queues = [QueueName::default()]; // Healthy worker $healthyWorker = new Worker( id: WorkerId::generate(), hostname: 'healthy', processId: 1234, queues: $queues, maxJobs: 5, registeredAt: new \DateTimeImmutable(), lastHeartbeat: new \DateTimeImmutable(), // Recent heartbeat isActive: true, cpuUsage: new Percentage(50), memoryUsage: Byte::fromMegabytes(500) // 500MB < 2GB limit ); expect($healthyWorker->isHealthy())->toBeTrue(); // Worker with old heartbeat $staleWorker = new Worker( id: WorkerId::generate(), hostname: 'stale', processId: 1234, queues: $queues, maxJobs: 5, registeredAt: new \DateTimeImmutable(), lastHeartbeat: new \DateTimeImmutable('-2 minutes'), // Too old isActive: true ); expect($staleWorker->isHealthy())->toBeFalse(); // Worker with high CPU $highCpuWorker = new Worker( id: WorkerId::generate(), hostname: 'high-cpu', processId: 1234, queues: $queues, maxJobs: 5, registeredAt: new \DateTimeImmutable(), lastHeartbeat: new \DateTimeImmutable(), isActive: true, cpuUsage: new Percentage(95) // Too high ); expect($highCpuWorker->isHealthy())->toBeFalse(); // Worker with high memory $highMemoryWorker = new Worker( id: WorkerId::generate(), hostname: 'high-memory', processId: 1234, queues: $queues, maxJobs: 5, registeredAt: new \DateTimeImmutable(), lastHeartbeat: new \DateTimeImmutable(), isActive: true, memoryUsage: Byte::fromGigabytes(3) // > 2GB limit ); expect($highMemoryWorker->isHealthy())->toBeFalse(); }); it('calculates load percentage correctly', function () { $queues = [QueueName::default()]; // Job load higher than CPU load $jobLoadWorker = new Worker( id: WorkerId::generate(), hostname: 'job-load', processId: 1234, queues: $queues, maxJobs: 10, registeredAt: new \DateTimeImmutable(), currentJobs: 8, // 80% job load cpuUsage: new Percentage(30) // 30% CPU load ); expect($jobLoadWorker->getLoadPercentage()->getValue())->toBe(80.0); // CPU load higher than job load $cpuLoadWorker = new Worker( id: WorkerId::generate(), hostname: 'cpu-load', processId: 1234, queues: $queues, maxJobs: 10, registeredAt: new \DateTimeImmutable(), currentJobs: 3, // 30% job load cpuUsage: new Percentage(70) // 70% CPU load ); expect($cpuLoadWorker->getLoadPercentage()->getValue())->toBe(70.0); // Zero max jobs edge case $zeroMaxWorker = new Worker( id: WorkerId::generate(), hostname: 'zero-max', processId: 1234, queues: $queues, maxJobs: 1, registeredAt: new \DateTimeImmutable(), currentJobs: 1 // 100% job load ); expect($zeroMaxWorker->getLoadPercentage()->getValue())->toBe(100.0); }); it('can check capabilities', function () { $worker = Worker::register('worker-01', 1234, [QueueName::default()], 5, ['pdf', 'email', 'resize']); expect($worker->hasCapability('pdf'))->toBeTrue(); expect($worker->hasCapability('email'))->toBeTrue(); expect($worker->hasCapability('video'))->toBeFalse(); }); it('can convert to monitoring array format', function () { $worker = Worker::register('worker-01', 1234, [QueueName::default()], 5, ['pdf']); $monitoring = $worker->toMonitoringArray(); expect($monitoring)->toHaveKey('id'); expect($monitoring)->toHaveKey('hostname'); expect($monitoring)->toHaveKey('process_id'); expect($monitoring)->toHaveKey('queues'); expect($monitoring)->toHaveKey('is_healthy'); expect($monitoring)->toHaveKey('is_available'); expect($monitoring)->toHaveKey('load_percentage'); expect($monitoring)->toHaveKey('capabilities'); expect($monitoring['hostname'])->toBe('worker-01'); expect($monitoring['process_id'])->toBe(1234); expect($monitoring['capabilities'])->toBe(['pdf']); }); it('can serialize to and from array', function () { $queues = [QueueName::default(), QueueName::high()]; $capabilities = ['pdf', 'email']; $worker = Worker::register('worker-01', 1234, $queues, 5, $capabilities); $array = $worker->toArray(); expect($array)->toHaveKey('id'); expect($array)->toHaveKey('hostname'); expect($array)->toHaveKey('queues'); expect($array)->toHaveKey('capabilities'); // Test that queues and capabilities are JSON encoded expect($array['queues'])->toBeString(); expect($array['capabilities'])->toBeString(); // Note: fromArray() is simplified in the implementation // In real testing, you'd want to test full serialization/deserialization $restoredWorker = Worker::fromArray($array); expect($restoredWorker->hostname)->toBe('worker-01'); expect($restoredWorker->processId)->toBe(1234); }); }); describe('WorkerRegistry Service', function () { it('can register a worker successfully', function () { $worker = Worker::register('test-host', 1234, [QueueName::default()]); $this->workerRegistry->register($worker); expect($this->mockLogger->logs)->toContain([ 'level' => 'info', 'message' => 'Registering worker', 'context' => [ 'worker_id' => $worker->id->toString(), 'hostname' => 'test-host', 'process_id' => 1234, 'queues' => [QueueName::default()], 'max_jobs' => 10, ], ]); expect($this->mockLogger->logs)->toContain([ 'level' => 'debug', 'message' => 'Worker registered successfully', 'context' => ['worker_id' => $worker->id->toString()], ]); }); it('can deregister a worker', function () { $workerId = WorkerId::generate(); // Setup worker data in mock $this->mockConnection->setWorkerData([ $workerId->toString() => [ 'id' => $workerId->toString(), 'is_active' => 1, ], ]); $this->workerRegistry->deregister($workerId); expect($this->mockLogger->logs)->toContain([ 'level' => 'info', 'message' => 'Deregistering worker', 'context' => ['worker_id' => $workerId->toString()], ]); }); it('can update worker heartbeat', function () { $workerId = WorkerId::generate(); $cpuUsage = new Percentage(45.5); $memoryUsage = Byte::fromMegabytes(256); $currentJobs = 3; // Setup worker data in mock $this->mockConnection->setWorkerData([ $workerId->toString() => [ 'id' => $workerId->toString(), 'is_active' => 1, ], ]); $this->workerRegistry->updateHeartbeat($workerId, $cpuUsage, $memoryUsage, $currentJobs); // Should not log warnings when worker found and updated $warningLogs = array_filter($this->mockLogger->logs, fn ($log) => $log['level'] === 'warning'); expect($warningLogs)->toBeEmpty(); }); it('logs warning when heartbeat update fails for non-existent worker', function () { $workerId = WorkerId::generate(); $cpuUsage = new Percentage(50); $memoryUsage = Byte::fromMegabytes(512); // No worker data setup - worker doesn't exist $this->workerRegistry->updateHeartbeat($workerId, $cpuUsage, $memoryUsage, 2); $warningLogs = array_filter($this->mockLogger->logs, fn ($log) => $log['level'] === 'warning'); expect($warningLogs)->not()->toBeEmpty(); }); it('can find worker by ID', function () { $workerId = WorkerId::generate(); $workerData = [ 'id' => $workerId->toString(), 'hostname' => 'test-worker', 'process_id' => 1234, 'queues' => json_encode(['default']), 'max_jobs' => 5, 'current_jobs' => 2, 'is_active' => 1, 'cpu_usage' => 50, 'memory_usage_bytes' => 512 * 1024 * 1024, 'registered_at' => '2024-01-01 12:00:00', 'last_heartbeat' => '2024-01-01 12:05:00', 'capabilities' => json_encode(['pdf']), 'version' => '1.0.0', ]; $this->mockConnection->setWorkerData([$workerId->toString() => $workerData]); $foundWorker = $this->workerRegistry->findById($workerId); expect($foundWorker)->toBeInstanceOf(Worker::class); expect($foundWorker->hostname)->toBe('test-worker'); expect($foundWorker->processId)->toBe(1234); }); it('returns null when worker not found by ID', function () { $workerId = WorkerId::generate(); $foundWorker = $this->workerRegistry->findById($workerId); expect($foundWorker)->toBeNull(); }); it('can get worker statistics', function () { $stats = $this->workerRegistry->getWorkerStatistics(); expect($stats)->toHaveKey('total_workers'); expect($stats)->toHaveKey('active_workers'); expect($stats)->toHaveKey('healthy_workers'); expect($stats)->toHaveKey('total_capacity'); expect($stats)->toHaveKey('current_load'); expect($stats)->toHaveKey('capacity_utilization'); expect($stats)->toHaveKey('avg_cpu_usage'); expect($stats)->toHaveKey('avg_memory_usage_mb'); expect($stats)->toHaveKey('unique_hosts'); expect($stats)->toHaveKey('queue_distribution'); expect($stats['capacity_utilization'])->toBe(50.0); expect($stats['avg_memory_usage_mb'])->toBe(512.0); }); it('can cleanup inactive workers', function () { $cleanedCount = $this->workerRegistry->cleanupInactiveWorkers(5); expect($cleanedCount)->toBeInt(); expect($this->mockLogger->logs)->toContain([ 'level' => 'info', 'message' => 'Starting cleanup of inactive workers', 'context' => ['inactive_minutes' => 5], ]); }); }); describe('WorkerHealthCheckService', function () { it('can perform health check on individual worker', function () { $worker = new Worker( id: WorkerId::generate(), hostname: 'test-worker', processId: 1234, queues: [QueueName::default()], maxJobs: 10, registeredAt: new \DateTimeImmutable(), lastHeartbeat: new \DateTimeImmutable(), isActive: true, cpuUsage: new Percentage(45), memoryUsage: Byte::fromMegabytes(800), currentJobs: 5 ); $health = $this->healthCheckService->checkWorkerHealth($worker); expect($health)->toHaveKey('worker_id'); expect($health)->toHaveKey('hostname'); expect($health)->toHaveKey('status'); expect($health)->toHaveKey('score'); expect($health)->toHaveKey('metrics'); expect($health)->toHaveKey('issues'); expect($health)->toHaveKey('warnings'); expect($health)->toHaveKey('checked_at'); expect($health['worker_id'])->toBe($worker->id->toString()); expect($health['hostname'])->toBe('test-worker'); expect($health['status'])->toBe('healthy'); expect($health['score'])->toBeGreaterThanOrEqual(80); expect($health['issues'])->toBeEmpty(); }); it('detects critical health issues', function () { $criticalWorker = new Worker( id: WorkerId::generate(), hostname: 'critical-worker', processId: 1234, queues: [QueueName::default()], maxJobs: 5, registeredAt: new \DateTimeImmutable(), lastHeartbeat: new \DateTimeImmutable('-5 minutes'), // Very old heartbeat isActive: true, cpuUsage: new Percentage(95), // Critical CPU memoryUsage: Byte::fromGigabytes(3), // Critical memory currentJobs: 5 // Overloaded ); $health = $this->healthCheckService->checkWorkerHealth($criticalWorker); expect($health['status'])->toBe('critical'); expect($health['score'])->toBeLessThan(50); expect($health['issues'])->not()->toBeEmpty(); expect($health['issues'])->toContain('No heartbeat for 300 seconds'); expect($health['issues'])->toContain('Critical CPU usage: 95%'); expect($health['issues'])->toContain('Critical memory usage: 3.000GB'); expect($health['issues'])->toContain('Worker overloaded: 95%'); }); it('detects warning conditions', function () { $warningWorker = new Worker( id: WorkerId::generate(), hostname: 'warning-worker', processId: 1234, queues: [QueueName::default()], maxJobs: 10, registeredAt: new \DateTimeImmutable(), lastHeartbeat: new \DateTimeImmutable('-70 seconds'), // Slightly delayed isActive: true, cpuUsage: new Percentage(80), // Warning level CPU memoryUsage: Byte::fromGigabytes(1.8), // Warning level memory currentJobs: 9 // High load ); $health = $this->healthCheckService->checkWorkerHealth($warningWorker); expect($health['status'])->toBe('warning'); expect($health['score'])->toBeGreaterThan(30); expect($health['score'])->toBeLessThan(80); expect($health['warnings'])->not()->toBeEmpty(); expect($health['warnings'])->toContain('Heartbeat delayed (70s)'); expect($health['warnings'])->toContain('High CPU usage: 80%'); expect($health['warnings'])->toContain('High memory usage: 1.800GB'); expect($health['warnings'])->toContain('High worker load: 90%'); }); it('handles inactive workers correctly', function () { $inactiveWorker = new Worker( id: WorkerId::generate(), hostname: 'inactive-worker', processId: 1234, queues: [QueueName::default()], maxJobs: 5, registeredAt: new \DateTimeImmutable(), isActive: false // Inactive ); $health = $this->healthCheckService->checkWorkerHealth($inactiveWorker); expect($health['status'])->toBe('critical'); expect($health['score'])->toBe(0); expect($health['issues'])->toContain('Worker marked as inactive'); }); it('can generate system health report', function () { // Setup some test workers in the mock $this->mockConnection->setWorkerData([ 'worker1' => [ 'id' => 'worker1', 'hostname' => 'test1', 'process_id' => 1234, 'queues' => json_encode(['default']), 'max_jobs' => 5, 'current_jobs' => 2, 'is_active' => 1, 'cpu_usage' => 50, 'memory_usage_bytes' => 512 * 1024 * 1024, 'registered_at' => '2024-01-01 12:00:00', 'last_heartbeat' => date('Y-m-d H:i:s'), 'capabilities' => json_encode([]), 'version' => '1.0.0', ], ]); $report = $this->healthCheckService->generateSystemHealthReport(); expect($report)->toHaveKey('current_health'); expect($report)->toHaveKey('trends_24h'); expect($report)->toHaveKey('top_issues_24h'); expect($report)->toHaveKey('generated_at'); expect($report['current_health'])->toHaveKey('workers'); expect($report['current_health'])->toHaveKey('overall'); }); it('can cleanup old health check records', function () { $deletedCount = $this->healthCheckService->cleanupHealthChecks(Duration::fromDays(7)); expect($deletedCount)->toBeInt(); expect($this->mockLogger->logs)->toContain([ 'level' => 'info', 'message' => 'Health check cleanup completed', 'context' => [ 'deleted_records' => $deletedCount, 'retention_days' => 7.0, ], ]); }); }); describe('Integration Testing', function () { it('can perform full worker lifecycle management', function () { // 1. Register worker $worker = Worker::register('integration-host', 9999, [QueueName::default(), QueueName::high()], 8, ['pdf']); $this->workerRegistry->register($worker); // 2. Update heartbeat $this->workerRegistry->updateHeartbeat( $worker->id, new Percentage(65), Byte::fromMegabytes(1024), 3 ); // 3. Perform health check $health = $this->healthCheckService->checkWorkerHealth($worker); expect($health['status'])->toBeIn(['healthy', 'warning']); // 4. Find worker $foundWorker = $this->workerRegistry->findById($worker->id); expect($foundWorker)->not()->toBeNull(); // 5. Get statistics $stats = $this->workerRegistry->getWorkerStatistics(); expect($stats['total_workers'])->toBeGreaterThanOrEqual(1); // 6. Deregister worker $this->workerRegistry->deregister($worker->id); // Verify all operations logged appropriately $logCount = count($this->mockLogger->logs); expect($logCount)->toBeGreaterThan(0); }); it('handles edge cases and error conditions gracefully', function () { // Test with non-existent worker ID $nonExistentId = WorkerId::generate(); $foundWorker = $this->workerRegistry->findById($nonExistentId); expect($foundWorker)->toBeNull(); // Test heartbeat update for non-existent worker $this->workerRegistry->updateHeartbeat( $nonExistentId, new Percentage(50), Byte::fromMegabytes(512), 1 ); // Should log warning $warningLogs = array_filter($this->mockLogger->logs, fn ($log) => $log['level'] === 'warning'); expect($warningLogs)->not()->toBeEmpty(); }); it('maintains data consistency across operations', function () { $worker = Worker::register('consistency-test', 7777, [QueueName::default()]); // Test immutability - operations should return new instances $originalId = $worker->id; $originalHostname = $worker->hostname; $updatedWorker = $worker->updateHeartbeat( new Percentage(30), Byte::fromMegabytes(256), 1 ); // Original worker unchanged expect($worker->id)->toBe($originalId); expect($worker->hostname)->toBe($originalHostname); expect($worker->currentJobs)->toBe(0); // Updated worker has new values expect($updatedWorker->id)->toBe($originalId); // Same ID expect($updatedWorker->hostname)->toBe($originalHostname); // Same hostname expect($updatedWorker->currentJobs)->toBe(1); // Updated jobs expect($updatedWorker->cpuUsage->getValue())->toBe(30.0); $inactiveWorker = $updatedWorker->markInactive(); expect($updatedWorker->isActive)->toBeTrue(); // Original still active expect($inactiveWorker->isActive)->toBeFalse(); // New instance inactive }); }); });