diff --git a/src/google/adk/agents/remote_a2a_agent.py b/src/google/adk/agents/remote_a2a_agent.py index 52eb5b59c0a..929b0ac62d8 100644 --- a/src/google/adk/agents/remote_a2a_agent.py +++ b/src/google/adk/agents/remote_a2a_agent.py @@ -990,10 +990,13 @@ async def _ensure_resolved( if not self._agent_card: # Resolve agent card if needed - self._agent_card = await self._resolve_agent_card(ctx) + resolved_agent_card = await self._resolve_agent_card(ctx) - # Validate agent card - await self._validate_agent_card(self._agent_card) + # Validate agent card before caching it. If validation fails, the + # card must not be cached, or a subsequent invocation would skip + # resolution and validation entirely and reuse the invalid card. + await self._validate_agent_card(resolved_agent_card) + self._agent_card = resolved_agent_card # Update description if empty if not self.description and self._agent_card.description: diff --git a/tests/unittests/agents/test_remote_a2a_agent.py b/tests/unittests/agents/test_remote_a2a_agent.py index 975b4ae0583..5a41a7fbda8 100644 --- a/tests/unittests/agents/test_remote_a2a_agent.py +++ b/tests/unittests/agents/test_remote_a2a_agent.py @@ -697,6 +697,47 @@ async def test_ensure_resolved_caches_card_without_interceptor(self): assert mock_resolve.await_count == 1 + @pytest.mark.asyncio + async def test_ensure_resolved_does_not_cache_card_on_validation_failure( + self, + ): + """A card that fails validation must not be cached. + + Otherwise a later invocation sees `self._agent_card` already set, skips + resolution and validation entirely, and reuses the invalid card. + """ + agent = RemoteA2aAgent( + name="test_agent", + agent_card="https://example.com/agent.json", + ) + + with patch.object( + agent, "_resolve_agent_card", new_callable=AsyncMock + ) as mock_resolve: + mock_resolve.return_value = self.agent_card + with patch.object( + agent, "_validate_agent_card", new_callable=AsyncMock + ) as mock_validate: + mock_validate.side_effect = ValueError("invalid rpc url") + + with pytest.raises(AgentCardResolutionError): + await agent._ensure_resolved(Mock()) + + assert agent._agent_card is None + + mock_validate.side_effect = None + with patch.object(agent, "_ensure_httpx_client") as mock_ensure: + mock_ensure.return_value = AsyncMock() + mock_factory = Mock() + mock_factory.create.return_value = Mock() + agent._a2a_client_factory = mock_factory + + await agent._ensure_resolved(Mock()) + + assert mock_resolve.await_count == 2 + assert mock_validate.await_count == 2 + assert agent._agent_card is self.agent_card + @pytest.mark.asyncio async def test_ensure_resolved_without_ctx_uses_cached_path(self): """_ensure_resolved() is callable with no ctx (backward compatible)."""