|
26 | 26 | (driving the app lifespan and asserting routes are attached). |
27 | 27 | """ |
28 | 28 |
|
| 29 | +import logging |
29 | 30 | from unittest.mock import ANY |
30 | 31 | from unittest.mock import AsyncMock |
31 | 32 | from unittest.mock import Mock |
@@ -225,6 +226,100 @@ def test_to_a2a_custom_host_port( |
225 | 226 | agent=self.mock_agent, rpc_url="http://example.com:9000/" |
226 | 227 | ) |
227 | 228 |
|
| 229 | + @patch("google.adk.a2a.utils.agent_to_a2a.A2aAgentExecutor") |
| 230 | + @patch("google.adk.a2a.utils.agent_to_a2a.InMemoryTaskStore") |
| 231 | + @patch("google.adk.a2a.utils.agent_to_a2a.AgentCardBuilder") |
| 232 | + @patch("google.adk.a2a.utils.agent_to_a2a.Starlette") |
| 233 | + def test_to_a2a_with_rpc_path( |
| 234 | + self, |
| 235 | + mock_starlette_class, |
| 236 | + mock_card_builder_class, |
| 237 | + mock_task_store_class, |
| 238 | + mock_agent_executor_class, |
| 239 | + ): |
| 240 | + """rpc_path is threaded into the advertised RPC URL.""" |
| 241 | + mock_starlette_class.return_value = Mock(spec=Starlette) |
| 242 | + mock_agent_executor_class.return_value = Mock(spec=A2aAgentExecutor) |
| 243 | + mock_card_builder_class.return_value = Mock(spec=AgentCardBuilder) |
| 244 | + |
| 245 | + to_a2a(self.mock_agent, rpc_path="/analysis-agent") |
| 246 | + |
| 247 | + mock_card_builder_class.assert_called_once_with( |
| 248 | + agent=self.mock_agent, rpc_url="http://localhost:8000/analysis-agent/" |
| 249 | + ) |
| 250 | + |
| 251 | + @pytest.mark.parametrize( |
| 252 | + "rpc_path", ["analysis-agent", "/analysis-agent", "/analysis-agent/"] |
| 253 | + ) |
| 254 | + @patch("google.adk.a2a.utils.agent_to_a2a.A2aAgentExecutor") |
| 255 | + @patch("google.adk.a2a.utils.agent_to_a2a.InMemoryTaskStore") |
| 256 | + @patch("google.adk.a2a.utils.agent_to_a2a.AgentCardBuilder") |
| 257 | + @patch("google.adk.a2a.utils.agent_to_a2a.Starlette") |
| 258 | + def test_to_a2a_rpc_path_normalizes_slashes( |
| 259 | + self, |
| 260 | + mock_starlette_class, |
| 261 | + mock_card_builder_class, |
| 262 | + mock_task_store_class, |
| 263 | + mock_agent_executor_class, |
| 264 | + rpc_path, |
| 265 | + ): |
| 266 | + """Leading/trailing slashes on rpc_path are normalized identically.""" |
| 267 | + mock_starlette_class.return_value = Mock(spec=Starlette) |
| 268 | + mock_agent_executor_class.return_value = Mock(spec=A2aAgentExecutor) |
| 269 | + mock_card_builder_class.return_value = Mock(spec=AgentCardBuilder) |
| 270 | + |
| 271 | + to_a2a(self.mock_agent, rpc_path=rpc_path) |
| 272 | + |
| 273 | + mock_card_builder_class.assert_called_once_with( |
| 274 | + agent=self.mock_agent, rpc_url="http://localhost:8000/analysis-agent/" |
| 275 | + ) |
| 276 | + |
| 277 | + @pytest.mark.parametrize("rpc_path", ["/", "//", "///"]) |
| 278 | + @patch("google.adk.a2a.utils.agent_to_a2a.A2aAgentExecutor") |
| 279 | + @patch("google.adk.a2a.utils.agent_to_a2a.InMemoryTaskStore") |
| 280 | + @patch("google.adk.a2a.utils.agent_to_a2a.AgentCardBuilder") |
| 281 | + @patch("google.adk.a2a.utils.agent_to_a2a.Starlette") |
| 282 | + def test_to_a2a_all_slash_rpc_path_collapses_to_root( |
| 283 | + self, |
| 284 | + mock_starlette_class, |
| 285 | + mock_card_builder_class, |
| 286 | + mock_task_store_class, |
| 287 | + mock_agent_executor_class, |
| 288 | + rpc_path, |
| 289 | + ): |
| 290 | + """An all-slash rpc_path collapses to a root mount.""" |
| 291 | + mock_starlette_class.return_value = Mock(spec=Starlette) |
| 292 | + mock_agent_executor_class.return_value = Mock(spec=A2aAgentExecutor) |
| 293 | + mock_card_builder_class.return_value = Mock(spec=AgentCardBuilder) |
| 294 | + |
| 295 | + to_a2a(self.mock_agent, rpc_path=rpc_path) |
| 296 | + |
| 297 | + mock_card_builder_class.assert_called_once_with( |
| 298 | + agent=self.mock_agent, rpc_url="http://localhost:8000/" |
| 299 | + ) |
| 300 | + |
| 301 | + @patch("google.adk.a2a.utils.agent_to_a2a.A2aAgentExecutor") |
| 302 | + @patch("google.adk.a2a.utils.agent_to_a2a.InMemoryTaskStore") |
| 303 | + @patch("google.adk.a2a.utils.agent_to_a2a.AgentCardBuilder") |
| 304 | + @patch("google.adk.a2a.utils.agent_to_a2a.Starlette") |
| 305 | + def test_to_a2a_multi_segment_rpc_path( |
| 306 | + self, |
| 307 | + mock_starlette_class, |
| 308 | + mock_card_builder_class, |
| 309 | + mock_task_store_class, |
| 310 | + mock_agent_executor_class, |
| 311 | + ): |
| 312 | + """A multi-segment rpc_path is preserved in the RPC URL.""" |
| 313 | + mock_starlette_class.return_value = Mock(spec=Starlette) |
| 314 | + mock_agent_executor_class.return_value = Mock(spec=A2aAgentExecutor) |
| 315 | + mock_card_builder_class.return_value = Mock(spec=AgentCardBuilder) |
| 316 | + |
| 317 | + to_a2a(self.mock_agent, rpc_path="/team/agent") |
| 318 | + |
| 319 | + mock_card_builder_class.assert_called_once_with( |
| 320 | + agent=self.mock_agent, rpc_url="http://localhost:8000/team/agent/" |
| 321 | + ) |
| 322 | + |
228 | 323 | @patch("google.adk.a2a.utils.agent_to_a2a.A2aAgentExecutor") |
229 | 324 | @patch("google.adk.a2a.utils.agent_to_a2a.InMemoryTaskStore") |
230 | 325 | @patch("google.adk.a2a.utils.agent_to_a2a.AgentCardBuilder") |
@@ -518,6 +613,26 @@ async def test_setup_a2a_builds_card_and_attaches_routes( |
518 | 613 | mock_card_builder.build.assert_called_once() |
519 | 614 | _assert_a2a_routes_attached(app) |
520 | 615 |
|
| 616 | + async def test_to_a2a_rpc_path_mounts_prefixed_routes(self): |
| 617 | + """rpc_path mounts the JSON-RPC and agent-card routes under the prefix.""" |
| 618 | + agent = LlmAgent( |
| 619 | + name="prefixed_agent", description="d", model="gemini-2.0-flash" |
| 620 | + ) |
| 621 | + app = to_a2a(agent, port=8001, rpc_path="/analysis-agent") |
| 622 | + |
| 623 | + async with app.router.lifespan_context(app): |
| 624 | + paths = _route_paths(app) |
| 625 | + |
| 626 | + assert ( |
| 627 | + "/analysis-agent" in paths |
| 628 | + ), f"missing prefixed RPC route; got {paths}" |
| 629 | + assert any( |
| 630 | + p and p.startswith("/analysis-agent/.well-known") for p in paths |
| 631 | + ), f"missing prefixed agent-card route; got {paths}" |
| 632 | + assert ( |
| 633 | + "/" not in paths |
| 634 | + ), f"root RPC route should not be mounted; got {paths}" |
| 635 | + |
521 | 636 | @patch("google.adk.a2a.utils.agent_to_a2a.A2aAgentExecutor") |
522 | 637 | @patch("google.adk.a2a.utils.agent_to_a2a.InMemoryTaskStore") |
523 | 638 | @patch("google.adk.a2a.utils.agent_to_a2a.AgentCardBuilder") |
@@ -560,6 +675,34 @@ async def test_to_a2a_with_custom_agent_card_object( |
560 | 675 | mock_card_builder.build.assert_not_called() |
561 | 676 | _assert_a2a_routes_attached(app) |
562 | 677 |
|
| 678 | + @patch("google.adk.a2a.utils.agent_to_a2a.A2aAgentExecutor") |
| 679 | + @patch("google.adk.a2a.utils.agent_to_a2a.InMemoryTaskStore") |
| 680 | + @patch("google.adk.a2a.utils.agent_to_a2a.AgentCardBuilder") |
| 681 | + @patch("google.adk.a2a.utils.agent_to_a2a.Starlette") |
| 682 | + def test_to_a2a_warns_when_agent_card_and_rpc_path_both_set( |
| 683 | + self, |
| 684 | + mock_starlette_class, |
| 685 | + mock_card_builder_class, |
| 686 | + mock_task_store_class, |
| 687 | + mock_agent_executor_class, |
| 688 | + caplog, |
| 689 | + ): |
| 690 | + """A provided agent_card plus a non-empty rpc_path logs a mismatch warning.""" |
| 691 | + mock_starlette_class.return_value = Mock(spec=Starlette) |
| 692 | + mock_agent_executor_class.return_value = Mock(spec=A2aAgentExecutor) |
| 693 | + mock_card_builder_class.return_value = Mock(spec=AgentCardBuilder) |
| 694 | + |
| 695 | + with caplog.at_level(logging.WARNING, logger="google_adk"): |
| 696 | + to_a2a( |
| 697 | + self.mock_agent, |
| 698 | + rpc_path="/analysis-agent", |
| 699 | + agent_card=_make_minimal_agent_card(), |
| 700 | + ) |
| 701 | + |
| 702 | + assert any( |
| 703 | + "agent_card and rpc_path" in r.message for r in caplog.records |
| 704 | + ), f"expected mismatch warning; got {[r.message for r in caplog.records]}" |
| 705 | + |
563 | 706 | @patch("google.adk.a2a.utils.agent_to_a2a.A2aAgentExecutor") |
564 | 707 | @patch("google.adk.a2a.utils.agent_to_a2a.InMemoryTaskStore") |
565 | 708 | @patch("google.adk.a2a.utils.agent_to_a2a.AgentCardBuilder") |
|
0 commit comments