AgentEvalTool/tests/integration/test_metrics_alerts_api.py
sinohqb 2e7d419f05 feat(intelligent-eval): implement monitoring and alerting (ticket 07)
- 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.
2026-08-12 10:41:15 +08:00

279 lines
7.9 KiB
Python

"""Integration tests for metrics and alerts API."""
from datetime import timedelta
import pytest
from fastapi.testclient import TestClient
from sqlmodel import Session, SQLModel, create_engine, select
from unittest.mock import patch, MagicMock
from agenteval.intelligent_eval.models import IntelligentEvalStatus
from agenteval.storage.db import (
IntelligentEvalDB,
IntelligentEvalTaskQueueDB,
OpenClawCronPoolDB,
utc_now,
)
from agenteval.web.app import app
from agenteval.web.deps import get_db
@pytest.fixture()
def client(tmp_path):
"""Create a TestClient with a fresh database."""
from agenteval.storage.db import ( # noqa: F401
IntelligentEvalDB,
IntelligentEvalTaskQueueDB,
OpenClawCronPoolDB,
)
from agenteval.intelligent_eval.alerts import AlertHistoryDB
engine = create_engine(
f"sqlite:///{tmp_path / 'test.db'}",
connect_args={"check_same_thread": False},
)
SQLModel.metadata.create_all(engine)
session = Session(engine)
def override_get_db():
try:
yield session
finally:
pass
app.dependency_overrides[get_db] = override_get_db
client = TestClient(app)
yield client
app.dependency_overrides.clear()
session.close()
engine.dispose()
@pytest.fixture()
def db_session(client):
"""Get the database session from the client fixture."""
return next(app.dependency_overrides[get_db]())
def test_get_metrics_api(client: TestClient, db_session: Session):
"""Test getting metrics via API."""
# Create some test data
for i in range(5):
cron = OpenClawCronPoolDB(
openclaw_cron_id=f"cron-{i}",
status="busy" if i < 3 else "idle",
last_active_at=utc_now(),
)
db_session.add(cron)
db_session.commit()
response = client.get("/api/openclaw/cron-pool/metrics")
assert response.status_code == 200
data = response.json()
assert "metrics" in data
assert "pool_utilization" in data["metrics"]
assert "task_backlog" in data["metrics"]
assert "stuck_rate" in data["metrics"]
assert "avg_processing_time_seconds" in data["metrics"]
assert "eval_completion_rate" in data["metrics"]
assert data["metrics"]["pool_utilization"] == 0.6
def test_check_alerts_api_no_alerts(client: TestClient, db_session: Session):
"""Test checking alerts when no rules are triggered."""
# Create healthy state
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()
response = client.post("/api/openclaw/cron-pool/check-alerts")
assert response.status_code == 200
data = response.json()
assert data["success"] is True
assert data["alerts_triggered"] == 0
def test_check_alerts_api_with_alerts(client: TestClient, db_session: Session):
"""Test checking alerts when rules are triggered."""
# Create high backlog
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()
response = client.post("/api/openclaw/cron-pool/check-alerts")
assert response.status_code == 200
data = response.json()
assert data["success"] is True
assert data["alerts_triggered"] > 0
# Should have task_backlog alert
backlog_alerts = [a for a in data["alerts"] if a["alert_type"] == "task_backlog"]
assert len(backlog_alerts) == 1
assert backlog_alerts[0]["metric_value"] == 60
def test_get_alert_history_api(client: TestClient, db_session: Session):
"""Test getting alert history via API."""
# Create some alerts
from agenteval.intelligent_eval.alerts import AlertHistoryDB
for i in range(3):
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()
response = client.get("/api/openclaw/cron-pool/alerts")
assert response.status_code == 200
data = response.json()
assert "alerts" in data
assert len(data["alerts"]) == 3
def test_get_alert_history_unresolved_only(client: TestClient, db_session: Session):
"""Test getting unresolved alerts only."""
from agenteval.intelligent_eval.alerts import AlertHistoryDB
# 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(),
)
db_session.add_all([alert1, alert2])
db_session.commit()
response = client.get("/api/openclaw/cron-pool/alerts?unresolved_only=true")
assert response.status_code == 200
data = response.json()
assert len(data["alerts"]) == 1
assert data["alerts"][0]["id"] == "alert-1"
def test_resolve_alert_api(client: TestClient, db_session: Session):
"""Test resolving an alert via API."""
from agenteval.intelligent_eval.alerts import AlertHistoryDB
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()
response = client.post("/api/openclaw/cron-pool/alerts/alert-1/resolve")
assert response.status_code == 200
data = response.json()
assert data["success"] is True
# Verify alert is resolved
db_session.refresh(alert)
assert alert.resolved_at is not None
def test_resolve_nonexistent_alert_api(client: TestClient):
"""Test resolving a non-existent alert."""
response = client.post("/api/openclaw/cron-pool/alerts/nonexistent/resolve")
assert response.status_code == 404
def test_end_to_end_metrics_and_alerts(client: TestClient, db_session: Session):
"""Test end-to-end metrics and alerts flow."""
# Create high utilization state
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)
# Create high backlog
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()
# Get metrics
response = client.get("/api/openclaw/cron-pool/metrics")
assert response.status_code == 200
metrics_data = response.json()["metrics"]
assert metrics_data["pool_utilization"] == 0.95
assert metrics_data["task_backlog"] == 60
# Check alerts
response = client.post("/api/openclaw/cron-pool/check-alerts")
assert response.status_code == 200
alerts_data = response.json()
assert alerts_data["alerts_triggered"] > 0
# Get alert history
response = client.get("/api/openclaw/cron-pool/alerts")
assert response.status_code == 200
history_data = response.json()
assert len(history_data["alerts"]) > 0
# Resolve first alert
if history_data["alerts"]:
alert_id = history_data["alerts"][0]["id"]
response = client.post(f"/api/openclaw/cron-pool/alerts/{alert_id}/resolve")
assert response.status_code == 200