|
| 1 | +import uuid |
| 2 | +from typing import Any |
| 3 | + |
1 | 4 | import pytest |
2 | 5 | from agenticlayer.agent_to_a2a import to_a2a |
3 | | -from google.adk.agents.base_agent import BaseAgent |
4 | | -from starlette.applications import Starlette |
| 6 | +from agenticlayer.config import parse_sub_agents, parse_tools |
| 7 | +from google.adk.agents.llm_agent import LlmAgent |
| 8 | +from google.adk.models.lite_llm import LiteLlm |
5 | 9 | from starlette.testclient import TestClient |
6 | 10 |
|
7 | 11 |
|
8 | | -class TestA2AStarlette: |
9 | | - """Test suite for the a2a_starlette module.""" |
| 12 | +def create_mock_agent_card( |
| 13 | + agent_name: str, |
| 14 | + base_url: str, |
| 15 | + skills: list[dict[str, Any]] | None = None, |
| 16 | +) -> dict[str, Any]: |
| 17 | + """Helper function to create a valid agent card response.""" |
| 18 | + return { |
| 19 | + "name": agent_name, |
| 20 | + "description": f"Mock agent {agent_name}", |
| 21 | + "url": base_url, |
| 22 | + "version": "1.0.0", |
| 23 | + "capabilities": {}, |
| 24 | + "skills": skills or [], |
| 25 | + "default_input_modes": ["text/plain"], |
| 26 | + "default_output_modes": ["text/plain"], |
| 27 | + "supports_authenticated_extended_card": False, |
| 28 | + } |
| 29 | + |
10 | 30 |
|
11 | | - @pytest.fixture |
12 | | - def test_agent(self) -> BaseAgent: |
13 | | - """Create a test agent for testing.""" |
14 | | - return BaseAgent(name="test_agent") |
| 31 | +def create_send_message_request( |
| 32 | + message_text: str = "Hello, agent!", |
| 33 | +) -> dict[str, Any]: |
| 34 | + """Helper function to create a valid A2A send message request.""" |
| 35 | + message_id = str(uuid.uuid4()) |
| 36 | + context_id = str(uuid.uuid4()) |
| 37 | + return { |
| 38 | + "jsonrpc": "2.0", |
| 39 | + "id": 1, |
| 40 | + "method": "message/send", |
| 41 | + "params": { |
| 42 | + "message": { |
| 43 | + "role": "user", |
| 44 | + "parts": [{"kind": "text", "text": message_text}], |
| 45 | + "messageId": message_id, |
| 46 | + "contextId": context_id, |
| 47 | + }, |
| 48 | + "metadata": {}, |
| 49 | + }, |
| 50 | + } |
15 | 51 |
|
16 | | - @pytest.fixture |
17 | | - def starlette_app(self, test_agent: BaseAgent) -> Starlette: |
18 | | - """Create a Starlette app with the test agent.""" |
19 | | - return to_a2a(test_agent) |
20 | 52 |
|
21 | | - @pytest.fixture |
22 | | - def client(self, starlette_app: Starlette) -> TestClient: |
23 | | - """Create a test client.""" |
24 | | - return TestClient(starlette_app) |
| 53 | +def create_agent( |
| 54 | + name: str = "test_agent", |
| 55 | + sub_agents_config: str = "{}", |
| 56 | + tools_config: str = "{}", |
| 57 | +) -> LlmAgent: |
| 58 | + sub_agents, agent_tools = parse_sub_agents(sub_agents_config) |
| 59 | + mcp_tools = parse_tools(tools_config) |
| 60 | + tools = [*agent_tools, *mcp_tools] |
| 61 | + return LlmAgent( |
| 62 | + name=name, |
| 63 | + model=LiteLlm(model="gemini/gemini-2.5-flash"), |
| 64 | + description="Test agent", |
| 65 | + instruction="You are a test agent.", |
| 66 | + sub_agents=sub_agents, |
| 67 | + tools=tools, |
| 68 | + ) |
25 | 69 |
|
26 | | - def test_agent_card_endpoint(self, starlette_app: Starlette, client: TestClient) -> None: |
| 70 | + |
| 71 | +class TestA2AStarlette: |
| 72 | + @pytest.mark.asyncio |
| 73 | + async def test_agent_card(self) -> None: |
27 | 74 | """Test that the agent card is available at /.well-known/agent-card.json""" |
28 | 75 |
|
29 | | - # Try the standard agent card endpoint |
| 76 | + # Given: |
| 77 | + agent = create_agent() |
| 78 | + app = await to_a2a(agent) |
| 79 | + client = TestClient(app) |
| 80 | + |
| 81 | + # When: Requesting the agent card endpoint |
30 | 82 | response = client.get("/.well-known/agent-card.json") |
31 | 83 |
|
32 | | - if response.status_code == 200: |
33 | | - # Great! We found the agent card |
34 | | - data = response.json() |
35 | | - assert isinstance(data, dict), "Agent card should return a JSON object" |
| 84 | + # Then: Agent card is returned |
| 85 | + assert response.status_code == 200 |
| 86 | + data = response.json() |
| 87 | + assert isinstance(data, dict), "Agent card should return a JSON object" |
| 88 | + assert data.get("name") == agent.name |
| 89 | + assert data.get("description") == agent.description |
| 90 | + |
| 91 | + @pytest.mark.asyncio |
| 92 | + async def test_agent_rpc_send_message(self) -> None: |
| 93 | + """Test that the RPC url is working for send message.""" |
| 94 | + |
| 95 | + # Given: |
| 96 | + agent = create_agent() |
| 97 | + app = await to_a2a(agent) |
| 98 | + client = TestClient(app) |
| 99 | + |
| 100 | + # When: Sending an A2A RPC request |
| 101 | + rpc_response = client.post("", json=create_send_message_request()) |
| 102 | + |
| 103 | + # Then: RPC response is returned |
| 104 | + assert rpc_response.status_code == 200 |
| 105 | + rpc_data = rpc_response.json() |
| 106 | + assert rpc_data.get("jsonrpc") == "2.0" |
| 107 | + assert rpc_data.get("id") == 1 |
| 108 | + |
| 109 | + @pytest.mark.asyncio |
| 110 | + async def test_sub_agents(self) -> None: |
| 111 | + """Test that sub-agents are parsed and integrated correctly.""" |
| 112 | + |
| 113 | + # When: Creating an agent with sub-agents |
| 114 | + sub_agents_config = """{ |
| 115 | + "sub_agent_1": { |
| 116 | + "url": "http://sub-agent-1.local/.well-known/agent-card.json", |
| 117 | + "interaction_type": "transfer" |
| 118 | + }, |
| 119 | + "sub_agent_2": { |
| 120 | + "url": "http://sub-agent-2.local/.well-known/agent-card.json", |
| 121 | + "interaction_type": "tool_call" |
| 122 | + } |
| 123 | + }""" |
| 124 | + agent = create_agent(sub_agents_config=sub_agents_config) |
| 125 | + |
| 126 | + # Then: Verify sub-agents and tools are parsed correctly |
| 127 | + assert len(agent.sub_agents) == 1, "There should be 1 sub-agent for transfer interaction type" |
| 128 | + assert len(agent.tools) == 1, "There should be 1 agent tool for tool_call interaction type" |
| 129 | + |
| 130 | + # When: Requesting the agent card endpoint |
| 131 | + app = await to_a2a(agent) |
| 132 | + client = TestClient(app) |
| 133 | + response = client.get("/.well-known/agent-card.json") |
| 134 | + |
| 135 | + # Then: Agent card is returned |
| 136 | + assert response.status_code == 200 |
| 137 | + |
| 138 | + @pytest.mark.asyncio |
| 139 | + async def test_tools(self) -> None: |
| 140 | + """Test that tools are parsed and integrated correctly.""" |
| 141 | + |
| 142 | + # When: Creating an agent with tools |
| 143 | + tools_config = """{ |
| 144 | + "tool_1": { |
| 145 | + "url": "http://tool-1.local/mcp" |
| 146 | + }, |
| 147 | + "tool_2": { |
| 148 | + "url": "http://tool-2.local/mcp" |
| 149 | + } |
| 150 | + }""" |
| 151 | + tools = parse_tools(tools_config) |
| 152 | + |
| 153 | + # Then: Verify McpToolsets are created correctly |
| 154 | + assert len(tools) == 2, "There should be 2 McpToolset tools" |
36 | 155 |
|
37 | | - # Verify it contains expected agent card fields |
38 | | - assert len(data) > 0, "Agent card should not be empty" |
| 156 | + # Note: Further integration tests would require mocking MCP tool behavior |
0 commit comments