Files
roboco/tests/integration/test_a2a_routes.py
T

247 lines
7.4 KiB
Python
Raw Normal View History

2026-05-05 05:50:01 +02:00
"""A2A API route coverage — agent cards, tasks, conversations."""
from __future__ import annotations
from typing import TYPE_CHECKING
from uuid import uuid4
import pytest
import pytest_asyncio
from fastapi import FastAPI
from httpx import ASGITransport, AsyncClient
from roboco.api.deps import get_current_agent_slug, get_db
from roboco.api.routes.a2a import router as a2a_router
from roboco.api.routes.a2a import wellknown_router
from roboco.db.tables import AgentTable, ProjectTable, TaskTable
from roboco.models import AgentRole, AgentStatus, Team
from roboco.models.base import (
TaskNature,
TaskStatus,
TaskType,
)
if TYPE_CHECKING:
from collections.abc import AsyncIterator
from sqlalchemy.ext.asyncio import AsyncSession
@pytest_asyncio.fixture
async def a2a_route_client(
db_session: AsyncSession,
) -> AsyncIterator[dict]:
dev = AgentTable(
id=uuid4(),
name="Dev",
slug=f"be-dev-{uuid4().hex[:8]}",
role=AgentRole.DEVELOPER,
team=Team.BACKEND,
status=AgentStatus.ACTIVE,
model_config={},
system_prompt="dev",
capabilities=[],
permissions={},
metrics={},
)
db_session.add(dev)
await db_session.flush()
project = ProjectTable(
id=uuid4(),
name="A2A-Proj",
slug=f"a2a-proj-{uuid4().hex[:6]}",
git_url="https://example.com/r.git",
assigned_cell=Team.BACKEND,
created_by=dev.id,
)
db_session.add(project)
await db_session.flush()
task = TaskTable(
id=uuid4(),
title="t",
description="d",
acceptance_criteria=["ac"],
status=TaskStatus.PENDING,
priority=2,
task_type=TaskType.CODE,
nature=TaskNature.TECHNICAL,
project_id=project.id,
created_by=dev.id,
team=Team.BACKEND,
)
db_session.add(task)
await db_session.flush()
app = FastAPI()
app.include_router(a2a_router, prefix="/api/a2a")
app.include_router(wellknown_router)
async def _override_db():
yield db_session
async def _override_agent_slug() -> str:
return dev.slug
app.dependency_overrides[get_db] = _override_db
app.dependency_overrides[get_current_agent_slug] = _override_agent_slug
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as client:
yield {"client": client, "dev": dev, "task": task}
app.dependency_overrides.clear()
_HDR = {"X-Agent-ID": "be-dev-1", "X-Agent-Role": "developer"}
# ---------------------------------------------------------------------------
# Well-known endpoints
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_get_system_agent_card(a2a_route_client: dict) -> None:
client = a2a_route_client["client"]
response = await client.get("/.well-known/agent.json")
assert response.status_code == 200
body = response.json()
assert body["id"] == "roboco-system"
@pytest.mark.asyncio
async def test_get_agent_card_by_slug(a2a_route_client: dict) -> None:
client = a2a_route_client["client"]
response = await client.get(
f"/agents/{a2a_route_client['dev'].slug}/.well-known/agent.json",
)
assert response.status_code == 200
@pytest.mark.asyncio
async def test_get_agent_card_unknown(a2a_route_client: dict) -> None:
client = a2a_route_client["client"]
response = await client.get(
f"/agents/{uuid4()}/.well-known/agent.json",
)
assert response.status_code == 404
# ---------------------------------------------------------------------------
# Tasks endpoints
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_get_a2a_task(a2a_route_client: dict) -> None:
client = a2a_route_client["client"]
response = await client.get(
f"/api/a2a/tasks/{a2a_route_client['task'].id}", headers=_HDR
)
assert response.status_code == 200
@pytest.mark.asyncio
async def test_get_a2a_task_not_found(a2a_route_client: dict) -> None:
client = a2a_route_client["client"]
response = await client.get(f"/api/a2a/tasks/{uuid4()}", headers=_HDR)
assert response.status_code == 404
@pytest.mark.asyncio
async def test_list_a2a_tasks(a2a_route_client: dict) -> None:
client = a2a_route_client["client"]
response = await client.get("/api/a2a/tasks", headers=_HDR)
assert response.status_code == 200
@pytest.mark.asyncio
async def test_cancel_a2a_task_invalid_id(a2a_route_client: dict) -> None:
client = a2a_route_client["client"]
response = await client.post(
"/api/a2a/tasks/not-a-uuid/cancel",
json={},
headers=_HDR,
)
# 400 for invalid UUID, or 404 if it parses then doesn't find.
assert response.status_code in (400, 404, 422)
# ---------------------------------------------------------------------------
# Discovery endpoints
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_list_agents(a2a_route_client: dict) -> None:
client = a2a_route_client["client"]
response = await client.get("/api/a2a/agents", headers=_HDR)
assert response.status_code == 200
@pytest.mark.asyncio
async def test_list_agents_filter_by_role(a2a_route_client: dict) -> None:
client = a2a_route_client["client"]
response = await client.get(
"/api/a2a/agents?role=developer", headers=_HDR
)
assert response.status_code == 200
@pytest.mark.asyncio
async def test_get_agent_card_endpoint(a2a_route_client: dict) -> None:
client = a2a_route_client["client"]
response = await client.get(
f"/api/a2a/agents/{a2a_route_client['dev'].slug}/card", headers=_HDR
)
assert response.status_code == 200
# ---------------------------------------------------------------------------
# Chat endpoints
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_chat_inbox(a2a_route_client: dict) -> None:
client = a2a_route_client["client"]
response = await client.get("/api/a2a/chat/inbox", headers=_HDR)
# Inbox needs proper agent context; route may 200 or 500.
assert response.status_code in (200, 500)
@pytest.mark.asyncio
async def test_chat_pairs(a2a_route_client: dict) -> None:
client = a2a_route_client["client"]
response = await client.get("/api/a2a/chat/pairs", headers=_HDR)
assert response.status_code in (200, 500)
@pytest.mark.asyncio
async def test_chat_list_conversations(a2a_route_client: dict) -> None:
client = a2a_route_client["client"]
response = await client.get("/api/a2a/chat/conversations", headers=_HDR)
assert response.status_code == 200
# ---------------------------------------------------------------------------
# Send message — task_id required
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_send_message_missing_task_id_returns_4xx(
a2a_route_client: dict,
) -> None:
"""task_id is required — schema or route enforces it."""
client = a2a_route_client["client"]
response = await client.post(
"/api/a2a/message/send",
json={
"message": {
"role": "user",
"parts": [{"kind": "text", "text": "hi"}],
}
},
headers=_HDR,
)
assert response.status_code in (400, 422)