@@ -291,6 +291,60 @@ async def noop_scheduler_loop():
291291 os .environ ["CRON_SCHEDULE" ] = previous_cron
292292
293293
294+ def test_metrics_endpoint_prometheus_gauges ():
295+ """Metrics endpoint should expose Prometheus gauges for guide/scheduler state."""
296+ from app import server
297+
298+ previous_cron = os .environ .get ("CRON_SCHEDULE" )
299+ original_scheduler_loop = server .scheduler_loop
300+
301+ async def noop_scheduler_loop ():
302+ return None
303+
304+ try :
305+ os .environ ["CRON_SCHEDULE" ] = "0 3 * * *"
306+ server .scheduler_loop = noop_scheduler_loop
307+
308+ server .M3U_PATH .parent .mkdir (parents = True , exist_ok = True )
309+ server .M3U_PATH .write_text ("#EXTM3U\n #EXTINF:-1,Toonami\n http://example.com/stream\n " )
310+ server .XML_PATH .write_text ("<tv></tv>\n " )
311+ now_ts = time .time ()
312+ os .utime (server .M3U_PATH , (now_ts , now_ts ))
313+ os .utime (server .XML_PATH , (now_ts , now_ts ))
314+ server .write_state (
315+ {
316+ "last_update" : datetime .now (UTC ).isoformat (),
317+ "consecutive_failures" : 2 ,
318+ }
319+ )
320+
321+ with TestClient (app ) as client :
322+ response = client .get ("/metrics" )
323+ assert response .status_code == 200
324+ assert "text/plain" in response .headers .get ("content-type" , "" )
325+
326+ metric_values = {}
327+ for line in response .text .splitlines ():
328+ if not line or line .startswith ("#" ):
329+ continue
330+ name , value = line .split (" " , 1 )
331+ metric_values [name ] = float (value .strip ())
332+
333+ assert "downlink_last_update_age_seconds" in metric_values
334+ assert metric_values ["downlink_last_update_age_seconds" ] >= 0.0
335+ assert metric_values ["downlink_last_update_age_seconds" ] < 600.0
336+ assert metric_values ["downlink_guide_stale" ] == 0.0
337+ assert metric_values ["downlink_scheduler_consecutive_failures" ] == 2.0
338+ assert metric_values ["downlink_cron_supported" ] == 1.0
339+ finally :
340+ server .scheduler_loop = original_scheduler_loop
341+ if previous_cron is None :
342+ os .environ .pop ("CRON_SCHEDULE" , None )
343+ else :
344+ os .environ ["CRON_SCHEDULE" ] = previous_cron
345+ print ("✅ Metrics endpoint exposes expected Prometheus gauges" )
346+
347+
294348def test_record_generation_failure_updates_state ():
295349 """Failure recorder should persist normalized error metadata."""
296350 from app import server
@@ -420,6 +474,7 @@ def main():
420474 test_lan_refresh_host_detection ()
421475 test_cron_next_respects_dom_mon_dow ()
422476 test_status_reports_cron_and_failure_diagnostics ()
477+ test_metrics_endpoint_prometheus_gauges ()
423478 test_record_generation_failure_updates_state ()
424479 test_health_reports_scheduler_failure_state ()
425480 test_health_reports_stale_freshness ()
0 commit comments