workerRegistry = Mockery::mock(WorkerRegistry::class); $this->componentId = ComponentId::create('worker-health', 'test'); $this->initialState = WorkerHealthState::empty(); }); afterEach(function () { Mockery::close(); }); it('polls and updates state with worker health data', function () { $now = Timestamp::now(); $oneMinuteAgo = $now->sub(Duration::fromSeconds(60)); $worker1 = new Worker( id: WorkerId::generate(), hostname: 'server-01', processId: 12345, startedAt: $oneMinuteAgo, lastHeartbeat: $now, status: 'active', currentJobs: 2, maxJobs: 10, memoryUsageMb: 128.5, cpuUsage: 45.2 ); $worker2 = new Worker( id: WorkerId::generate(), hostname: 'server-02', processId: 67890, startedAt: $oneMinuteAgo, lastHeartbeat: $now->sub(Duration::fromMinutes(5)), // Unhealthy - old heartbeat status: 'active', currentJobs: 0, maxJobs: 10, memoryUsageMb: 64.0, cpuUsage: 98.5 // Unhealthy - high CPU ); $this->workerRegistry->shouldReceive('findActiveWorkers') ->once() ->andReturn([$worker1, $worker2]); $component = new WorkerHealthComponent( id: $this->componentId, state: $this->initialState, workerRegistry: $this->workerRegistry ); $newState = $component->poll(); expect($newState)->toBeInstanceOf(WorkerHealthState::class); expect($newState->activeWorkers)->toBe(2); expect($newState->totalWorkers)->toBe(2); expect($newState->jobsInProgress)->toBe(2); // Only worker1 has jobs expect($newState->workerDetails)->toHaveCount(2); // Worker 1 should be healthy expect($newState->workerDetails[0]['healthy'])->toBeTrue(); expect($newState->workerDetails[0]['hostname'])->toBe('server-01'); // Worker 2 should be unhealthy expect($newState->workerDetails[1]['healthy'])->toBeFalse(); }); it('identifies healthy workers correctly', function () { $now = Timestamp::now(); $healthyWorker = new Worker( id: WorkerId::generate(), hostname: 'healthy-server', processId: 11111, startedAt: $now->sub(Duration::fromMinutes(5)), lastHeartbeat: $now->sub(Duration::fromSeconds(30)), // Recent heartbeat status: 'active', currentJobs: 5, maxJobs: 10, memoryUsageMb: 100.0, cpuUsage: 50.0 // Normal CPU ); $this->workerRegistry->shouldReceive('findActiveWorkers') ->once() ->andReturn([$healthyWorker]); $component = new WorkerHealthComponent( id: $this->componentId, state: $this->initialState, workerRegistry: $this->workerRegistry ); $newState = $component->poll(); expect($newState->workerDetails[0]['healthy'])->toBeTrue(); }); it('identifies unhealthy workers by stale heartbeat', function () { $now = Timestamp::now(); $staleWorker = new Worker( id: WorkerId::generate(), hostname: 'stale-server', processId: 22222, startedAt: $now->sub(Duration::fromMinutes(10)), lastHeartbeat: $now->sub(Duration::fromMinutes(3)), // Stale heartbeat status: 'active', currentJobs: 2, maxJobs: 10, memoryUsageMb: 80.0, cpuUsage: 30.0 ); $this->workerRegistry->shouldReceive('findActiveWorkers') ->once() ->andReturn([$staleWorker]); $component = new WorkerHealthComponent( id: $this->componentId, state: $this->initialState, workerRegistry: $this->workerRegistry ); $newState = $component->poll(); expect($newState->workerDetails[0]['healthy'])->toBeFalse(); }); it('identifies unhealthy workers by high CPU', function () { $now = Timestamp::now(); $highCpuWorker = new Worker( id: WorkerId::generate(), hostname: 'high-cpu-server', processId: 33333, startedAt: $now->sub(Duration::fromMinutes(5)), lastHeartbeat: $now, // Recent heartbeat status: 'active', currentJobs: 8, maxJobs: 10, memoryUsageMb: 200.0, cpuUsage: 96.5 // High CPU ); $this->workerRegistry->shouldReceive('findActiveWorkers') ->once() ->andReturn([$highCpuWorker]); $component = new WorkerHealthComponent( id: $this->componentId, state: $this->initialState, workerRegistry: $this->workerRegistry ); $newState = $component->poll(); expect($newState->workerDetails[0]['healthy'])->toBeFalse(); }); it('has correct poll interval', function () { $component = new WorkerHealthComponent( id: $this->componentId, state: $this->initialState, workerRegistry: $this->workerRegistry ); expect($component->getPollInterval())->toBe(5000); }); it('returns correct render data', function () { $workerDetails = [ [ 'id' => 'worker-1', 'hostname' => 'server-01', 'healthy' => true, 'jobs' => 5, ], ]; $state = new WorkerHealthState( activeWorkers: 1, totalWorkers: 1, jobsInProgress: 5, workerDetails: $workerDetails, lastUpdated: '2024-01-15 12:00:00' ); $component = new WorkerHealthComponent( id: $this->componentId, state: $state, workerRegistry: $this->workerRegistry ); $renderData = $component->getRenderData(); expect($renderData->templatePath)->toBe('livecomponent-worker-health'); expect($renderData->data)->toHaveKey('componentId'); expect($renderData->data)->toHaveKey('pollInterval'); expect($renderData->data['pollInterval'])->toBe(5000); expect($renderData->data['activeWorkers'])->toBe(1); expect($renderData->data['jobsInProgress'])->toBe(5); }); it('handles no active workers', function () { $this->workerRegistry->shouldReceive('findActiveWorkers') ->once() ->andReturn([]); $component = new WorkerHealthComponent( id: $this->componentId, state: $this->initialState, workerRegistry: $this->workerRegistry ); $newState = $component->poll(); expect($newState->activeWorkers)->toBe(0); expect($newState->totalWorkers)->toBe(0); expect($newState->jobsInProgress)->toBe(0); expect($newState->workerDetails)->toBe([]); }); });