workerId = WorkerId::generate(); $this->queues = [ QueueName::defaultQueue(), QueueName::emailQueue(), ]; $this->capabilities = ['email', 'pdf-generation', 'image-processing']; }); it('can register a new worker with valid parameters', function () { $worker = Worker::register( hostname: 'app-server-1', processId: 1001, queues: $this->queues, maxJobs: 10, capabilities: $this->capabilities ); expect($worker->hostname)->toBe('app-server-1'); expect($worker->processId)->toBe(1001); expect($worker->queues)->toHaveCount(2); expect($worker->maxJobs)->toBe(10); expect($worker->currentJobs)->toBe(0); expect($worker->isActive)->toBeTrue(); expect($worker->capabilities)->toBe($this->capabilities); expect($worker->registeredAt)->toBeInstanceOf(\DateTimeImmutable::class); expect($worker->lastHeartbeat)->toBeInstanceOf(\DateTimeImmutable::class); }); it('validates worker construction constraints', function () { // Empty queues expect(fn () => Worker::register( hostname: 'test-host', processId: 1001, queues: [], // Invalid maxJobs: 10 ))->toThrow(\InvalidArgumentException::class, 'Worker must handle at least one queue'); // Invalid max jobs expect(fn () => Worker::register( hostname: 'test-host', processId: 1001, queues: $this->queues, maxJobs: 0 // Invalid ))->toThrow(\InvalidArgumentException::class, 'Max jobs must be greater than 0'); expect(fn () => Worker::register( hostname: 'test-host', processId: 1001, queues: $this->queues, maxJobs: -5 // Invalid ))->toThrow(\InvalidArgumentException::class, 'Max jobs must be greater than 0'); }); it('validates current jobs constraints', function () { $worker = Worker::register( hostname: 'test-host', processId: 1001, queues: $this->queues, maxJobs: 5 ); // Negative current jobs should fail during construction expect(fn () => new Worker( id: $worker->id, hostname: $worker->hostname, processId: $worker->processId, queues: $worker->queues, maxJobs: $worker->maxJobs, registeredAt: $worker->registeredAt, currentJobs: -1 // Invalid ))->toThrow(\InvalidArgumentException::class, 'Current jobs cannot be negative'); // Current jobs exceeding max jobs should fail expect(fn () => new Worker( id: $worker->id, hostname: $worker->hostname, processId: $worker->processId, queues: $worker->queues, maxJobs: $worker->maxJobs, registeredAt: $worker->registeredAt, currentJobs: 10 // Exceeds maxJobs of 5 ))->toThrow(\InvalidArgumentException::class, 'Current jobs cannot exceed max jobs'); }); it('can update heartbeat with resource usage', function () { $worker = Worker::register( hostname: 'test-host', processId: 1001, queues: $this->queues, maxJobs: 10 ); $updatedWorker = $worker->updateHeartbeat( cpuUsage: new Percentage(45), memoryUsage: Byte::fromMegabytes(800), currentJobs: 3 ); expect($updatedWorker->cpuUsage->getValue())->toBe(45.0); expect($updatedWorker->memoryUsage->toMegabytes())->toBe(800.0); expect($updatedWorker->currentJobs)->toBe(3); expect($updatedWorker->isActive)->toBeTrue(); expect($updatedWorker->lastHeartbeat)->toBeInstanceOf(\DateTimeImmutable::class); // Original worker should be unchanged (immutable) expect($worker->cpuUsage->getValue())->toBe(0.0); expect($worker->currentJobs)->toBe(0); }); it('can mark worker as inactive', function () { $worker = Worker::register( hostname: 'test-host', processId: 1001, queues: $this->queues, maxJobs: 10 ); $inactiveWorker = $worker->markInactive(); expect($inactiveWorker->isActive)->toBeFalse(); expect($worker->isActive)->toBeTrue(); // Original unchanged }); it('correctly determines worker availability', function () { $worker = Worker::register( hostname: 'test-host', processId: 1001, queues: $this->queues, maxJobs: 5 ); // Fresh worker should be available expect($worker->isAvailableForJobs())->toBeTrue(); // Worker at capacity should not be available $atCapacityWorker = $worker->updateHeartbeat( new Percentage(30), Byte::fromMegabytes(500), 5 // At max capacity ); expect($atCapacityWorker->isAvailableForJobs())->toBeFalse(); // Inactive worker should not be available $inactiveWorker = $worker->markInactive(); expect($inactiveWorker->isAvailableForJobs())->toBeFalse(); // Unhealthy worker should not be available $unhealthyWorker = $worker->updateHeartbeat( new Percentage(95), // Critical CPU Byte::fromGigabytes(3), // Over memory limit 2 ); expect($unhealthyWorker->isAvailableForJobs())->toBeFalse(); }); it('can check if worker handles specific queues', function () { $worker = Worker::register( hostname: 'test-host', processId: 1001, queues: [ QueueName::defaultQueue(), QueueName::emailQueue(), ], maxJobs: 10 ); expect($worker->handlesQueue(QueueName::defaultQueue()))->toBeTrue(); expect($worker->handlesQueue(QueueName::emailQueue()))->toBeTrue(); expect($worker->handlesQueue(QueueName::fromString('unknown-queue')))->toBeFalse(); }); it('correctly determines worker health status', function () { $worker = Worker::register( hostname: 'test-host', processId: 1001, queues: $this->queues, maxJobs: 10 ); // Fresh worker should be healthy expect($worker->isHealthy())->toBeTrue(); // Worker with high CPU should be unhealthy $highCpuWorker = $worker->updateHeartbeat( new Percentage(95), // Over 90% threshold Byte::fromMegabytes(500), 3 ); expect($highCpuWorker->isHealthy())->toBeFalse(); // Worker with excessive memory should be unhealthy $highMemoryWorker = $worker->updateHeartbeat( new Percentage(30), Byte::fromGigabytes(3), // Over 2GB threshold 3 ); expect($highMemoryWorker->isHealthy())->toBeFalse(); // Inactive worker should be unhealthy $inactiveWorker = $worker->markInactive(); expect($inactiveWorker->isHealthy())->toBeFalse(); // Worker with stale heartbeat should be unhealthy $staleWorker = new Worker( id: $worker->id, hostname: $worker->hostname, processId: $worker->processId, queues: $worker->queues, maxJobs: $worker->maxJobs, registeredAt: $worker->registeredAt, lastHeartbeat: new \DateTimeImmutable('-2 minutes'), // Stale isActive: true, cpuUsage: new Percentage(30), memoryUsage: Byte::fromMegabytes(500), currentJobs: 3 ); expect($staleWorker->isHealthy())->toBeFalse(); // Worker with no heartbeat should be unhealthy $noHeartbeatWorker = new Worker( id: $worker->id, hostname: $worker->hostname, processId: $worker->processId, queues: $worker->queues, maxJobs: $worker->maxJobs, registeredAt: $worker->registeredAt, lastHeartbeat: null, // No heartbeat isActive: true ); expect($noHeartbeatWorker->isHealthy())->toBeFalse(); }); it('calculates load percentage correctly', function () { $worker = Worker::register( hostname: 'test-host', processId: 1001, queues: $this->queues, maxJobs: 10 ); // Test job-based load $jobLoadWorker = $worker->updateHeartbeat( new Percentage(20), // 20% CPU Byte::fromMegabytes(500), 3 // 3/10 = 30% job load ); expect($jobLoadWorker->getLoadPercentage()->getValue())->toBe(30.0); // Higher of 20% CPU or 30% jobs // Test CPU-based load $cpuLoadWorker = $worker->updateHeartbeat( new Percentage(75), // 75% CPU Byte::fromMegabytes(500), 2 // 2/10 = 20% job load ); expect($cpuLoadWorker->getLoadPercentage()->getValue())->toBe(75.0); // Higher of 75% CPU or 20% jobs // Test worker with zero max jobs $zeroJobsWorker = new Worker( id: $worker->id, hostname: $worker->hostname, processId: $worker->processId, queues: $worker->queues, maxJobs: 0, // Special case registeredAt: $worker->registeredAt, lastHeartbeat: new \DateTimeImmutable(), isActive: true, currentJobs: 0 ); expect($zeroJobsWorker->getLoadPercentage()->getValue())->toBe(100.0); }); it('can check worker capabilities', function () { $worker = Worker::register( hostname: 'test-host', processId: 1001, queues: $this->queues, maxJobs: 10, capabilities: ['email', 'pdf-generation', 'image-processing'] ); expect($worker->hasCapability('email'))->toBeTrue(); expect($worker->hasCapability('pdf-generation'))->toBeTrue(); expect($worker->hasCapability('image-processing'))->toBeTrue(); expect($worker->hasCapability('video-processing'))->toBeFalse(); expect($worker->hasCapability(''))->toBeFalse(); }); it('provides comprehensive monitoring data', function () { $worker = Worker::register( hostname: 'test-host', processId: 1001, queues: $this->queues, maxJobs: 10, capabilities: $this->capabilities )->updateHeartbeat( new Percentage(45), Byte::fromMegabytes(800), 3 ); $monitoringData = $worker->toMonitoringArray(); expect($monitoringData)->toHaveKey('id'); expect($monitoringData)->toHaveKey('hostname'); expect($monitoringData)->toHaveKey('process_id'); expect($monitoringData)->toHaveKey('queues'); expect($monitoringData)->toHaveKey('max_jobs'); expect($monitoringData)->toHaveKey('current_jobs'); expect($monitoringData)->toHaveKey('is_active'); expect($monitoringData)->toHaveKey('is_healthy'); expect($monitoringData)->toHaveKey('is_available'); expect($monitoringData)->toHaveKey('load_percentage'); expect($monitoringData)->toHaveKey('cpu_usage'); expect($monitoringData)->toHaveKey('memory_usage_mb'); expect($monitoringData)->toHaveKey('capabilities'); expect($monitoringData['hostname'])->toBe('test-host'); expect($monitoringData['process_id'])->toBe(1001); expect($monitoringData['max_jobs'])->toBe(10); expect($monitoringData['current_jobs'])->toBe(3); expect($monitoringData['is_active'])->toBeTrue(); expect($monitoringData['load_percentage'])->toBe(45.0); expect($monitoringData['cpu_usage'])->toBe(45.0); expect($monitoringData['memory_usage_mb'])->toBe(800.0); expect($monitoringData['capabilities'])->toBe($this->capabilities); }); it('can be serialized to array for persistence', function () { $worker = Worker::register( hostname: 'test-host', processId: 1001, queues: $this->queues, maxJobs: 10, capabilities: $this->capabilities ); $array = $worker->toArray(); expect($array)->toHaveKey('id'); expect($array)->toHaveKey('hostname'); expect($array)->toHaveKey('process_id'); expect($array)->toHaveKey('queues'); expect($array)->toHaveKey('max_jobs'); expect($array)->toHaveKey('current_jobs'); expect($array)->toHaveKey('is_active'); expect($array)->toHaveKey('cpu_usage'); expect($array)->toHaveKey('memory_usage_bytes'); expect($array)->toHaveKey('registered_at'); expect($array)->toHaveKey('last_heartbeat'); expect($array)->toHaveKey('capabilities'); expect($array)->toHaveKey('version'); // Queues should be JSON encoded $queues = json_decode($array['queues'], true); expect($queues)->toBeArray(); expect($queues)->toHaveCount(2); // Capabilities should be JSON encoded $capabilities = json_decode($array['capabilities'], true); expect($capabilities)->toBe($this->capabilities); }); it('can be reconstructed from array data', function () { $originalWorker = Worker::register( hostname: 'test-host', processId: 1001, queues: $this->queues, maxJobs: 10, capabilities: $this->capabilities ); $array = $originalWorker->toArray(); $reconstructedWorker = Worker::fromArray($array); expect($reconstructedWorker->hostname)->toBe($originalWorker->hostname); expect($reconstructedWorker->processId)->toBe($originalWorker->processId); expect($reconstructedWorker->maxJobs)->toBe($originalWorker->maxJobs); expect($reconstructedWorker->currentJobs)->toBe($originalWorker->currentJobs); expect($reconstructedWorker->isActive)->toBe($originalWorker->isActive); expect($reconstructedWorker->capabilities)->toBe($originalWorker->capabilities); expect($reconstructedWorker->version)->toBe($originalWorker->version); }); it('handles edge cases in array reconstruction', function () { $minimalData = [ 'id' => 'test-worker-id', 'hostname' => 'test-host', 'process_id' => 1001, 'queues' => '["default"]', 'max_jobs' => 5, 'registered_at' => '2024-01-01 12:00:00', 'is_active' => 1, ]; $worker = Worker::fromArray($minimalData); expect($worker->hostname)->toBe('test-host'); expect($worker->processId)->toBe(1001); expect($worker->maxJobs)->toBe(5); expect($worker->currentJobs)->toBe(0); // Default value expect($worker->isActive)->toBeTrue(); expect($worker->lastHeartbeat)->toBeNull(); expect($worker->cpuUsage->getValue())->toBe(0.0); expect($worker->memoryUsage->toBytes())->toBe(0); expect($worker->capabilities)->toBe([]); expect($worker->version)->toBe('1.0.0'); }); });