Skip to content

Commit 4260017

Browse files
web-flowclaude
andcommitted
test(frigate-health-checker): update tests for simplified FPS-based health check
Remove tests for legacy checks (slow inference, stuck detection, backlog) Add tests for new camera FPS check: - test_check_health_no_frames: fails when camera_fps < 1 - test_check_health_skipped_camera_no_frames: passes when skipped camera has no frames - test_no_frames_triggers_restart: integration test for restart workflow - test_healthy_with_all_cameras_working: integration test for healthy state Update fixtures to use camera_fps field (not fps) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 80c4af6 commit 4260017

3 files changed

Lines changed: 64 additions & 80 deletions

File tree

apps/frigate-health-checker/tests/conftest.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -101,25 +101,28 @@ def frigate_stats_healthy() -> dict:
101101
return {
102102
"detectors": {
103103
"coral": {
104-
"inference_speed": 45.5,
104+
"inference_speed": 15.0,
105105
"pid": 123,
106106
}
107107
},
108108
"cameras": {
109-
"front_door": {"fps": 30, "detection_fps": 5},
109+
"front_door": {"camera_fps": 5.0, "detection_fps": 0.2},
110+
"back_yard": {"camera_fps": 5.0, "detection_fps": 0.1},
110111
},
111112
}
112113

113114

114115
@pytest.fixture
115-
def frigate_stats_slow_inference() -> dict:
116-
"""Create Frigate stats with slow inference."""
116+
def frigate_stats_no_frames() -> dict:
117+
"""Create Frigate stats with camera having no frames."""
117118
return {
118119
"detectors": {
119120
"coral": {
120-
"inference_speed": 250.0,
121+
"inference_speed": 15.0,
121122
"pid": 123,
122123
}
123124
},
124-
"cameras": {},
125+
"cameras": {
126+
"front_door": {"camera_fps": 0.0, "detection_fps": 0.0},
127+
},
125128
}

apps/frigate-health-checker/tests/test_health_checker.py

Lines changed: 34 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -48,104 +48,90 @@ def test_check_health_api_unresponsive(
4848
assert result.status == HealthStatus.UNHEALTHY
4949
assert result.reason == UnhealthyReason.API_UNRESPONSIVE
5050

51-
def test_check_health_slow_inference(
51+
def test_check_health_no_frames(
5252
self,
5353
settings: Settings,
5454
mock_k8s_client: MagicMock,
5555
mock_pod: MagicMock,
56-
frigate_stats_slow_inference: dict,
5756
) -> None:
58-
"""Test health check with slow Coral inference."""
57+
"""Test health check when camera has no frames (fps=0)."""
58+
stats_no_frames = {
59+
"detectors": {"coral": {"inference_speed": 15.0}},
60+
"cameras": {
61+
"front_door": {"camera_fps": 0.0, "detection_fps": 0.0},
62+
"back_yard": {"camera_fps": 5.0, "detection_fps": 0.1},
63+
},
64+
}
5965
mock_k8s_client.get_frigate_pod.return_value = mock_pod
6066
mock_k8s_client.get_pod_node_name.return_value = "still-fawn"
6167
mock_k8s_client.exec_in_pod.return_value = (
62-
json.dumps(frigate_stats_slow_inference),
68+
json.dumps(stats_no_frames),
6369
True,
6470
)
6571
checker = HealthChecker(settings, mock_k8s_client)
6672

6773
result = checker.check_health()
6874

6975
assert result.status == HealthStatus.UNHEALTHY
70-
assert result.reason == UnhealthyReason.INFERENCE_SLOW
71-
assert result.metrics.inference_speed_ms == 250.0
76+
assert result.reason == UnhealthyReason.NO_FRAMES
77+
assert "front_door" in result.message
7278

73-
def test_check_health_detection_stuck(
79+
def test_check_health_skipped_camera_no_frames(
7480
self,
7581
settings: Settings,
7682
mock_k8s_client: MagicMock,
7783
mock_pod: MagicMock,
78-
frigate_stats_healthy: dict,
7984
) -> None:
80-
"""Test health check with stuck detection."""
85+
"""Test that skipped cameras (e.g., doorbell) don't fail health check."""
86+
stats_doorbell_down = {
87+
"detectors": {"coral": {"inference_speed": 15.0}},
88+
"cameras": {
89+
"reolink_doorbell": {"camera_fps": 0.0, "detection_fps": 0.0},
90+
"back_yard": {"camera_fps": 5.0, "detection_fps": 0.1},
91+
},
92+
}
8193
mock_k8s_client.get_frigate_pod.return_value = mock_pod
8294
mock_k8s_client.get_pod_node_name.return_value = "still-fawn"
8395
mock_k8s_client.exec_in_pod.return_value = (
84-
json.dumps(frigate_stats_healthy),
96+
json.dumps(stats_doorbell_down),
8597
True,
8698
)
87-
# Simulate logs with stuck detection messages
88-
mock_k8s_client.get_pod_logs.return_value = """
89-
2024-01-15 10:00:00 Detection appears to be stuck
90-
2024-01-15 10:01:00 Detection appears to be stuck
91-
2024-01-15 10:02:00 Detection appears to be stuck
92-
"""
9399
checker = HealthChecker(settings, mock_k8s_client)
94100

95101
result = checker.check_health()
96102

97-
assert result.status == HealthStatus.UNHEALTHY
98-
assert result.reason == UnhealthyReason.DETECTION_STUCK
99-
assert result.metrics.stuck_detection_count == 3
100-
101-
def test_check_health_recording_backlog(
102-
self,
103-
settings: Settings,
104-
mock_k8s_client: MagicMock,
105-
mock_pod: MagicMock,
106-
frigate_stats_healthy: dict,
107-
) -> None:
108-
"""Test health check with recording backlog."""
109-
mock_k8s_client.get_frigate_pod.return_value = mock_pod
110-
mock_k8s_client.get_pod_node_name.return_value = "still-fawn"
111-
mock_k8s_client.exec_in_pod.return_value = (
112-
json.dumps(frigate_stats_healthy),
113-
True,
114-
)
115-
# Simulate logs with backlog messages (more than threshold)
116-
backlog_msg = "Too many unprocessed recording segments\n"
117-
mock_k8s_client.get_pod_logs.return_value = backlog_msg * 6
118-
checker = HealthChecker(settings, mock_k8s_client)
119-
120-
result = checker.check_health()
121-
122-
assert result.status == HealthStatus.UNHEALTHY
123-
assert result.reason == UnhealthyReason.RECORDING_BACKLOG
124-
assert result.metrics.recording_backlog_count == 6
103+
# Should be healthy because reolink_doorbell is in skip list
104+
assert result.status == HealthStatus.HEALTHY
105+
assert result.reason is None
125106

126107
def test_check_health_all_healthy(
127108
self,
128109
settings: Settings,
129110
mock_k8s_client: MagicMock,
130111
mock_pod: MagicMock,
131-
frigate_stats_healthy: dict,
132112
) -> None:
133113
"""Test health check when everything is healthy."""
114+
stats_healthy = {
115+
"detectors": {"coral": {"inference_speed": 15.0}},
116+
"cameras": {
117+
"front_door": {"camera_fps": 5.0, "detection_fps": 0.2},
118+
"back_yard": {"camera_fps": 5.0, "detection_fps": 0.1},
119+
},
120+
}
134121
mock_k8s_client.get_frigate_pod.return_value = mock_pod
135122
mock_k8s_client.get_pod_node_name.return_value = "still-fawn"
136123
mock_k8s_client.exec_in_pod.return_value = (
137-
json.dumps(frigate_stats_healthy),
124+
json.dumps(stats_healthy),
138125
True,
139126
)
140-
mock_k8s_client.get_pod_logs.return_value = "Normal log output"
141127
checker = HealthChecker(settings, mock_k8s_client)
142128

143129
result = checker.check_health()
144130

145131
assert result.status == HealthStatus.HEALTHY
146132
assert result.reason is None
147133
assert result.metrics.api_responsive is True
148-
assert result.metrics.inference_speed_ms == 45.5
134+
assert result.metrics.inference_speed_ms == 15.0
149135

150136

151137
class TestRestartManager:

apps/frigate-health-checker/tests/test_integration.py

Lines changed: 21 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -237,19 +237,20 @@ def test_alert_sent_only_once_per_incident(
237237
assert decision2.should_restart is True
238238
assert decision2.should_alert is False
239239

240-
def test_slow_inference_triggers_correct_workflow(
240+
def test_no_frames_triggers_restart(
241241
self,
242242
settings: Settings,
243243
mock_k8s: MagicMock,
244244
) -> None:
245-
"""Test workflow with slow Coral inference detection."""
246-
# Setup: Slow inference
247-
slow_stats = {
248-
"detectors": {"coral": {"inference_speed": 250.0}},
249-
"cameras": {},
245+
"""Test workflow when camera has no frames."""
246+
# Setup: Camera with 0 FPS
247+
no_frames_stats = {
248+
"detectors": {"coral": {"inference_speed": 15.0}},
249+
"cameras": {
250+
"front_door": {"camera_fps": 0.0, "detection_fps": 0.0},
251+
},
250252
}
251-
mock_k8s.exec_in_pod.return_value = (json.dumps(slow_stats), True)
252-
mock_k8s.get_pod_logs.return_value = "Normal logs"
253+
mock_k8s.exec_in_pod.return_value = (json.dumps(no_frames_stats), True)
253254
mock_k8s.get_configmap_data.return_value = {
254255
"consecutive_failures": "1",
255256
"alert_sent_for_incident": "false",
@@ -267,33 +268,29 @@ def test_slow_inference_triggers_correct_workflow(
267268

268269
# Verify
269270
assert result.is_healthy is False
270-
assert "inference" in result.message.lower()
271-
assert result.metrics.inference_speed_ms == 250.0
271+
assert "front_door" in result.message
272272
assert decision.should_restart is True
273273

274-
def test_stuck_detection_triggers_restart(
274+
def test_healthy_with_all_cameras_working(
275275
self,
276276
settings: Settings,
277277
mock_k8s: MagicMock,
278278
) -> None:
279-
"""Test that stuck detection messages trigger restart."""
280-
# Setup: Normal stats but stuck detection in logs
279+
"""Test healthy status when all cameras have frames."""
280+
# Setup: All cameras working
281281
healthy_stats = {
282-
"detectors": {"coral": {"inference_speed": 50.0}},
283-
"cameras": {},
282+
"detectors": {"coral": {"inference_speed": 15.0}},
283+
"cameras": {
284+
"front_door": {"camera_fps": 5.0, "detection_fps": 0.2},
285+
"back_yard": {"camera_fps": 5.0, "detection_fps": 0.1},
286+
},
284287
}
285288
mock_k8s.exec_in_pod.return_value = (json.dumps(healthy_stats), True)
286-
mock_k8s.get_pod_logs.return_value = """
287-
Detection appears to be stuck
288-
Detection appears to be stuck
289-
Detection appears to be stuck
290-
"""
291289
mock_k8s.get_configmap_data.return_value = {
292-
"consecutive_failures": "1",
290+
"consecutive_failures": "0",
293291
"alert_sent_for_incident": "false",
294292
"last_restart_times": "",
295293
}
296-
mock_k8s.is_node_ready.return_value = True
297294

298295
# Execute
299296
checker = HealthChecker(settings, mock_k8s)
@@ -304,10 +301,8 @@ def test_stuck_detection_triggers_restart(
304301
decision = manager.evaluate_restart(result, state)
305302

306303
# Verify
307-
assert result.is_healthy is False
308-
assert result.metrics.stuck_detection_count == 3
309-
assert "stuck" in result.message.lower()
310-
assert decision.should_restart is True
304+
assert result.is_healthy is True
305+
assert decision.should_restart is False
311306

312307

313308
class TestEmailIntegration:

0 commit comments

Comments
 (0)