"""Alert streak gate — consecutive over-threshold before broadcast.""" from __future__ import annotations from unittest.mock import AsyncMock, patch import pytest from server.utils.alert_streak import ( process_metric_alerts, record_over_threshold, reset_streak, streak_required, ) def test_streak_required_minimum_one(): with patch("server.utils.alert_streak.settings") as mock_settings: mock_settings.ALERT_STREAK_REQUIRED = 0 assert streak_required() == 1 mock_settings.ALERT_STREAK_REQUIRED = 3 assert streak_required() == 3 @pytest.mark.asyncio async def test_record_and_reset_streak(): redis = AsyncMock() redis.incr = AsyncMock(return_value=2) redis.expire = AsyncMock() redis.delete = AsyncMock() assert await record_over_threshold(redis, 10, "cpu") == 2 redis.incr.assert_called_once_with("alert_streak:10:cpu") redis.expire.assert_called_once() await reset_streak(redis, 10, "cpu") redis.delete.assert_called_once_with("alert_streak:10:cpu") @pytest.mark.asyncio async def test_suppresses_until_streak_reached(): redis = AsyncMock() redis.smembers = AsyncMock(return_value=set()) redis.incr = AsyncMock(side_effect=[1, 2]) redis.expire = AsyncMock() redis.sadd = AsyncMock() redis.delete = AsyncMock() thresholds = {"cpu": 80, "mem": 80, "disk": 80} system_info = {"cpu_usage": 95.0, "mem_usage": 10.0, "disk_usage": 10.0} with patch("server.utils.alert_streak.streak_required", return_value=3): with patch("server.utils.alert_streak.broadcast_alert", AsyncMock()) as mock_alert: await process_metric_alerts( redis=redis, server_id=477, server_name="test", system_info=system_info, thresholds=thresholds, ) mock_alert.assert_not_called() await process_metric_alerts( redis=redis, server_id=477, server_name="test", system_info=system_info, thresholds=thresholds, ) mock_alert.assert_not_called() @pytest.mark.asyncio async def test_fires_on_third_consecutive_sample(): redis = AsyncMock() redis.smembers = AsyncMock(return_value=set()) redis.incr = AsyncMock(return_value=3) redis.expire = AsyncMock() redis.sadd = AsyncMock() thresholds = {"cpu": 80, "mem": 80, "disk": 80} system_info = {"cpu_usage": 100.0, "mem_usage": 10.0, "disk_usage": 10.0} with patch("server.utils.alert_streak.streak_required", return_value=3): with patch("server.utils.alert_streak.broadcast_alert", AsyncMock()) as mock_alert: await process_metric_alerts( redis=redis, server_id=477, server_name="飒遂", system_info=system_info, thresholds=thresholds, ) mock_alert.assert_called_once_with(477, "cpu", 100.0, "飒遂") redis.sadd.assert_called_once() @pytest.mark.asyncio async def test_does_not_rebroadcast_while_already_active(): redis = AsyncMock() redis.smembers = AsyncMock(return_value={"cpu"}) redis.incr = AsyncMock(return_value=5) redis.expire = AsyncMock() redis.sadd = AsyncMock() thresholds = {"cpu": 80, "mem": 80, "disk": 80} system_info = {"cpu_usage": 100.0, "mem_usage": 10.0, "disk_usage": 10.0} with patch("server.utils.alert_streak.streak_required", return_value=3): with patch("server.utils.alert_streak.broadcast_alert", AsyncMock()) as mock_alert: await process_metric_alerts( redis=redis, server_id=477, server_name="飒遂", system_info=system_info, thresholds=thresholds, ) mock_alert.assert_not_called() redis.sadd.assert_not_called() @pytest.mark.asyncio async def test_reset_streak_on_recovery_path(): redis = AsyncMock() redis.smembers = AsyncMock(return_value={"cpu"}) redis.delete = AsyncMock() redis.srem = AsyncMock() thresholds = {"cpu": 80, "mem": 80, "disk": 80} system_info = {"cpu_usage": 40.0, "mem_usage": 10.0, "disk_usage": 10.0} with patch("server.utils.alert_streak.broadcast_recovery", AsyncMock()) as mock_recovery: await process_metric_alerts( redis=redis, server_id=477, server_name="飒遂", system_info=system_info, thresholds=thresholds, ) mock_recovery.assert_called_once_with(477, "cpu", 40.0, "飒遂") redis.delete.assert_any_call("alert_streak:477:cpu")