mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
247 lines
7.4 KiB
Python
247 lines
7.4 KiB
Python
"""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)
|