All checks were successful
CI / test (push) Successful in 4m1s
方案③的定时触发(scan loop 每 60s 入队 + 触发 worker)此前只有 Worker
消费端 API,无可查看的列表。新增:
- GET /api/intelligent-evals/tasks:任务明细(含评估名/状态)+ 状态分布统计
(注册在 /{eval_id} 之前避免被捕获为 eval_id="tasks")
- 前端 TaskQueueMonitor 组件 + 智能评估页任务队列入口:5s 轮询
(usePolling),状态卡 + 状态筛选 + 明细表
测试:+3(列表/筛选/不被 {eval_id} 遮蔽),892 passed,tsc 通过
277 lines
9.0 KiB
Python
277 lines
9.0 KiB
Python
"""Integration tests for intelligent eval task queue API."""
|
||
|
||
from datetime import timedelta
|
||
|
||
import pytest
|
||
from agenteval.intelligent_eval.models import IntelligentEvalStatus
|
||
from agenteval.storage.db import IntelligentEvalDB, IntelligentEvalTaskQueueDB, utc_now
|
||
from agenteval.web.app import app
|
||
from agenteval.web.deps import get_db
|
||
from fastapi.testclient import TestClient
|
||
from sqlmodel import Session, SQLModel, create_engine
|
||
|
||
|
||
@pytest.fixture()
|
||
def client(tmp_path):
|
||
"""Create a TestClient with a fresh database."""
|
||
from agenteval.storage.db import ( # noqa: F401
|
||
IntelligentEvalDB,
|
||
IntelligentEvalSessionDB,
|
||
IntelligentEvalTaskQueueDB,
|
||
)
|
||
|
||
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."""
|
||
# The session is stored in the dependency override
|
||
return next(app.dependency_overrides[get_db]())
|
||
|
||
|
||
def test_get_next_task_empty(client: TestClient):
|
||
"""Test getting next task when queue is empty."""
|
||
response = client.get("/api/intelligent-evals/tasks/next")
|
||
assert response.status_code == 200
|
||
assert response.json() == {"task": None}
|
||
|
||
|
||
def test_get_next_task_with_pending_task(client: TestClient, db_session: Session):
|
||
"""Test getting next task when there is a pending task."""
|
||
# Create eval
|
||
eval_db = IntelligentEvalDB(
|
||
name="test",
|
||
target_id="target1",
|
||
status=IntelligentEvalStatus.EXECUTING.value,
|
||
started_at=utc_now() - timedelta(hours=9),
|
||
)
|
||
eval_db.set_plan(
|
||
{
|
||
"time_distribution": [{"time_slot": "8-10h", "sessions": 2}],
|
||
"estimated_sessions": 2,
|
||
}
|
||
)
|
||
db_session.add(eval_db)
|
||
db_session.commit()
|
||
|
||
# Create pending task
|
||
task = IntelligentEvalTaskQueueDB(
|
||
eval_id=eval_db.id,
|
||
status="pending",
|
||
priority=10,
|
||
reason="slot_due",
|
||
)
|
||
db_session.add(task)
|
||
db_session.commit()
|
||
|
||
# Get next task
|
||
response = client.get("/api/intelligent-evals/tasks/next")
|
||
assert response.status_code == 200
|
||
|
||
data = response.json()
|
||
assert data["task"] is not None
|
||
assert data["task"]["id"] == task.id
|
||
assert data["task"]["eval_id"] == eval_db.id
|
||
assert data["task"]["priority"] == 10
|
||
assert data["task"]["reason"] == "slot_due"
|
||
assert data["task"]["eval"]["id"] == eval_db.id
|
||
assert data["task"]["eval"]["name"] == "test"
|
||
assert data["task"]["eval"]["status"] == IntelligentEvalStatus.EXECUTING.value
|
||
|
||
|
||
def test_assign_task(client: TestClient, db_session: Session):
|
||
"""Test assigning a task to a cron."""
|
||
# Create pending task
|
||
task = IntelligentEvalTaskQueueDB(
|
||
eval_id="eval1",
|
||
status="pending",
|
||
priority=1,
|
||
reason="slot_due",
|
||
)
|
||
db_session.add(task)
|
||
db_session.commit()
|
||
|
||
# Assign task
|
||
response = client.post(f"/api/intelligent-evals/tasks/{task.id}/assign?cron_id=cron1")
|
||
assert response.status_code == 200
|
||
assert response.json() == {"success": True}
|
||
|
||
# Verify assignment
|
||
db_session.refresh(task)
|
||
assert task.status == "assigned"
|
||
assert task.assigned_cron_id == "cron1"
|
||
|
||
|
||
def test_assign_task_not_found(client: TestClient):
|
||
"""Test assigning a non-existent task."""
|
||
response = client.post("/api/intelligent-evals/tasks/nonexistent/assign?cron_id=cron1")
|
||
assert response.status_code == 404
|
||
|
||
|
||
def test_complete_task(client: TestClient, db_session: Session):
|
||
"""Test completing a task."""
|
||
# Create assigned task
|
||
task = IntelligentEvalTaskQueueDB(
|
||
eval_id="eval1",
|
||
status="assigned",
|
||
priority=1,
|
||
reason="slot_due",
|
||
assigned_cron_id="cron1",
|
||
)
|
||
db_session.add(task)
|
||
db_session.commit()
|
||
|
||
# Complete task
|
||
response = client.post(f"/api/intelligent-evals/tasks/{task.id}/complete?success=true")
|
||
assert response.status_code == 200
|
||
assert response.json() == {"success": True}
|
||
|
||
# Verify completion
|
||
db_session.refresh(task)
|
||
assert task.status == "completed"
|
||
assert task.completed_at is not None
|
||
|
||
|
||
def test_complete_task_with_error(client: TestClient, db_session: Session):
|
||
"""Test completing a task with error."""
|
||
# Create assigned task
|
||
task = IntelligentEvalTaskQueueDB(
|
||
eval_id="eval1",
|
||
status="assigned",
|
||
priority=1,
|
||
reason="slot_due",
|
||
assigned_cron_id="cron1",
|
||
)
|
||
db_session.add(task)
|
||
db_session.commit()
|
||
|
||
# Complete task with error
|
||
response = client.post(f"/api/intelligent-evals/tasks/{task.id}/complete?success=false&error=test_error")
|
||
assert response.status_code == 200
|
||
assert response.json() == {"success": True}
|
||
|
||
# Verify completion
|
||
db_session.refresh(task)
|
||
assert task.status == "failed"
|
||
assert task.error == "test_error"
|
||
|
||
|
||
def test_end_to_end_task_lifecycle(client: TestClient, db_session: Session):
|
||
"""Test end-to-end task lifecycle: create eval -> scan -> enqueue -> assign -> complete."""
|
||
# Create eval that needs attention
|
||
eval_db = IntelligentEvalDB(
|
||
name="test",
|
||
target_id="target1",
|
||
status=IntelligentEvalStatus.EXECUTING.value,
|
||
started_at=utc_now() - timedelta(hours=9),
|
||
)
|
||
eval_db.set_plan(
|
||
{
|
||
"time_distribution": [{"time_slot": "8-10h", "sessions": 2}],
|
||
"estimated_sessions": 2,
|
||
}
|
||
)
|
||
db_session.add(eval_db)
|
||
db_session.commit()
|
||
|
||
# Scan and enqueue tasks
|
||
from agenteval.intelligent_eval import task_queue
|
||
|
||
enqueued = task_queue.scan_and_enqueue_tasks(db_session)
|
||
assert enqueued == 1
|
||
|
||
# Get next task
|
||
response = client.get("/api/intelligent-evals/tasks/next")
|
||
assert response.status_code == 200
|
||
task_data = response.json()["task"]
|
||
assert task_data is not None
|
||
assert task_data["eval_id"] == eval_db.id
|
||
|
||
# Assign task
|
||
response = client.post(f"/api/intelligent-evals/tasks/{task_data['id']}/assign?cron_id=cron1")
|
||
assert response.status_code == 200
|
||
|
||
# Complete task
|
||
response = client.post(f"/api/intelligent-evals/tasks/{task_data['id']}/complete?success=true")
|
||
assert response.status_code == 200
|
||
|
||
# Verify task completed
|
||
task = db_session.get(IntelligentEvalTaskQueueDB, task_data["id"])
|
||
assert task.status == "completed"
|
||
|
||
|
||
def test_list_tasks(client: TestClient, db_session: Session):
|
||
"""Task list returns entries (newest first) with eval names and stats."""
|
||
eval_db = IntelligentEvalDB(
|
||
name="list-eval",
|
||
target_id="target1",
|
||
status=IntelligentEvalStatus.EXECUTING.value,
|
||
started_at=utc_now(),
|
||
)
|
||
eval_db.set_plan({"time_distribution": [{"time_slot": "0-1h", "sessions": 1}], "estimated_sessions": 1})
|
||
db_session.add(eval_db)
|
||
db_session.commit()
|
||
|
||
old = IntelligentEvalTaskQueueDB(eval_id=eval_db.id, status="completed", priority=5, reason="done")
|
||
new = IntelligentEvalTaskQueueDB(eval_id=eval_db.id, status="pending", priority=1, reason="slot_due")
|
||
db_session.add_all([old, new])
|
||
db_session.commit()
|
||
# 确保 old 早于 new(created_at 由 default_factory 生成,顺序可能同秒)
|
||
old.created_at = utc_now() - timedelta(seconds=5)
|
||
db_session.commit()
|
||
|
||
response = client.get("/api/intelligent-evals/tasks")
|
||
assert response.status_code == 200
|
||
data = response.json()
|
||
assert data["stats"]["pending"] == 1
|
||
assert data["stats"]["completed"] == 1
|
||
assert data["stats"]["unresolved"] == 1
|
||
# newest first
|
||
assert [t["id"] for t in data["tasks"]] == [new.id, old.id]
|
||
task = data["tasks"][0]
|
||
assert task["eval_id"] == eval_db.id
|
||
assert task["eval_name"] == "list-eval"
|
||
assert task["eval_status"] == IntelligentEvalStatus.EXECUTING.value
|
||
assert task["priority"] == 1
|
||
assert task["reason"] == "slot_due"
|
||
|
||
|
||
def test_list_tasks_status_filter(client: TestClient, db_session: Session):
|
||
"""Status filter narrows the task list."""
|
||
db_session.add(IntelligentEvalTaskQueueDB(eval_id="eval1", status="pending", priority=1, reason="slot_due"))
|
||
db_session.add(IntelligentEvalTaskQueueDB(eval_id="eval1", status="failed", priority=1, reason="slot_due"))
|
||
db_session.commit()
|
||
|
||
response = client.get("/api/intelligent-evals/tasks?status=failed")
|
||
assert response.status_code == 200
|
||
data = response.json()
|
||
assert len(data["tasks"]) == 1
|
||
assert data["tasks"][0]["status"] == "failed"
|
||
assert data["stats"]["failed"] == 1
|
||
|
||
|
||
def test_list_tasks_not_shadowed_by_eval_id(client: TestClient):
|
||
"""GET /tasks must hit the task-list endpoint, not /{eval_id} with eval_id="tasks"."""
|
||
response = client.get("/api/intelligent-evals/tasks")
|
||
assert response.status_code == 200
|
||
assert "tasks" in response.json()
|