"""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)