@@ -339,6 +339,135 @@ public function testHealthEndpointPublishesCompatibilityAlertFactsWhenFailClosed
339339 $ this ->assertStringContainsString ('0 supporting workers ' , (string ) ($ alert ['details ' ] ?? '' ));
340340 }
341341
342+ public function testHealthEndpointPublishesRoutingHealthAlertFactsForWorkersPanel (): void
343+ {
344+ Carbon::setTestNow ('2026-04-09 12:00:00 ' );
345+ $ this ->beforeApplicationDestroyed (static function (): void {
346+ Carbon::setTestNow ();
347+ });
348+
349+ config ()->set ('queue.default ' , 'redis ' );
350+ config ()->set ('queue.connections.redis.driver ' , 'redis ' );
351+ config ()->set ('cache.default ' , 'file ' );
352+ config ()->set ('waterline.namespace ' , 'waterline-routing-health ' );
353+ config ()->set ('workflows.v2.matching_role.queue_wake_enabled ' , false );
354+ config ()->set ('workflows.v2.task_dispatch_mode ' , 'poll ' );
355+
356+ $ this ->createWorkerRegistrationsTable ();
357+
358+ WorkerRegistration::create ([
359+ 'worker_id ' => 'routing-health-poller ' ,
360+ 'namespace ' => 'waterline-routing-health ' ,
361+ 'task_queue ' => 'default ' ,
362+ 'runtime ' => 'php ' ,
363+ 'sdk_version ' => '1.0.0 ' ,
364+ 'build_id ' => 'build-routing-health ' ,
365+ 'supported_workflow_types ' => ['workflow.test ' ],
366+ 'supported_activity_types ' => ['activity.test ' ],
367+ 'max_concurrent_workflow_tasks ' => 8 ,
368+ 'max_concurrent_activity_tasks ' => 4 ,
369+ 'last_heartbeat_at ' => now ()->subSeconds (15 ),
370+ 'status ' => 'active ' ,
371+ ]);
372+
373+ $ instance = WorkflowInstance::query ()->create ([
374+ 'id ' => 'waterline-routing-health-instance ' ,
375+ 'workflow_class ' => 'WorkflowClass ' ,
376+ 'workflow_type ' => 'workflow.test ' ,
377+ 'run_count ' => 1 ,
378+ ]);
379+
380+ $ run = WorkflowRun::query ()->create ([
381+ 'id ' => '01JHWATERLINEROUTINGRUN01 ' ,
382+ 'workflow_instance_id ' => $ instance ->id ,
383+ 'run_number ' => 1 ,
384+ 'namespace ' => 'waterline-routing-health ' ,
385+ 'workflow_class ' => 'WorkflowClass ' ,
386+ 'workflow_type ' => 'workflow.test ' ,
387+ 'status ' => 'running ' ,
388+ 'started_at ' => now ()->subMinutes (12 ),
389+ 'last_progress_at ' => now ()->subMinute (),
390+ ]);
391+
392+ $ instance ->forceFill ([
393+ 'current_run_id ' => $ run ->id ,
394+ ])->save ();
395+
396+ WorkflowRunSummary::query ()->create ([
397+ 'id ' => $ run ->id ,
398+ 'workflow_instance_id ' => $ instance ->id ,
399+ 'run_number ' => 1 ,
400+ 'is_current_run ' => true ,
401+ 'engine_source ' => 'v2 ' ,
402+ 'class ' => 'WorkflowClass ' ,
403+ 'workflow_type ' => 'workflow.test ' ,
404+ 'status ' => 'running ' ,
405+ 'status_bucket ' => 'running ' ,
406+ 'namespace ' => 'waterline-routing-health ' ,
407+ 'started_at ' => now ()->subMinutes (12 ),
408+ 'next_task_at ' => now ()->subMinutes (7 ),
409+ 'liveness_state ' => 'workflow_task_waiting_for_compatible_worker ' ,
410+ 'liveness_reason ' => 'No active worker heartbeat advertises the required compatibility marker. ' ,
411+ 'created_at ' => now ()->subMinutes (12 ),
412+ 'updated_at ' => now (),
413+ ]);
414+
415+ WorkflowTask::query ()->create ([
416+ 'id ' => '01JHWATERLINEROUTINGTASK0001 ' ,
417+ 'workflow_run_id ' => $ run ->id ,
418+ 'namespace ' => 'waterline-routing-health ' ,
419+ 'task_type ' => TaskType::Workflow->value ,
420+ 'status ' => TaskStatus::Ready->value ,
421+ 'connection ' => 'redis ' ,
422+ 'queue ' => 'default ' ,
423+ 'available_at ' => now ()->subMinutes (4 ),
424+ 'last_dispatched_at ' => now ()->subMinutes (4 ),
425+ 'created_at ' => now ()->subMinutes (4 ),
426+ 'updated_at ' => now (),
427+ ]);
428+
429+ WorkflowTask::query ()->create ([
430+ 'id ' => '01JHWATERLINEROUTINGTASK0002 ' ,
431+ 'workflow_run_id ' => $ run ->id ,
432+ 'namespace ' => 'waterline-routing-health ' ,
433+ 'task_type ' => TaskType::Workflow->value ,
434+ 'status ' => TaskStatus::Ready->value ,
435+ 'connection ' => 'redis ' ,
436+ 'queue ' => 'default ' ,
437+ 'available_at ' => now ()->subSeconds (90 ),
438+ 'last_claim_failed_at ' => now ()->subSeconds (90 ),
439+ 'last_claim_error ' => 'Previous claim attempt failed before lease grant. ' ,
440+ 'created_at ' => now ()->subSeconds (90 ),
441+ 'updated_at ' => now (),
442+ ]);
443+
444+ $ payload = $ this ->get ('/waterline/api/v2/health ' )
445+ ->assertStatus (200 )
446+ ->json ();
447+
448+ $ alert = $ this ->coordinationAlertByKey ($ payload , 'routing_health ' );
449+ $ this ->assertNotNull ($ alert );
450+ $ this ->assertSame ('health_check ' , $ alert ['source ' ]);
451+ $ this ->assertSame ('warning ' , $ alert ['status ' ]);
452+ $ this ->assertSame ('correctness ' , $ alert ['category ' ]);
453+ $ this ->assertSame (1 , $ alert ['facts ' ]['compatibility_blocked_runs ' ] ?? null );
454+ $ this ->assertSame (1 , $ alert ['facts ' ]['dispatch_overdue_tasks ' ] ?? null );
455+ $ this ->assertSame (1 , $ alert ['facts ' ]['claim_failed_tasks ' ] ?? null );
456+ $ this ->assertSame (7 * 60 * 1000 , $ alert ['facts ' ]['max_compatibility_blocked_age_ms ' ] ?? null );
457+ $ this ->assertSame (4 * 60 * 1000 , $ alert ['facts ' ]['max_dispatch_overdue_age_ms ' ] ?? null );
458+ $ this ->assertSame (90 * 1000 , $ alert ['facts ' ]['max_claim_failed_age_ms ' ] ?? null );
459+ $ this ->assertFalse ($ alert ['facts ' ]['queue_wake_enabled ' ] ?? true );
460+ $ this ->assertSame ('dedicated ' , $ alert ['facts ' ]['matching_shape ' ] ?? null );
461+ $ this ->assertSame ('poll ' , $ alert ['facts ' ]['task_dispatch_mode ' ] ?? null );
462+ $ this ->assertSame (0 , $ alert ['facts ' ]['active_worker_scopes ' ] ?? null );
463+ $ this ->assertStringContainsString ('compatibility-blocked run ' , (string ) ($ alert ['details ' ] ?? '' ));
464+ $ this ->assertStringContainsString ('dispatch-overdue task ' , (string ) ($ alert ['details ' ] ?? '' ));
465+ $ this ->assertStringContainsString ('claim-failed task ' , (string ) ($ alert ['details ' ] ?? '' ));
466+ $ this ->assertStringContainsString ('matching role dedicated in poll mode ' , (string ) ($ alert ['details ' ] ?? '' ));
467+ $ this ->assertStringContainsString ('queue wake disabled ' , (string ) ($ alert ['details ' ] ?? '' ));
468+ $ this ->assertStringContainsString ('worst-case age 7m00s ' , (string ) ($ alert ['details ' ] ?? '' ));
469+ }
470+
342471 public function testHealthEndpointReturnsUnavailableForBlockingBackendIssues (): void
343472 {
344473 config ()->set ('queue.default ' , 'sync ' );
0 commit comments