ec2b666284
Continuous Integration / Pre-commit Linter (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.10) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.11) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.12) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.12) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.14) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.12) (push) Has been cancelled
Copybara PR Handler / close-imported-pr (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.14) (push) Has been cancelled
460 lines
14 KiB
Python
460 lines
14 KiB
Python
# Copyright 2026 Google LLC
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
from unittest.mock import Mock
|
|
|
|
from a2a.server.agent_execution import RequestContext
|
|
from google.adk.a2a import _compat
|
|
from google.adk.a2a.converters.request_converter import _get_user_id
|
|
from google.adk.a2a.converters.request_converter import convert_a2a_request_to_agent_run_request
|
|
from google.adk.runners import RunConfig
|
|
from google.genai import types as genai_types
|
|
import pytest
|
|
|
|
|
|
class TestGetUserId:
|
|
"""Test cases for _get_user_id function."""
|
|
|
|
def test_get_user_id_from_call_context(self):
|
|
"""Test getting user ID from call context when auth is enabled."""
|
|
# Arrange
|
|
mock_user = Mock()
|
|
mock_user.user_name = "authenticated_user"
|
|
|
|
mock_call_context = Mock()
|
|
mock_call_context.user = mock_user
|
|
|
|
request = Mock(spec=RequestContext)
|
|
request.call_context = mock_call_context
|
|
request.context_id = "test_context"
|
|
|
|
# Act
|
|
result = _get_user_id(request)
|
|
|
|
# Assert
|
|
assert result == "authenticated_user"
|
|
|
|
def test_get_user_id_from_context_when_no_call_context(self):
|
|
"""Test getting user ID from context when call context is not available."""
|
|
# Arrange
|
|
request = Mock(spec=RequestContext)
|
|
request.call_context = None
|
|
request.context_id = "test_context"
|
|
|
|
# Act
|
|
result = _get_user_id(request)
|
|
|
|
# Assert
|
|
assert result == "A2A_USER_test_context"
|
|
|
|
def test_get_user_id_from_context_when_call_context_has_no_user(self):
|
|
"""Test getting user ID from context when call context has no user."""
|
|
# Arrange
|
|
mock_call_context = Mock()
|
|
mock_call_context.user = None
|
|
|
|
request = Mock(spec=RequestContext)
|
|
request.call_context = mock_call_context
|
|
request.context_id = "test_context"
|
|
|
|
# Act
|
|
result = _get_user_id(request)
|
|
|
|
# Assert
|
|
assert result == "A2A_USER_test_context"
|
|
|
|
def test_get_user_id_with_empty_user_name(self):
|
|
"""Test getting user ID when user exists but user_name is empty."""
|
|
# Arrange
|
|
mock_user = Mock()
|
|
mock_user.user_name = ""
|
|
|
|
mock_call_context = Mock()
|
|
mock_call_context.user = mock_user
|
|
|
|
request = Mock(spec=RequestContext)
|
|
request.call_context = mock_call_context
|
|
request.context_id = "test_context"
|
|
|
|
# Act
|
|
result = _get_user_id(request)
|
|
|
|
# Assert
|
|
assert result == "A2A_USER_test_context"
|
|
|
|
def test_get_user_id_with_none_user_name(self):
|
|
"""Test getting user ID when user exists but user_name is None."""
|
|
# Arrange
|
|
mock_user = Mock()
|
|
mock_user.user_name = None
|
|
|
|
mock_call_context = Mock()
|
|
mock_call_context.user = mock_user
|
|
|
|
request = Mock(spec=RequestContext)
|
|
request.call_context = mock_call_context
|
|
request.context_id = "test_context"
|
|
|
|
# Act
|
|
result = _get_user_id(request)
|
|
|
|
# Assert
|
|
assert result == "A2A_USER_test_context"
|
|
|
|
def test_get_user_id_with_none_context_id(self):
|
|
"""Test getting user ID when context_id is None."""
|
|
# Arrange
|
|
request = Mock(spec=RequestContext)
|
|
request.call_context = None
|
|
request.context_id = None
|
|
|
|
# Act
|
|
result = _get_user_id(request)
|
|
|
|
# Assert
|
|
assert result == "A2A_USER_None"
|
|
|
|
|
|
class TestConvertA2aRequestToAgentRunRequest:
|
|
"""Test cases for convert_a2a_request_to_agent_run_request function."""
|
|
|
|
def test_convert_a2a_request_basic(self):
|
|
"""Test basic conversion of A2A request to ADK AgentRunRequest."""
|
|
# Arrange
|
|
mock_part1 = Mock()
|
|
mock_part2 = Mock()
|
|
|
|
mock_message = Mock()
|
|
mock_message.parts = [mock_part1, mock_part2]
|
|
|
|
mock_user = Mock()
|
|
mock_user.user_name = "test_user"
|
|
|
|
mock_call_context = Mock()
|
|
mock_call_context.user = mock_user
|
|
|
|
request = Mock(spec=RequestContext)
|
|
request.message = mock_message
|
|
request.context_id = "test_context_123"
|
|
request.call_context = mock_call_context
|
|
request.metadata = {"test_key": "test_value"}
|
|
|
|
# Create proper genai_types.Part objects instead of mocks
|
|
mock_genai_part1 = genai_types.Part(text="test part 1")
|
|
mock_genai_part2 = genai_types.Part(text="test part 2")
|
|
mock_convert_part = Mock()
|
|
mock_convert_part.side_effect = [mock_genai_part1, mock_genai_part2]
|
|
|
|
# Act
|
|
result = convert_a2a_request_to_agent_run_request(
|
|
request, mock_convert_part
|
|
)
|
|
|
|
# Assert
|
|
assert result is not None
|
|
assert result.user_id == "test_user"
|
|
assert result.session_id == "test_context_123"
|
|
assert isinstance(result.new_message, genai_types.Content)
|
|
assert result.new_message.role == "user"
|
|
assert result.new_message.parts == [mock_genai_part1, mock_genai_part2]
|
|
assert isinstance(result.run_config, RunConfig)
|
|
assert result.run_config.custom_metadata == {
|
|
"a2a_metadata": {"test_key": "test_value"}
|
|
}
|
|
|
|
# Verify calls
|
|
assert mock_convert_part.call_count == 2
|
|
mock_convert_part.assert_any_call(mock_part1)
|
|
mock_convert_part.assert_any_call(mock_part2)
|
|
|
|
def test_convert_a2a_request_multiple_parts(self):
|
|
"""Test basic conversion of A2A request to ADK AgentRunRequest."""
|
|
# Arrange
|
|
mock_part1 = Mock()
|
|
mock_part2 = Mock()
|
|
|
|
mock_message = Mock()
|
|
mock_message.parts = [mock_part1, mock_part2]
|
|
|
|
mock_user = Mock()
|
|
mock_user.user_name = "test_user"
|
|
|
|
mock_call_context = Mock()
|
|
mock_call_context.user = mock_user
|
|
|
|
request = Mock(spec=RequestContext)
|
|
request.message = mock_message
|
|
request.context_id = "test_context_123"
|
|
request.call_context = mock_call_context
|
|
request.metadata = {"test_key": "test_value"}
|
|
|
|
# Create proper genai_types.Part objects instead of mocks
|
|
mock_genai_part1 = genai_types.Part(text="test part 1")
|
|
mock_genai_part2 = genai_types.Part(text="test part 2")
|
|
mock_convert_part = Mock()
|
|
mock_convert_part.side_effect = [mock_genai_part1, mock_genai_part2]
|
|
|
|
# Act
|
|
result = convert_a2a_request_to_agent_run_request(
|
|
request, mock_convert_part
|
|
)
|
|
|
|
# Assert
|
|
assert result is not None
|
|
assert result.user_id == "test_user"
|
|
assert result.session_id == "test_context_123"
|
|
assert isinstance(result.new_message, genai_types.Content)
|
|
assert result.new_message.role == "user"
|
|
assert result.new_message.parts == [
|
|
mock_genai_part1,
|
|
mock_genai_part2,
|
|
]
|
|
assert isinstance(result.run_config, RunConfig)
|
|
assert result.run_config.custom_metadata == {
|
|
"a2a_metadata": {"test_key": "test_value"}
|
|
}
|
|
|
|
# Verify calls
|
|
assert mock_convert_part.call_count == 2
|
|
mock_convert_part.assert_any_call(mock_part1)
|
|
mock_convert_part.assert_any_call(mock_part2)
|
|
|
|
def test_convert_a2a_request_normalizes_proto_struct_metadata(self):
|
|
"""Request metadata is normalized to a plain dict across SDK versions."""
|
|
# Build metadata in the native shape of the active SDK (proto Struct on 1.x,
|
|
# plain dict on 0.3.x) by writing onto a real a2a Message.
|
|
message_with_meta = _compat.make_message(
|
|
message_id="m1", role=_compat.ROLE_USER, parts=[]
|
|
)
|
|
_compat.set_struct_metadata(
|
|
message_with_meta, {"test_key": "test_value", "n": 1}
|
|
)
|
|
|
|
mock_message = Mock()
|
|
mock_message.parts = []
|
|
|
|
request = Mock(spec=RequestContext)
|
|
request.message = mock_message
|
|
request.context_id = "ctx"
|
|
request.call_context = None
|
|
request.metadata = message_with_meta.metadata
|
|
|
|
result = convert_a2a_request_to_agent_run_request(request, Mock())
|
|
|
|
stored = result.run_config.custom_metadata["a2a_metadata"]
|
|
# Must be a plain dict (not a proto Struct) regardless of SDK version.
|
|
assert isinstance(stored, dict)
|
|
assert stored["test_key"] == "test_value"
|
|
# Numbers may come back as float on 1.x (proto Struct) -> tolerate both.
|
|
assert float(stored["n"]) == 1.0
|
|
|
|
def test_convert_a2a_request_empty_metadata_omitted(self):
|
|
"""Empty request metadata must not add an ``a2a_metadata`` entry."""
|
|
empty_meta_msg = _compat.make_message(
|
|
message_id="m1", role=_compat.ROLE_USER, parts=[]
|
|
)
|
|
|
|
mock_message = Mock()
|
|
mock_message.parts = []
|
|
|
|
request = Mock(spec=RequestContext)
|
|
request.message = mock_message
|
|
request.context_id = "ctx"
|
|
request.call_context = None
|
|
request.metadata = empty_meta_msg.metadata
|
|
|
|
result = convert_a2a_request_to_agent_run_request(request, Mock())
|
|
|
|
assert "a2a_metadata" not in result.run_config.custom_metadata
|
|
|
|
def test_convert_a2a_request_no_message_raises_error(self):
|
|
"""Test that conversion raises ValueError when message is None."""
|
|
# Arrange
|
|
request = Mock(spec=RequestContext)
|
|
request.message = None
|
|
|
|
# Act & Assert
|
|
with pytest.raises(ValueError, match="Request message cannot be None"):
|
|
convert_a2a_request_to_agent_run_request(request)
|
|
|
|
def test_convert_a2a_request_empty_parts(self):
|
|
"""Test conversion with empty parts list."""
|
|
# Arrange
|
|
mock_message = Mock()
|
|
mock_message.parts = []
|
|
mock_convert_part = Mock()
|
|
|
|
request = Mock(spec=RequestContext)
|
|
request.message = mock_message
|
|
request.context_id = "test_context_123"
|
|
request.call_context = None
|
|
request.metadata = {}
|
|
|
|
# Act
|
|
result = convert_a2a_request_to_agent_run_request(
|
|
request, mock_convert_part
|
|
)
|
|
|
|
# Assert
|
|
assert result is not None
|
|
assert result.user_id == "A2A_USER_test_context_123"
|
|
assert result.session_id == "test_context_123"
|
|
assert isinstance(result.new_message, genai_types.Content)
|
|
assert result.new_message.role == "user"
|
|
assert result.new_message.parts == []
|
|
assert isinstance(result.run_config, RunConfig)
|
|
|
|
# Verify convert_part wasn't called
|
|
mock_convert_part.assert_not_called()
|
|
|
|
def test_convert_a2a_request_none_context_id(self):
|
|
"""Test conversion when context_id is None."""
|
|
# Arrange
|
|
mock_part = Mock()
|
|
mock_message = Mock()
|
|
mock_message.parts = [mock_part]
|
|
|
|
request = Mock(spec=RequestContext)
|
|
request.message = mock_message
|
|
request.context_id = None
|
|
request.call_context = None
|
|
request.metadata = {}
|
|
|
|
# Create proper genai_types.Part object instead of mock
|
|
mock_genai_part = genai_types.Part(text="test part")
|
|
mock_convert_part = Mock()
|
|
mock_convert_part.return_value = mock_genai_part
|
|
|
|
# Act
|
|
result = convert_a2a_request_to_agent_run_request(
|
|
request, mock_convert_part
|
|
)
|
|
|
|
# Assert
|
|
assert result is not None
|
|
assert result.user_id == "A2A_USER_None"
|
|
assert result.session_id is None
|
|
assert isinstance(result.new_message, genai_types.Content)
|
|
assert result.new_message.role == "user"
|
|
assert result.new_message.parts == [mock_genai_part]
|
|
assert isinstance(result.run_config, RunConfig)
|
|
|
|
def test_convert_a2a_request_no_auth(self):
|
|
"""Test conversion when no authentication is available."""
|
|
# Arrange
|
|
mock_part = Mock()
|
|
mock_message = Mock()
|
|
mock_message.parts = [mock_part]
|
|
|
|
request = Mock(spec=RequestContext)
|
|
request.message = mock_message
|
|
request.context_id = "session_123"
|
|
request.call_context = None
|
|
request.metadata = {}
|
|
|
|
# Create proper genai_types.Part object instead of mock
|
|
mock_genai_part = genai_types.Part(text="test part")
|
|
mock_convert_part = Mock()
|
|
mock_convert_part.return_value = mock_genai_part
|
|
|
|
# Act
|
|
result = convert_a2a_request_to_agent_run_request(
|
|
request, mock_convert_part
|
|
)
|
|
|
|
# Assert
|
|
assert result is not None
|
|
assert result.user_id == "A2A_USER_session_123"
|
|
assert result.session_id == "session_123"
|
|
assert isinstance(result.new_message, genai_types.Content)
|
|
assert result.new_message.role == "user"
|
|
assert result.new_message.parts == [mock_genai_part]
|
|
assert isinstance(result.run_config, RunConfig)
|
|
|
|
|
|
class TestIntegration:
|
|
"""Integration test cases combining both functions."""
|
|
|
|
def test_end_to_end_conversion_with_auth_user(self):
|
|
"""Test end-to-end conversion with authenticated user."""
|
|
# Arrange
|
|
mock_user = Mock()
|
|
mock_user.user_name = "auth_user"
|
|
|
|
mock_call_context = Mock()
|
|
mock_call_context.user = mock_user
|
|
|
|
mock_part = Mock()
|
|
mock_message = Mock()
|
|
mock_message.parts = [mock_part]
|
|
|
|
request = Mock(spec=RequestContext)
|
|
request.call_context = mock_call_context
|
|
request.message = mock_message
|
|
request.context_id = "mysession"
|
|
request.metadata = {}
|
|
|
|
# Create proper genai_types.Part object instead of mock
|
|
mock_genai_part = genai_types.Part(text="test part")
|
|
mock_convert_part = Mock()
|
|
mock_convert_part.return_value = mock_genai_part
|
|
|
|
# Act
|
|
result = convert_a2a_request_to_agent_run_request(
|
|
request, mock_convert_part
|
|
)
|
|
|
|
# Assert
|
|
assert result is not None
|
|
assert result.user_id == "auth_user" # Should use authenticated user
|
|
assert result.session_id == "mysession"
|
|
assert isinstance(result.new_message, genai_types.Content)
|
|
assert result.new_message.role == "user"
|
|
assert result.new_message.parts == [mock_genai_part]
|
|
assert isinstance(result.run_config, RunConfig)
|
|
|
|
def test_end_to_end_conversion_with_fallback_user(self):
|
|
"""Test end-to-end conversion with fallback user ID."""
|
|
# Arrange
|
|
mock_part = Mock()
|
|
mock_message = Mock()
|
|
mock_message.parts = [mock_part]
|
|
|
|
request = Mock(spec=RequestContext)
|
|
request.call_context = None
|
|
request.message = mock_message
|
|
request.context_id = "test_session_456"
|
|
request.metadata = {}
|
|
|
|
# Create proper genai_types.Part object instead of mock
|
|
mock_genai_part = genai_types.Part(text="test part")
|
|
mock_convert_part = Mock()
|
|
mock_convert_part.return_value = mock_genai_part
|
|
|
|
# Act
|
|
result = convert_a2a_request_to_agent_run_request(
|
|
request, mock_convert_part
|
|
)
|
|
|
|
# Assert
|
|
assert result is not None
|
|
assert (
|
|
result.user_id == "A2A_USER_test_session_456"
|
|
) # Should fall back to context ID
|
|
assert result.session_id == "test_session_456"
|
|
assert isinstance(result.new_message, genai_types.Content)
|
|
assert result.new_message.role == "user"
|
|
assert result.new_message.parts == [mock_genai_part]
|
|
assert isinstance(result.run_config, RunConfig)
|