- Add OpenClawClient wrapping CLI commands (create/delete/list crons) - Implement pool initialization, scale up/down, auto-scaling logic - Implement cron state sync and stuck cron detection - Add pool status and manual scaling APIs - Add 13 unit tests and 5 integration tests Pool automatically scales between 5-20 crons based on load. All 778 tests passing.
186 lines
5.5 KiB
Python
186 lines
5.5 KiB
Python
"""Integration tests for cron pool API."""
|
|
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
from sqlmodel import Session, SQLModel, create_engine
|
|
|
|
from agenteval.intelligent_eval.openclaw_client import OpenClawClient
|
|
from agenteval.storage.db import 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 OpenClawCronPoolDB # noqa: F401
|
|
|
|
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]())
|
|
|
|
|
|
@pytest.fixture()
|
|
def mock_openclaw_client():
|
|
"""Mock OpenClaw client."""
|
|
client = MagicMock(spec=OpenClawClient)
|
|
|
|
counter = {"value": 0}
|
|
|
|
async def create_cron_impl(**kwargs):
|
|
counter["value"] += 1
|
|
return f"cron-{counter['value']}"
|
|
|
|
client.create_cron = AsyncMock(side_effect=create_cron_impl)
|
|
client.delete_cron = AsyncMock()
|
|
client.list_crons = AsyncMock(return_value=[])
|
|
return client
|
|
|
|
|
|
def test_get_cron_pool_status_empty(client: TestClient):
|
|
"""Test getting pool status when pool is empty."""
|
|
response = client.get("/api/openclaw/cron-pool")
|
|
assert response.status_code == 200
|
|
|
|
data = response.json()
|
|
assert data["pool"]["total"] == 0
|
|
assert data["pool"]["idle"] == 0
|
|
assert data["pool"]["busy"] == 0
|
|
assert data["pool"]["stuck"] == 0
|
|
assert data["pool"]["min_size"] == 5
|
|
assert data["pool"]["max_size"] == 20
|
|
|
|
|
|
def test_get_cron_pool_status_with_crons(client: TestClient, db_session: Session):
|
|
"""Test getting pool status with existing crons."""
|
|
# Create crons
|
|
for i in range(10):
|
|
status = ["idle", "busy", "stuck"][i % 3]
|
|
cron = OpenClawCronPoolDB(
|
|
openclaw_cron_id=f"cron-{i}",
|
|
status=status,
|
|
last_active_at=utc_now(),
|
|
)
|
|
db_session.add(cron)
|
|
db_session.commit()
|
|
|
|
response = client.get("/api/openclaw/cron-pool")
|
|
assert response.status_code == 200
|
|
|
|
data = response.json()
|
|
assert data["pool"]["total"] == 10
|
|
assert data["pool"]["idle"] == 4
|
|
assert data["pool"]["busy"] == 3
|
|
assert data["pool"]["stuck"] == 3
|
|
|
|
|
|
@patch("agenteval.web.routers.openclaw_cron_pool.OpenClawClient")
|
|
def test_scale_up_pool(mock_client_class, client: TestClient, db_session: Session, mock_openclaw_client):
|
|
"""Test manually scaling up the pool."""
|
|
mock_client_class.return_value = mock_openclaw_client
|
|
|
|
# Create 5 crons
|
|
for i in range(5):
|
|
cron = OpenClawCronPoolDB(
|
|
openclaw_cron_id=f"existing-cron-{i}",
|
|
status="idle",
|
|
last_active_at=utc_now(),
|
|
)
|
|
db_session.add(cron)
|
|
db_session.commit()
|
|
|
|
# Scale up to 10
|
|
response = client.post("/api/openclaw/cron-pool/scale", json={"target_size": 10})
|
|
assert response.status_code == 200
|
|
|
|
data = response.json()
|
|
assert data["success"] is True
|
|
assert data["scaled_up"] == 5
|
|
assert data["current_size"] == 10
|
|
|
|
# Verify crons created
|
|
from sqlmodel import select
|
|
|
|
crons = db_session.exec(select(OpenClawCronPoolDB)).all()
|
|
assert len(crons) == 10
|
|
|
|
|
|
@patch("agenteval.web.routers.openclaw_cron_pool.OpenClawClient")
|
|
def test_scale_down_pool(mock_client_class, client: TestClient, db_session: Session, mock_openclaw_client):
|
|
"""Test manually scaling down the pool."""
|
|
mock_client_class.return_value = mock_openclaw_client
|
|
|
|
# Create 15 idle crons
|
|
for i in range(15):
|
|
cron = OpenClawCronPoolDB(
|
|
openclaw_cron_id=f"cron-{i}",
|
|
status="idle",
|
|
last_active_at=utc_now(),
|
|
)
|
|
db_session.add(cron)
|
|
db_session.commit()
|
|
|
|
# Scale down to 8
|
|
response = client.post("/api/openclaw/cron-pool/scale", json={"target_size": 8})
|
|
assert response.status_code == 200
|
|
|
|
data = response.json()
|
|
assert data["success"] is True
|
|
assert data["scaled_down"] == 7
|
|
assert data["current_size"] == 8
|
|
|
|
# Verify crons deleted
|
|
from sqlmodel import select
|
|
|
|
crons = db_session.exec(select(OpenClawCronPoolDB)).all()
|
|
assert len(crons) == 8
|
|
|
|
|
|
@patch("agenteval.web.routers.openclaw_cron_pool.OpenClawClient")
|
|
def test_scale_pool_no_change(mock_client_class, client: TestClient, db_session: Session):
|
|
"""Test scaling pool to same size (no change)."""
|
|
mock_client_class.return_value = MagicMock()
|
|
|
|
# Create 10 crons
|
|
for i in range(10):
|
|
cron = OpenClawCronPoolDB(
|
|
openclaw_cron_id=f"cron-{i}",
|
|
status="idle",
|
|
last_active_at=utc_now(),
|
|
)
|
|
db_session.add(cron)
|
|
db_session.commit()
|
|
|
|
# Scale to same size
|
|
response = client.post("/api/openclaw/cron-pool/scale", json={"target_size": 10})
|
|
assert response.status_code == 200
|
|
|
|
data = response.json()
|
|
assert data["success"] is True
|
|
assert data["current_size"] == 10
|
|
assert "already at target size" in data["message"]
|