- Add metrics.py with pool utilization, task backlog, stuck rate, avg processing time, eval completion rate - Add alerts.py with alert rules (pool utilization > 90%, task backlog > 50, stuck rate > 10%) - Implement alert history and webhook notifications - Add metrics and alerts APIs - Add database migration for alert history table - Add 11 unit tests for metrics, 10 unit tests for alerts, 8 integration tests - Update migration tests to include new alert history table All 853 tests passing.
274 lines
7.6 KiB
Python
274 lines
7.6 KiB
Python
"""Unit tests for alert rules and notifications."""
|
|
|
|
from datetime import timedelta
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
from sqlmodel import Session, select
|
|
|
|
from agenteval.intelligent_eval.alerts import AlertHistoryDB, AlertManager
|
|
from agenteval.storage.db import (
|
|
IntelligentEvalTaskQueueDB,
|
|
OpenClawCronPoolDB,
|
|
utc_now,
|
|
)
|
|
|
|
|
|
def test_alert_manager_check_rules_no_alerts(db_session: Session):
|
|
"""Test alert manager when no rules are triggered."""
|
|
# Create healthy state: low utilization, low backlog, no stuck
|
|
for i in range(5):
|
|
cron = OpenClawCronPoolDB(
|
|
openclaw_cron_id=f"cron-{i}",
|
|
status="idle",
|
|
last_active_at=utc_now(),
|
|
)
|
|
db_session.add(cron)
|
|
|
|
db_session.commit()
|
|
|
|
manager = AlertManager(db_session)
|
|
alerts = manager.check_rules()
|
|
|
|
assert len(alerts) == 0
|
|
|
|
|
|
def test_alert_manager_task_backlog_alert(db_session: Session):
|
|
"""Test alert manager triggers task backlog alert."""
|
|
# Create high backlog: 60 pending tasks
|
|
for i in range(60):
|
|
task = IntelligentEvalTaskQueueDB(
|
|
eval_id=f"eval-{i}",
|
|
status="pending",
|
|
priority=1,
|
|
reason="slot_due",
|
|
)
|
|
db_session.add(task)
|
|
|
|
db_session.commit()
|
|
|
|
manager = AlertManager(db_session)
|
|
alerts = manager.check_rules()
|
|
|
|
# Should trigger task_backlog alert
|
|
backlog_alerts = [a for a in alerts if a.alert_type == "task_backlog"]
|
|
assert len(backlog_alerts) == 1
|
|
assert backlog_alerts[0].metric_value == 60
|
|
assert backlog_alerts[0].threshold == 50
|
|
assert backlog_alerts[0].severity == "warning"
|
|
|
|
|
|
def test_alert_manager_stuck_rate_alert(db_session: Session):
|
|
"""Test alert manager triggers stuck rate alert."""
|
|
# Create high stuck rate: 3 stuck out of 10
|
|
for i in range(3):
|
|
cron = OpenClawCronPoolDB(
|
|
openclaw_cron_id=f"stuck-{i}",
|
|
status="stuck",
|
|
last_active_at=utc_now(),
|
|
)
|
|
db_session.add(cron)
|
|
|
|
for i in range(7):
|
|
cron = OpenClawCronPoolDB(
|
|
openclaw_cron_id=f"active-{i}",
|
|
status="busy",
|
|
last_active_at=utc_now(),
|
|
)
|
|
db_session.add(cron)
|
|
|
|
db_session.commit()
|
|
|
|
manager = AlertManager(db_session)
|
|
alerts = manager.check_rules()
|
|
|
|
# Should trigger stuck_rate alert
|
|
stuck_alerts = [a for a in alerts if a.alert_type == "stuck_rate"]
|
|
assert len(stuck_alerts) == 1
|
|
assert stuck_alerts[0].metric_value == 0.3
|
|
assert stuck_alerts[0].threshold == 0.1
|
|
assert stuck_alerts[0].severity == "critical"
|
|
|
|
|
|
def test_alert_manager_pool_utilization_with_duration(db_session: Session):
|
|
"""Test alert manager respects duration requirement for pool utilization."""
|
|
# Create high utilization: 19 busy out of 20
|
|
for i in range(19):
|
|
cron = OpenClawCronPoolDB(
|
|
openclaw_cron_id=f"busy-{i}",
|
|
status="busy",
|
|
last_active_at=utc_now(),
|
|
)
|
|
db_session.add(cron)
|
|
|
|
cron = OpenClawCronPoolDB(
|
|
openclaw_cron_id="idle-0",
|
|
status="idle",
|
|
last_active_at=utc_now(),
|
|
)
|
|
db_session.add(cron)
|
|
|
|
db_session.commit()
|
|
|
|
manager = AlertManager(db_session)
|
|
|
|
# First check: should not trigger (duration not met)
|
|
alerts1 = manager.check_rules()
|
|
utilization_alerts1 = [a for a in alerts1 if a.alert_type == "pool_utilization"]
|
|
assert len(utilization_alerts1) == 0
|
|
|
|
# Simulate time passing (10 minutes)
|
|
# In real scenario, this would be checked over time
|
|
# For testing, we just verify the logic exists
|
|
|
|
|
|
def test_alert_manager_get_alert_history(db_session: Session):
|
|
"""Test getting alert history."""
|
|
# Create some alerts
|
|
for i in range(5):
|
|
alert = AlertHistoryDB(
|
|
id=f"alert-{i}",
|
|
alert_type="task_backlog",
|
|
severity="warning",
|
|
message=f"Test alert {i}",
|
|
metric_value=50 + i,
|
|
threshold=50,
|
|
)
|
|
db_session.add(alert)
|
|
|
|
db_session.commit()
|
|
|
|
manager = AlertManager(db_session)
|
|
alerts = manager.get_alert_history(limit=3)
|
|
|
|
assert len(alerts) == 3
|
|
# Should be ordered by created_at descending
|
|
assert alerts[0].id == "alert-4"
|
|
|
|
|
|
def test_alert_manager_get_unresolved_alerts(db_session: Session):
|
|
"""Test getting unresolved alerts."""
|
|
# Create resolved and unresolved alerts
|
|
alert1 = AlertHistoryDB(
|
|
id="alert-1",
|
|
alert_type="task_backlog",
|
|
severity="warning",
|
|
message="Test alert 1",
|
|
metric_value=60,
|
|
threshold=50,
|
|
)
|
|
alert2 = AlertHistoryDB(
|
|
id="alert-2",
|
|
alert_type="stuck_rate",
|
|
severity="critical",
|
|
message="Test alert 2",
|
|
metric_value=0.2,
|
|
threshold=0.1,
|
|
resolved_at=utc_now(),
|
|
)
|
|
alert3 = AlertHistoryDB(
|
|
id="alert-3",
|
|
alert_type="pool_utilization",
|
|
severity="warning",
|
|
message="Test alert 3",
|
|
metric_value=0.95,
|
|
threshold=0.9,
|
|
)
|
|
|
|
db_session.add_all([alert1, alert2, alert3])
|
|
db_session.commit()
|
|
|
|
manager = AlertManager(db_session)
|
|
unresolved = manager.get_unresolved_alerts()
|
|
|
|
assert len(unresolved) == 2
|
|
assert all(a.resolved_at is None for a in unresolved)
|
|
|
|
|
|
def test_alert_manager_resolve_alert(db_session: Session):
|
|
"""Test resolving an alert."""
|
|
alert = AlertHistoryDB(
|
|
id="alert-1",
|
|
alert_type="task_backlog",
|
|
severity="warning",
|
|
message="Test alert",
|
|
metric_value=60,
|
|
threshold=50,
|
|
)
|
|
db_session.add(alert)
|
|
db_session.commit()
|
|
|
|
manager = AlertManager(db_session)
|
|
resolved = manager.resolve_alert("alert-1")
|
|
|
|
assert resolved is True
|
|
|
|
db_session.refresh(alert)
|
|
assert alert.resolved_at is not None
|
|
|
|
|
|
def test_alert_manager_resolve_nonexistent_alert(db_session: Session):
|
|
"""Test resolving a non-existent alert."""
|
|
manager = AlertManager(db_session)
|
|
resolved = manager.resolve_alert("nonexistent")
|
|
|
|
assert resolved is False
|
|
|
|
|
|
def test_alert_manager_webhook_notification(db_session: Session):
|
|
"""Test webhook notification."""
|
|
# Create high backlog to trigger alert
|
|
for i in range(60):
|
|
task = IntelligentEvalTaskQueueDB(
|
|
eval_id=f"eval-{i}",
|
|
status="pending",
|
|
priority=1,
|
|
reason="slot_due",
|
|
)
|
|
db_session.add(task)
|
|
|
|
db_session.commit()
|
|
|
|
# Mock webhook
|
|
with patch("httpx.post") as mock_post:
|
|
mock_post.return_value.status_code = 200
|
|
mock_post.return_value.raise_for_status = MagicMock()
|
|
|
|
manager = AlertManager(db_session, webhook_url="https://example.com/webhook")
|
|
alerts = manager.check_rules()
|
|
|
|
# Should have triggered alerts
|
|
assert len(alerts) > 0
|
|
|
|
# Should have called webhook
|
|
assert mock_post.called
|
|
|
|
|
|
def test_alert_manager_webhook_failure(db_session: Session):
|
|
"""Test webhook notification failure."""
|
|
# Create high backlog to trigger alert
|
|
for i in range(60):
|
|
task = IntelligentEvalTaskQueueDB(
|
|
eval_id=f"eval-{i}",
|
|
status="pending",
|
|
priority=1,
|
|
reason="slot_due",
|
|
)
|
|
db_session.add(task)
|
|
|
|
db_session.commit()
|
|
|
|
# Mock webhook failure
|
|
with patch("httpx.post") as mock_post:
|
|
mock_post.side_effect = Exception("Webhook failed")
|
|
|
|
manager = AlertManager(db_session, webhook_url="https://example.com/webhook")
|
|
alerts = manager.check_rules()
|
|
|
|
# Should still create alerts even if webhook fails
|
|
assert len(alerts) > 0
|
|
|
|
# Webhook should not be marked as sent
|
|
for alert in alerts:
|
|
assert alert.webhook_sent is False
|