chore: import upstream snapshot with attribution
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
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
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
# 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.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,208 @@
|
||||
# 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.
|
||||
|
||||
"""Round trip tests for ADK and A2A event converters."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Dict
|
||||
from unittest.mock import Mock
|
||||
|
||||
from a2a.types import TaskArtifactUpdateEvent
|
||||
from a2a.types import TaskStatusUpdateEvent
|
||||
from google.adk.a2a.converters.from_adk_event import convert_event_to_a2a_events
|
||||
from google.adk.a2a.converters.from_adk_event import create_error_status_event
|
||||
from google.adk.a2a.converters.to_adk_event import convert_a2a_artifact_update_to_event
|
||||
from google.adk.a2a.converters.to_adk_event import convert_a2a_status_update_to_event
|
||||
from google.adk.agents.invocation_context import InvocationContext
|
||||
from google.adk.events.event import Event
|
||||
from google.genai import types as genai_types
|
||||
|
||||
|
||||
def test_round_trip_text_event():
|
||||
original_event = Event(
|
||||
invocation_id="test_invocation",
|
||||
author="test_agent",
|
||||
branch="main",
|
||||
content=genai_types.Content(
|
||||
role="model",
|
||||
parts=[genai_types.Part.from_text(text="Hello world!")],
|
||||
),
|
||||
partial=False,
|
||||
)
|
||||
agents_artifacts: Dict[str, str] = {}
|
||||
|
||||
a2a_events = convert_event_to_a2a_events(
|
||||
event=original_event,
|
||||
agents_artifacts=agents_artifacts,
|
||||
task_id="task1",
|
||||
context_id="context1",
|
||||
)
|
||||
|
||||
assert len(a2a_events) == 1
|
||||
a2a_event = a2a_events[0]
|
||||
assert isinstance(a2a_event, TaskArtifactUpdateEvent)
|
||||
|
||||
mock_context = Mock(
|
||||
spec=InvocationContext, invocation_id="test_invocation", branch="main"
|
||||
)
|
||||
|
||||
restored_event = convert_a2a_artifact_update_to_event(
|
||||
a2a_artifact_update=a2a_event,
|
||||
author="test_agent",
|
||||
invocation_context=mock_context,
|
||||
)
|
||||
|
||||
assert restored_event is not None
|
||||
assert restored_event.author == original_event.author
|
||||
assert restored_event.invocation_id == original_event.invocation_id
|
||||
assert restored_event.branch == original_event.branch
|
||||
assert restored_event.partial == original_event.partial
|
||||
assert len(restored_event.content.parts) == len(original_event.content.parts)
|
||||
assert (
|
||||
restored_event.content.parts[0].text
|
||||
== original_event.content.parts[0].text
|
||||
)
|
||||
|
||||
|
||||
def test_round_trip_error_status_event():
|
||||
original_event = Event(
|
||||
invocation_id="error_inv",
|
||||
author="error_agent",
|
||||
branch="main",
|
||||
error_message="Test Error",
|
||||
)
|
||||
|
||||
a2a_event = create_error_status_event(
|
||||
event=original_event,
|
||||
task_id="task2",
|
||||
context_id="ctx2",
|
||||
)
|
||||
|
||||
assert isinstance(a2a_event, TaskStatusUpdateEvent)
|
||||
|
||||
mock_context = Mock(
|
||||
spec=InvocationContext, invocation_id="error_inv", branch="main"
|
||||
)
|
||||
|
||||
restored_event = convert_a2a_status_update_to_event(
|
||||
a2a_status_update=a2a_event,
|
||||
author="error_agent",
|
||||
invocation_context=mock_context,
|
||||
)
|
||||
|
||||
assert restored_event is not None
|
||||
assert restored_event.author == original_event.author
|
||||
assert restored_event.invocation_id == original_event.invocation_id
|
||||
assert restored_event.branch == original_event.branch
|
||||
assert len(restored_event.content.parts) == 1
|
||||
assert restored_event.content.parts[0].text == "Test Error"
|
||||
|
||||
|
||||
def test_round_trip_function_call_event():
|
||||
original_event = Event(
|
||||
invocation_id="test_invocation",
|
||||
author="test_agent",
|
||||
branch="main",
|
||||
content=genai_types.Content(
|
||||
role="model",
|
||||
parts=[
|
||||
genai_types.Part.from_function_call(
|
||||
name="my_function",
|
||||
args={"arg1": "value1"},
|
||||
)
|
||||
],
|
||||
),
|
||||
partial=False,
|
||||
)
|
||||
agents_artifacts: Dict[str, str] = {}
|
||||
|
||||
a2a_events = convert_event_to_a2a_events(
|
||||
event=original_event,
|
||||
agents_artifacts=agents_artifacts,
|
||||
task_id="task1",
|
||||
context_id="context1",
|
||||
)
|
||||
|
||||
assert len(a2a_events) == 1
|
||||
a2a_event = a2a_events[0]
|
||||
|
||||
mock_context = Mock(
|
||||
spec=InvocationContext, invocation_id="test_invocation", branch="main"
|
||||
)
|
||||
|
||||
restored_event = convert_a2a_artifact_update_to_event(
|
||||
a2a_artifact_update=a2a_event,
|
||||
author="test_agent",
|
||||
invocation_context=mock_context,
|
||||
)
|
||||
|
||||
assert restored_event is not None
|
||||
assert restored_event.author == original_event.author
|
||||
assert restored_event.invocation_id == original_event.invocation_id
|
||||
assert restored_event.branch == original_event.branch
|
||||
assert len(restored_event.content.parts) == 1
|
||||
assert restored_event.content.parts[0].function_call.name == "my_function"
|
||||
assert restored_event.content.parts[0].function_call.args == {
|
||||
"arg1": "value1"
|
||||
}
|
||||
|
||||
|
||||
def test_round_trip_function_response_event():
|
||||
original_event = Event(
|
||||
invocation_id="test_invocation",
|
||||
author="test_agent",
|
||||
branch="main",
|
||||
content=genai_types.Content(
|
||||
role="user",
|
||||
parts=[
|
||||
genai_types.Part.from_function_response(
|
||||
name="my_function",
|
||||
response={"result": "success"},
|
||||
)
|
||||
],
|
||||
),
|
||||
partial=False,
|
||||
)
|
||||
agents_artifacts: Dict[str, str] = {}
|
||||
|
||||
a2a_events = convert_event_to_a2a_events(
|
||||
event=original_event,
|
||||
agents_artifacts=agents_artifacts,
|
||||
task_id="task1",
|
||||
context_id="context1",
|
||||
)
|
||||
|
||||
assert len(a2a_events) == 1
|
||||
a2a_event = a2a_events[0]
|
||||
|
||||
mock_context = Mock(
|
||||
spec=InvocationContext, invocation_id="test_invocation", branch="main"
|
||||
)
|
||||
|
||||
restored_event = convert_a2a_artifact_update_to_event(
|
||||
a2a_artifact_update=a2a_event,
|
||||
author="test_agent",
|
||||
invocation_context=mock_context,
|
||||
)
|
||||
|
||||
assert restored_event is not None
|
||||
assert restored_event.author == original_event.author
|
||||
assert restored_event.invocation_id == original_event.invocation_id
|
||||
assert restored_event.branch == original_event.branch
|
||||
assert len(restored_event.content.parts) == 1
|
||||
assert restored_event.content.parts[0].function_response.name == "my_function"
|
||||
assert restored_event.content.parts[0].function_response.response == {
|
||||
"result": "success"
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
# 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 __future__ import annotations
|
||||
|
||||
from unittest.mock import Mock
|
||||
|
||||
from a2a.types import TaskArtifactUpdateEvent
|
||||
from a2a.types import TaskStatusUpdateEvent
|
||||
from google.adk.a2a import _compat
|
||||
from google.adk.a2a.converters.from_adk_event import convert_event_to_a2a_events
|
||||
from google.adk.events import event_actions
|
||||
from google.adk.events.event import Event
|
||||
from google.genai import types as genai_types
|
||||
import pytest
|
||||
|
||||
|
||||
class TestFromAdk:
|
||||
"""Test suite for from_adk functions."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Set up test fixtures."""
|
||||
self.mock_event = Mock(spec=Event)
|
||||
self.mock_event.id = "test-event-id"
|
||||
self.mock_event.invocation_id = "test-invocation-id"
|
||||
self.mock_event.author = "test-author"
|
||||
self.mock_event.branch = None
|
||||
self.mock_event.content = None
|
||||
self.mock_event.error_code = None
|
||||
self.mock_event.error_message = None
|
||||
self.mock_event.grounding_metadata = None
|
||||
self.mock_event.citation_metadata = None
|
||||
self.mock_event.custom_metadata = None
|
||||
self.mock_event.usage_metadata = None
|
||||
self.mock_event.actions = None
|
||||
self.mock_event.partial = True
|
||||
self.mock_event.long_running_tool_ids = None
|
||||
|
||||
def test_convert_event_to_a2a_events_artifact_update(self):
|
||||
"""Test conversion of event to TaskArtifactUpdateEvent."""
|
||||
# Setup event with content
|
||||
self.mock_event.content = genai_types.Content(
|
||||
parts=[genai_types.Part(text="hello")], role="model"
|
||||
)
|
||||
self.mock_event.author = "agent-1"
|
||||
|
||||
agents_artifacts = {}
|
||||
|
||||
# Mock part converter to return a standard text part
|
||||
mock_a2a_part = _compat.make_text_part("hello")
|
||||
mock_convert_part = Mock(return_value=[mock_a2a_part])
|
||||
|
||||
result = convert_event_to_a2a_events(
|
||||
self.mock_event,
|
||||
agents_artifacts,
|
||||
task_id="task-123",
|
||||
context_id="context-456",
|
||||
part_converter=mock_convert_part,
|
||||
)
|
||||
|
||||
assert len(result) == 1
|
||||
assert isinstance(result[0], TaskArtifactUpdateEvent)
|
||||
assert result[0].task_id == "task-123"
|
||||
assert result[0].context_id == "context-456"
|
||||
assert result[0].artifact.parts == [mock_a2a_part]
|
||||
assert "agent-1" in agents_artifacts # Artifact ID should be stored
|
||||
|
||||
def test_convert_event_to_a2a_events_error(self):
|
||||
"""Test conversion of event with error to TaskStatusUpdateEvent."""
|
||||
self.mock_event.error_code = "ERR001"
|
||||
self.mock_event.error_message = "Something went wrong"
|
||||
|
||||
agents_artifacts = {}
|
||||
|
||||
result = convert_event_to_a2a_events(
|
||||
self.mock_event,
|
||||
agents_artifacts,
|
||||
task_id="task-123",
|
||||
context_id="context-456",
|
||||
)
|
||||
|
||||
# Should not return any artifact events
|
||||
assert len(result) == 0
|
||||
|
||||
def test_convert_event_to_a2a_events_none_event(self):
|
||||
"""Test convert_event_to_a2a_events with None event."""
|
||||
with pytest.raises(ValueError, match="Event cannot be None"):
|
||||
convert_event_to_a2a_events(None, {})
|
||||
|
||||
def test_convert_event_to_a2a_events_none_artifacts(self):
|
||||
"""Test convert_event_to_a2a_events with None agents_artifacts."""
|
||||
with pytest.raises(ValueError, match="Agents artifacts cannot be None"):
|
||||
convert_event_to_a2a_events(self.mock_event, None)
|
||||
|
||||
def test_convert_event_to_a2a_events_with_actions(self):
|
||||
"""Test conversion of event with actions to TaskStatusUpdateEvent."""
|
||||
self.mock_event.actions = event_actions.EventActions()
|
||||
self.mock_event.actions.artifact_delta["image"] = 0
|
||||
|
||||
agents_artifacts = {}
|
||||
|
||||
result = convert_event_to_a2a_events(
|
||||
self.mock_event,
|
||||
agents_artifacts,
|
||||
task_id="task-123",
|
||||
context_id="context-456",
|
||||
)
|
||||
|
||||
assert len(result) == 1
|
||||
assert isinstance(result[0], TaskStatusUpdateEvent)
|
||||
assert result[0].task_id == "task-123"
|
||||
assert result[0].context_id == "context-456"
|
||||
|
||||
metadata = result[0].status.message.metadata
|
||||
assert "adk_actions" in metadata
|
||||
assert metadata["adk_actions"]["artifactDelta"] == {"image": 0}
|
||||
|
||||
|
||||
class TestSerializeValue:
|
||||
"""Tests for _serialize_value preserving JSON-native types."""
|
||||
|
||||
def setup_method(self) -> None:
|
||||
from google.adk.a2a.converters.from_adk_event import _serialize_value
|
||||
|
||||
self.serialize = _serialize_value
|
||||
|
||||
def test_dict_preserved(self) -> None:
|
||||
value = {"key": "val", "nested": {"a": 1}}
|
||||
result = self.serialize(value)
|
||||
assert result == value
|
||||
assert isinstance(result, dict)
|
||||
|
||||
def test_list_preserved(self) -> None:
|
||||
value = [1, "two", {"three": 3}]
|
||||
result = self.serialize(value)
|
||||
assert result == value
|
||||
assert isinstance(result, list)
|
||||
|
||||
def test_int_preserved(self) -> None:
|
||||
result = self.serialize(42)
|
||||
assert result == 42
|
||||
assert isinstance(result, int)
|
||||
|
||||
def test_float_preserved(self) -> None:
|
||||
result = self.serialize(3.14)
|
||||
assert result == 3.14
|
||||
assert isinstance(result, float)
|
||||
|
||||
def test_bool_preserved(self) -> None:
|
||||
assert self.serialize(True) is True
|
||||
assert self.serialize(False) is False
|
||||
|
||||
def test_string_preserved(self) -> None:
|
||||
assert self.serialize("hello") == "hello"
|
||||
|
||||
def test_none_returns_none(self) -> None:
|
||||
assert self.serialize(None) is None
|
||||
|
||||
def test_non_json_type_stringified(self) -> None:
|
||||
"""Non-JSON-native types should still be converted to str."""
|
||||
from datetime import datetime
|
||||
|
||||
dt = datetime(2025, 1, 1)
|
||||
result = self.serialize(dt)
|
||||
assert isinstance(result, str)
|
||||
|
||||
def test_nested_non_json_value_in_dict_stringified(self) -> None:
|
||||
"""A non-JSON-native value nested in a dict is stringified."""
|
||||
from datetime import datetime
|
||||
|
||||
dt = datetime(2025, 1, 1)
|
||||
value = {"when": dt, "count": 1, "label": "x"}
|
||||
result = self.serialize(value)
|
||||
assert result == {"when": str(dt), "count": 1, "label": "x"}
|
||||
assert isinstance(result["when"], str)
|
||||
assert isinstance(result["count"], int)
|
||||
assert isinstance(result["label"], str)
|
||||
|
||||
def test_nested_non_json_value_in_list_stringified(self) -> None:
|
||||
"""A non-JSON-native value nested in a list is stringified."""
|
||||
from datetime import datetime
|
||||
|
||||
dt = datetime(2025, 1, 1)
|
||||
value = [dt, 1, "x"]
|
||||
result = self.serialize(value)
|
||||
assert result == [str(dt), 1, "x"]
|
||||
assert isinstance(result[0], str)
|
||||
assert isinstance(result[1], int)
|
||||
assert isinstance(result[2], str)
|
||||
|
||||
def test_non_string_dict_key_stringified(self) -> None:
|
||||
"""Non-string dict keys are stringified so the result is JSON-encodable."""
|
||||
from datetime import datetime
|
||||
|
||||
dt = datetime(2025, 1, 1)
|
||||
value = {dt: "when", 1: "one", "label": "x"}
|
||||
result = self.serialize(value)
|
||||
assert result == {str(dt): "when", "1": "one", "label": "x"}
|
||||
assert all(isinstance(k, str) for k in result)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,459 @@
|
||||
# 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)
|
||||
@@ -0,0 +1,682 @@
|
||||
# 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 __future__ import annotations
|
||||
|
||||
import json
|
||||
from unittest.mock import Mock
|
||||
|
||||
from a2a.types import Message
|
||||
from a2a.types import Part as A2APart
|
||||
from a2a.types import Task
|
||||
from a2a.types import TaskArtifactUpdateEvent
|
||||
from google.adk.a2a import _compat
|
||||
from google.adk.a2a.converters.part_converter import A2A_DATA_PART_END_TAG
|
||||
from google.adk.a2a.converters.part_converter import A2A_DATA_PART_METADATA_IS_LONG_RUNNING_KEY
|
||||
from google.adk.a2a.converters.part_converter import A2A_DATA_PART_START_TAG
|
||||
from google.adk.a2a.converters.part_converter import A2A_DATA_PART_TEXT_MIME_TYPE
|
||||
from google.adk.a2a.converters.to_adk_event import convert_a2a_artifact_update_to_event
|
||||
from google.adk.a2a.converters.to_adk_event import convert_a2a_message_to_event
|
||||
from google.adk.a2a.converters.to_adk_event import convert_a2a_status_update_to_event
|
||||
from google.adk.a2a.converters.to_adk_event import convert_a2a_task_to_event
|
||||
from google.adk.a2a.converters.to_adk_event import MOCK_FUNCTION_CALL_FOR_REQUIRED_USER_AUTH
|
||||
from google.adk.a2a.converters.to_adk_event import MOCK_FUNCTION_CALL_FOR_REQUIRED_USER_INPUT
|
||||
from google.adk.a2a.converters.utils import _get_adk_metadata_key
|
||||
from google.adk.agents.invocation_context import InvocationContext
|
||||
from google.genai import types as genai_types
|
||||
import pytest
|
||||
|
||||
|
||||
def _make_a2a_part_for_test(metadata=None):
|
||||
"""Returns a real proto Part on 1.x, or Mock(spec=A2APart) on 0.3."""
|
||||
if _compat.IS_A2A_V1:
|
||||
p = _compat.make_text_part("test")
|
||||
if metadata:
|
||||
_compat.set_part_metadata(p, metadata)
|
||||
return p
|
||||
else:
|
||||
from unittest.mock import Mock
|
||||
|
||||
from a2a.types import TextPart
|
||||
|
||||
m = Mock(spec=A2APart)
|
||||
m.root = Mock(spec=TextPart)
|
||||
m.root.metadata = metadata or {}
|
||||
return m
|
||||
|
||||
|
||||
class TestToAdk:
|
||||
"""Test suite for to_adk functions."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Set up test fixtures."""
|
||||
self.mock_context = Mock(spec=InvocationContext)
|
||||
self.mock_context.invocation_id = "test-invocation"
|
||||
self.mock_context.branch = "test-branch"
|
||||
|
||||
def test_convert_a2a_message_to_event_success(self):
|
||||
"""Test successful conversion of A2A message to Event."""
|
||||
a2a_part = _make_a2a_part_for_test({})
|
||||
message = Message(
|
||||
message_id="msg-1", role=_compat.ROLE_USER, parts=[a2a_part]
|
||||
)
|
||||
|
||||
mock_genai_part = genai_types.Part.from_text(text="hello")
|
||||
mock_part_converter = Mock(return_value=[mock_genai_part])
|
||||
|
||||
event = convert_a2a_message_to_event(
|
||||
message,
|
||||
author="test-author",
|
||||
invocation_context=self.mock_context,
|
||||
part_converter=mock_part_converter,
|
||||
)
|
||||
|
||||
assert event.author == "test-author"
|
||||
assert event.invocation_id == "test-invocation"
|
||||
assert event.branch == "test-branch"
|
||||
assert len(event.content.parts) == 1
|
||||
assert event.content.parts[0] == mock_genai_part
|
||||
|
||||
def test_convert_a2a_message_to_event_none(self):
|
||||
"""Test convert_a2a_message_to_event with None."""
|
||||
with pytest.raises(ValueError, match="A2A message cannot be None"):
|
||||
convert_a2a_message_to_event(None)
|
||||
|
||||
def test_convert_a2a_message_to_event_restores_actions_from_metadata(self):
|
||||
"""Test A2A message conversion restores ADK actions metadata."""
|
||||
a2a_part = _make_a2a_part_for_test({})
|
||||
message = Message(
|
||||
message_id="msg-1",
|
||||
role=_compat.ROLE_USER,
|
||||
parts=[a2a_part],
|
||||
metadata={
|
||||
_get_adk_metadata_key("actions"): {
|
||||
"stateDelta": {"saved_key": "saved-value"}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
mock_genai_part = genai_types.Part.from_text(text="hello")
|
||||
mock_part_converter = Mock(return_value=[mock_genai_part])
|
||||
|
||||
event = convert_a2a_message_to_event(
|
||||
message,
|
||||
author="test-author",
|
||||
invocation_context=self.mock_context,
|
||||
part_converter=mock_part_converter,
|
||||
)
|
||||
|
||||
assert event.actions.state_delta == {"saved_key": "saved-value"}
|
||||
assert event.content is not None
|
||||
assert event.content.parts[0] == mock_genai_part
|
||||
|
||||
def test_convert_a2a_message_to_event_returns_action_only_event(self):
|
||||
"""Test A2A message conversion returns action-only events."""
|
||||
message = Message(
|
||||
message_id="msg-1",
|
||||
role=_compat.ROLE_USER,
|
||||
parts=[],
|
||||
metadata={
|
||||
_get_adk_metadata_key("actions"): {
|
||||
"stateDelta": {"saved_key": "saved-value"}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
event = convert_a2a_message_to_event(
|
||||
message,
|
||||
author="test-author",
|
||||
invocation_context=self.mock_context,
|
||||
part_converter=Mock(),
|
||||
)
|
||||
|
||||
assert event is not None
|
||||
assert event.actions.state_delta == {"saved_key": "saved-value"}
|
||||
assert event.content is None
|
||||
|
||||
def test_convert_a2a_task_to_event_success(self):
|
||||
"""Test successful conversion of A2A task to Event."""
|
||||
a2a_part = _make_a2a_part_for_test({})
|
||||
task = Task(
|
||||
id="task-1",
|
||||
status=_compat.make_task_status(
|
||||
_compat.TS_SUBMITTED, timestamp="2024-01-01T00:00:00Z"
|
||||
),
|
||||
context_id="context-1",
|
||||
history=[
|
||||
Message(
|
||||
message_id="msg-1", role=_compat.ROLE_AGENT, parts=[a2a_part]
|
||||
)
|
||||
],
|
||||
artifacts=[
|
||||
_compat.make_artifact(
|
||||
artifact_id="art-1", artifact_type="message", parts=[a2a_part]
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
mock_genai_part = genai_types.Part.from_text(text="task artifact text")
|
||||
mock_part_converter = Mock(return_value=[mock_genai_part])
|
||||
|
||||
event = convert_a2a_task_to_event(
|
||||
task,
|
||||
author="test-author",
|
||||
invocation_context=self.mock_context,
|
||||
part_converter=mock_part_converter,
|
||||
)
|
||||
|
||||
assert event.author == "test-author"
|
||||
assert event.invocation_id == "test-invocation"
|
||||
assert len(event.content.parts) == 1
|
||||
assert event.content.parts[0] == mock_genai_part
|
||||
|
||||
def test_convert_a2a_task_to_event_returns_action_only_event(self):
|
||||
"""Test A2A task conversion returns action-only events."""
|
||||
task = Task(
|
||||
id="task-1",
|
||||
status=_compat.make_task_status(
|
||||
_compat.TS_SUBMITTED, timestamp="2024-01-01T00:00:00Z"
|
||||
),
|
||||
context_id="context-1",
|
||||
artifacts=[
|
||||
_compat.make_artifact(
|
||||
artifact_id="art-1",
|
||||
artifact_type="message",
|
||||
parts=[],
|
||||
metadata={
|
||||
_get_adk_metadata_key("actions"): {
|
||||
"stateDelta": {"saved_key": "saved-value"}
|
||||
}
|
||||
},
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
event = convert_a2a_task_to_event(
|
||||
task,
|
||||
author="test-author",
|
||||
invocation_context=self.mock_context,
|
||||
part_converter=Mock(),
|
||||
)
|
||||
|
||||
assert event is not None
|
||||
assert event.actions.state_delta == {"saved_key": "saved-value"}
|
||||
assert event.content is None
|
||||
|
||||
def test_convert_a2a_task_to_event_merges_actions_across_artifacts(self):
|
||||
"""Test task conversion merges actions across artifact metadata."""
|
||||
task = Task(
|
||||
id="task-1",
|
||||
status=_compat.make_task_status(
|
||||
_compat.TS_SUBMITTED, timestamp="2024-01-01T00:00:00Z"
|
||||
),
|
||||
context_id="context-1",
|
||||
artifacts=[
|
||||
_compat.make_artifact(
|
||||
artifact_id="art-1",
|
||||
artifact_type="message",
|
||||
parts=[],
|
||||
metadata={
|
||||
_get_adk_metadata_key("actions"): {
|
||||
"stateDelta": {"first_key": "first-value"}
|
||||
}
|
||||
},
|
||||
),
|
||||
_compat.make_artifact(
|
||||
artifact_id="art-2",
|
||||
artifact_type="message",
|
||||
parts=[],
|
||||
metadata={},
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
event = convert_a2a_task_to_event(
|
||||
task,
|
||||
author="test-author",
|
||||
invocation_context=self.mock_context,
|
||||
part_converter=Mock(),
|
||||
)
|
||||
|
||||
assert event is not None
|
||||
assert event.actions.state_delta == {"first_key": "first-value"}
|
||||
assert event.content is None
|
||||
|
||||
def test_convert_a2a_task_to_event_overwrites_nested_state_delta_values(self):
|
||||
"""Test task conversion preserves top-level state overwrite semantics."""
|
||||
task = Task(
|
||||
id="task-1",
|
||||
status=_compat.make_task_status(
|
||||
_compat.TS_SUBMITTED, timestamp="2024-01-01T00:00:00Z"
|
||||
),
|
||||
context_id="context-1",
|
||||
artifacts=[
|
||||
_compat.make_artifact(
|
||||
artifact_id="art-1",
|
||||
artifact_type="message",
|
||||
parts=[],
|
||||
metadata={
|
||||
_get_adk_metadata_key("actions"): {
|
||||
"stateDelta": {
|
||||
"settings": {
|
||||
"theme": "light",
|
||||
"language": "en",
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
_compat.make_artifact(
|
||||
artifact_id="art-2",
|
||||
artifact_type="message",
|
||||
parts=[],
|
||||
metadata={
|
||||
_get_adk_metadata_key("actions"): {
|
||||
"stateDelta": {"settings": {"theme": "dark"}}
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
event = convert_a2a_task_to_event(
|
||||
task,
|
||||
author="test-author",
|
||||
invocation_context=self.mock_context,
|
||||
part_converter=Mock(),
|
||||
)
|
||||
|
||||
assert event is not None
|
||||
assert event.actions.state_delta == {"settings": {"theme": "dark"}}
|
||||
assert event.content is None
|
||||
|
||||
def test_convert_a2a_task_to_event_merges_status_and_artifact_actions(self):
|
||||
"""Test task conversion merges status and artifact actions."""
|
||||
a2a_part = _make_a2a_part_for_test({})
|
||||
task = Task(
|
||||
id="task-1",
|
||||
status=_compat.make_task_status(
|
||||
_compat.TS_INPUT_REQUIRED,
|
||||
timestamp="2024-01-01T00:00:00Z",
|
||||
message=Message(
|
||||
message_id="msg-1",
|
||||
role=_compat.ROLE_AGENT,
|
||||
parts=[a2a_part],
|
||||
metadata={
|
||||
_get_adk_metadata_key("actions"): {
|
||||
"transferToAgent": "agent-2"
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
context_id="context-1",
|
||||
artifacts=[
|
||||
_compat.make_artifact(
|
||||
artifact_id="art-1",
|
||||
artifact_type="message",
|
||||
parts=[],
|
||||
metadata={
|
||||
_get_adk_metadata_key("actions"): {
|
||||
"stateDelta": {"saved_key": "saved-value"}
|
||||
}
|
||||
},
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
mock_genai_part = genai_types.Part.from_text(text="need input")
|
||||
|
||||
event = convert_a2a_task_to_event(
|
||||
task,
|
||||
author="test-author",
|
||||
invocation_context=self.mock_context,
|
||||
part_converter=Mock(return_value=[mock_genai_part]),
|
||||
)
|
||||
|
||||
assert event is not None
|
||||
assert event.actions.state_delta == {"saved_key": "saved-value"}
|
||||
assert event.actions.transfer_to_agent == "agent-2"
|
||||
assert event.content is not None
|
||||
assert (
|
||||
event.content.parts[0].function_call.name
|
||||
== MOCK_FUNCTION_CALL_FOR_REQUIRED_USER_INPUT
|
||||
)
|
||||
assert (
|
||||
event.content.parts[0].function_call.args["input_required"]
|
||||
== "need input"
|
||||
)
|
||||
|
||||
def test_convert_a2a_task_to_event_auth_required_uses_auth_args_key(self):
|
||||
"""Test auth-required state populates the function call with auth args."""
|
||||
a2a_part = _make_a2a_part_for_test({})
|
||||
task = _compat.make_task(
|
||||
id="task-1",
|
||||
context_id="context-1",
|
||||
kind="task",
|
||||
status=_compat.make_task_status(
|
||||
_compat.TS_AUTH_REQUIRED,
|
||||
timestamp="now",
|
||||
message=Message(
|
||||
message_id="m1",
|
||||
role=_compat.ROLE_AGENT,
|
||||
parts=[a2a_part],
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
mock_genai_part = genai_types.Part.from_text(text="need auth")
|
||||
|
||||
event = convert_a2a_task_to_event(
|
||||
task,
|
||||
author="test-author",
|
||||
invocation_context=self.mock_context,
|
||||
part_converter=Mock(return_value=[mock_genai_part]),
|
||||
)
|
||||
|
||||
assert event is not None
|
||||
assert event.content is not None
|
||||
assert (
|
||||
event.content.parts[0].function_call.name
|
||||
== MOCK_FUNCTION_CALL_FOR_REQUIRED_USER_AUTH
|
||||
)
|
||||
# auth_required state should populate the auth_required arg key, not
|
||||
# input_required.
|
||||
assert (
|
||||
event.content.parts[0].function_call.args["auth_required"]
|
||||
== "need auth"
|
||||
)
|
||||
assert "input_required" not in event.content.parts[0].function_call.args
|
||||
|
||||
def test_convert_a2a_task_to_event_multiple_parts_replaces_last_text(self):
|
||||
"""Test converting A2A task with multiple text parts, only replacing the last text."""
|
||||
part1 = _make_a2a_part_for_test({})
|
||||
part2 = _make_a2a_part_for_test({})
|
||||
|
||||
task = _compat.make_task(
|
||||
id="task-1",
|
||||
context_id="context-1",
|
||||
kind="task",
|
||||
status=_compat.make_task_status(
|
||||
_compat.TS_INPUT_REQUIRED,
|
||||
timestamp="now",
|
||||
message=Message(
|
||||
message_id="m1",
|
||||
role=_compat.ROLE_AGENT,
|
||||
parts=[part1, part2],
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
mock_genai_part_1 = genai_types.Part.from_text(text="Part 1")
|
||||
mock_genai_part_2 = genai_types.Part.from_text(text="Part 2")
|
||||
|
||||
part_converter_mock = Mock()
|
||||
part_converter_mock.side_effect = [[mock_genai_part_1], [mock_genai_part_2]]
|
||||
|
||||
event = convert_a2a_task_to_event(
|
||||
task,
|
||||
author="test-author",
|
||||
invocation_context=self.mock_context,
|
||||
part_converter=part_converter_mock,
|
||||
)
|
||||
|
||||
assert event is not None
|
||||
assert event.content is not None
|
||||
assert len(event.content.parts) == 2
|
||||
assert event.content.parts[0].text == "Part 1"
|
||||
assert (
|
||||
event.content.parts[1].function_call.name
|
||||
== MOCK_FUNCTION_CALL_FOR_REQUIRED_USER_INPUT
|
||||
)
|
||||
|
||||
def test_convert_a2a_task_to_event_no_text_parts(self):
|
||||
"""Test converting A2A task with no text parts should not inject function call."""
|
||||
# A real non-text (data) part; converter output is mocked below.
|
||||
part1 = _compat.make_data_part(data={"placeholder": True})
|
||||
|
||||
task = _compat.make_task(
|
||||
id="task-1",
|
||||
context_id="context-1",
|
||||
kind="task",
|
||||
status=_compat.make_task_status(
|
||||
_compat.TS_INPUT_REQUIRED,
|
||||
timestamp="now",
|
||||
message=Message(
|
||||
message_id="m1",
|
||||
role=_compat.ROLE_AGENT,
|
||||
parts=[part1],
|
||||
),
|
||||
),
|
||||
)
|
||||
mock_image_part = genai_types.Part(
|
||||
inline_data=genai_types.Blob(mime_type="image/jpeg", data=b"fake")
|
||||
)
|
||||
|
||||
event = convert_a2a_task_to_event(
|
||||
task,
|
||||
author="test-author",
|
||||
invocation_context=self.mock_context,
|
||||
part_converter=Mock(return_value=[mock_image_part]),
|
||||
)
|
||||
|
||||
assert event is not None
|
||||
assert event.content is not None
|
||||
assert event.content.parts == [mock_image_part]
|
||||
|
||||
def test_convert_a2a_task_to_event_data_part_input_required(self):
|
||||
"""Input-required prompt carried in a data part becomes a function call."""
|
||||
# A real non-text (data) part; converter output is mocked below.
|
||||
part1 = _compat.make_data_part(data={"placeholder": True})
|
||||
|
||||
task = _compat.make_task(
|
||||
id="task-1",
|
||||
context_id="context-1",
|
||||
kind="task",
|
||||
status=_compat.make_task_status(
|
||||
_compat.TS_INPUT_REQUIRED,
|
||||
timestamp="now",
|
||||
message=Message(
|
||||
message_id="m1",
|
||||
role=_compat.ROLE_AGENT,
|
||||
parts=[part1],
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
prompt = {
|
||||
"id": "abc123",
|
||||
"text": "Please confirm this action. Do you want to continue?",
|
||||
}
|
||||
data_part_json = json.dumps({"data": prompt, "kind": "data"}).encode(
|
||||
"utf-8"
|
||||
)
|
||||
mock_data_blob_part = genai_types.Part(
|
||||
inline_data=genai_types.Blob(
|
||||
mime_type=A2A_DATA_PART_TEXT_MIME_TYPE,
|
||||
data=A2A_DATA_PART_START_TAG
|
||||
+ data_part_json
|
||||
+ A2A_DATA_PART_END_TAG,
|
||||
)
|
||||
)
|
||||
|
||||
event = convert_a2a_task_to_event(
|
||||
task,
|
||||
author="test-author",
|
||||
invocation_context=self.mock_context,
|
||||
part_converter=Mock(return_value=[mock_data_blob_part]),
|
||||
)
|
||||
|
||||
assert event is not None
|
||||
assert event.content is not None
|
||||
assert (
|
||||
event.content.parts[0].function_call.name
|
||||
== MOCK_FUNCTION_CALL_FOR_REQUIRED_USER_INPUT
|
||||
)
|
||||
assert event.content.parts[0].function_call.args["input_required"] == prompt
|
||||
assert event.long_running_tool_ids
|
||||
|
||||
def test_convert_a2a_task_to_event_data_part_malformed_json(self):
|
||||
"""A malformed data-part blob is left untouched (no crash, no fc)."""
|
||||
# A real non-text (data) part; converter output is mocked below.
|
||||
part1 = _compat.make_data_part(data={"placeholder": True})
|
||||
|
||||
task = _compat.make_task(
|
||||
id="task-1",
|
||||
context_id="context-1",
|
||||
kind="task",
|
||||
status=_compat.make_task_status(
|
||||
_compat.TS_INPUT_REQUIRED,
|
||||
timestamp="now",
|
||||
message=Message(
|
||||
message_id="m1",
|
||||
role=_compat.ROLE_AGENT,
|
||||
parts=[part1],
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
mock_bad_blob_part = genai_types.Part(
|
||||
inline_data=genai_types.Blob(
|
||||
mime_type=A2A_DATA_PART_TEXT_MIME_TYPE,
|
||||
data=A2A_DATA_PART_START_TAG + b"not-json" + A2A_DATA_PART_END_TAG,
|
||||
)
|
||||
)
|
||||
|
||||
event = convert_a2a_task_to_event(
|
||||
task,
|
||||
author="test-author",
|
||||
invocation_context=self.mock_context,
|
||||
part_converter=Mock(return_value=[mock_bad_blob_part]),
|
||||
)
|
||||
|
||||
assert event is not None
|
||||
assert event.content is not None
|
||||
assert event.content.parts == [mock_bad_blob_part]
|
||||
assert not event.long_running_tool_ids
|
||||
|
||||
def test_convert_a2a_status_update_to_event_success(self):
|
||||
"""Test successful conversion of A2A status update to Event."""
|
||||
a2a_part = _make_a2a_part_for_test({
|
||||
_get_adk_metadata_key(A2A_DATA_PART_METADATA_IS_LONG_RUNNING_KEY): True
|
||||
})
|
||||
update = _compat.make_task_status_update_event(
|
||||
task_id="task-1",
|
||||
status=_compat.make_task_status(
|
||||
_compat.TS_INPUT_REQUIRED,
|
||||
timestamp="now",
|
||||
message=Message(
|
||||
message_id="m1",
|
||||
role=_compat.ROLE_AGENT,
|
||||
parts=[a2a_part],
|
||||
),
|
||||
),
|
||||
context_id="context-1",
|
||||
final=False,
|
||||
)
|
||||
|
||||
mock_genai_part = genai_types.Part(
|
||||
function_call=genai_types.FunctionCall(
|
||||
name="status update text", args={"arg": "value"}, id="call-1"
|
||||
)
|
||||
)
|
||||
mock_part_converter = Mock(return_value=[mock_genai_part])
|
||||
|
||||
event = convert_a2a_status_update_to_event(
|
||||
update,
|
||||
author="test-author",
|
||||
invocation_context=self.mock_context,
|
||||
part_converter=mock_part_converter,
|
||||
)
|
||||
|
||||
assert event.author == "test-author"
|
||||
assert event.invocation_id == "test-invocation"
|
||||
assert len(event.content.parts) == 1
|
||||
assert event.content.parts[0] == mock_genai_part
|
||||
|
||||
def test_convert_a2a_status_update_to_event_none(self):
|
||||
"""Test convert_a2a_status_update_to_event with None."""
|
||||
with pytest.raises(ValueError, match="A2A status update cannot be None"):
|
||||
convert_a2a_status_update_to_event(None)
|
||||
|
||||
def test_convert_a2a_artifact_update_to_event_success(self):
|
||||
"""Test successful conversion of A2A artifact update to Event."""
|
||||
a2a_part = _make_a2a_part_for_test({})
|
||||
update = TaskArtifactUpdateEvent(
|
||||
task_id="task-1",
|
||||
artifact=_compat.make_artifact(
|
||||
artifact_id="art-1", artifact_type="message", parts=[a2a_part]
|
||||
),
|
||||
append=True,
|
||||
context_id="context-1",
|
||||
last_chunk=False,
|
||||
)
|
||||
|
||||
mock_genai_part = genai_types.Part.from_text(text="artifact chunk text")
|
||||
mock_part_converter = Mock(return_value=[mock_genai_part])
|
||||
|
||||
event = convert_a2a_artifact_update_to_event(
|
||||
update,
|
||||
author="test-author",
|
||||
invocation_context=self.mock_context,
|
||||
part_converter=mock_part_converter,
|
||||
)
|
||||
|
||||
assert event.author == "test-author"
|
||||
assert event.invocation_id == "test-invocation"
|
||||
assert event.partial is True
|
||||
assert len(event.content.parts) == 1
|
||||
assert event.content.parts[0] == mock_genai_part
|
||||
|
||||
def test_convert_a2a_artifact_update_to_event_none(self):
|
||||
"""Test convert_a2a_artifact_update_to_event with None."""
|
||||
with pytest.raises(ValueError, match="A2A artifact update cannot be None"):
|
||||
convert_a2a_artifact_update_to_event(None)
|
||||
|
||||
def test_convert_a2a_message_to_event_user_role(self) -> None:
|
||||
"""Test that A2A user role maps to GenAI content role 'user'."""
|
||||
a2a_part = _make_a2a_part_for_test({})
|
||||
message = Message(
|
||||
message_id="msg-1", role=_compat.ROLE_USER, parts=[a2a_part]
|
||||
)
|
||||
|
||||
mock_genai_part = genai_types.Part.from_text(text="hello from user")
|
||||
mock_part_converter = Mock(return_value=[mock_genai_part])
|
||||
|
||||
event = convert_a2a_message_to_event(
|
||||
message,
|
||||
author="user",
|
||||
invocation_context=self.mock_context,
|
||||
part_converter=mock_part_converter,
|
||||
)
|
||||
|
||||
assert event.content.role == "user"
|
||||
|
||||
def test_convert_a2a_message_to_event_agent_role(self) -> None:
|
||||
"""Test that A2A agent role maps to GenAI content role 'model'."""
|
||||
a2a_part = _make_a2a_part_for_test({})
|
||||
message = Message(
|
||||
message_id="msg-1", role=_compat.ROLE_AGENT, parts=[a2a_part]
|
||||
)
|
||||
|
||||
mock_genai_part = genai_types.Part.from_text(text="hello from agent")
|
||||
mock_part_converter = Mock(return_value=[mock_genai_part])
|
||||
|
||||
event = convert_a2a_message_to_event(
|
||||
message,
|
||||
author="test-agent",
|
||||
invocation_context=self.mock_context,
|
||||
part_converter=mock_part_converter,
|
||||
)
|
||||
|
||||
assert event.content.role == "model"
|
||||
@@ -0,0 +1,204 @@
|
||||
# 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 google.adk.a2a.converters.utils import _from_a2a_context_id
|
||||
from google.adk.a2a.converters.utils import _get_adk_metadata_key
|
||||
from google.adk.a2a.converters.utils import _to_a2a_context_id
|
||||
from google.adk.a2a.converters.utils import ADK_CONTEXT_ID_PREFIX
|
||||
from google.adk.a2a.converters.utils import ADK_METADATA_KEY_PREFIX
|
||||
import pytest
|
||||
|
||||
|
||||
class TestUtilsFunctions:
|
||||
"""Test suite for utils module functions."""
|
||||
|
||||
def test_get_adk_metadata_key_success(self):
|
||||
"""Test successful metadata key generation."""
|
||||
key = "test_key"
|
||||
result = _get_adk_metadata_key(key)
|
||||
assert result == f"{ADK_METADATA_KEY_PREFIX}{key}"
|
||||
|
||||
def test_get_adk_metadata_key_empty_string(self):
|
||||
"""Test metadata key generation with empty string."""
|
||||
with pytest.raises(
|
||||
ValueError, match="Metadata key cannot be empty or None"
|
||||
):
|
||||
_get_adk_metadata_key("")
|
||||
|
||||
def test_get_adk_metadata_key_none(self):
|
||||
"""Test metadata key generation with None."""
|
||||
with pytest.raises(
|
||||
ValueError, match="Metadata key cannot be empty or None"
|
||||
):
|
||||
_get_adk_metadata_key(None)
|
||||
|
||||
def test_get_adk_metadata_key_whitespace(self):
|
||||
"""Test metadata key generation with whitespace string."""
|
||||
key = " "
|
||||
result = _get_adk_metadata_key(key)
|
||||
assert result == f"{ADK_METADATA_KEY_PREFIX}{key}"
|
||||
|
||||
def test_to_a2a_context_id_success(self):
|
||||
"""Test successful context ID generation."""
|
||||
app_name = "test-app"
|
||||
user_id = "test-user"
|
||||
session_id = "test-session"
|
||||
|
||||
result = _to_a2a_context_id(app_name, user_id, session_id)
|
||||
|
||||
expected = f"{ADK_CONTEXT_ID_PREFIX}/test-app/test-user/test-session"
|
||||
assert result == expected
|
||||
|
||||
def test_to_a2a_context_id_empty_app_name(self):
|
||||
"""Test context ID generation with empty app name."""
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
match=(
|
||||
"All parameters \\(app_name, user_id, session_id\\) must be"
|
||||
" non-empty"
|
||||
),
|
||||
):
|
||||
_to_a2a_context_id("", "user", "session")
|
||||
|
||||
def test_to_a2a_context_id_empty_user_id(self):
|
||||
"""Test context ID generation with empty user ID."""
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
match=(
|
||||
"All parameters \\(app_name, user_id, session_id\\) must be"
|
||||
" non-empty"
|
||||
),
|
||||
):
|
||||
_to_a2a_context_id("app", "", "session")
|
||||
|
||||
def test_to_a2a_context_id_empty_session_id(self):
|
||||
"""Test context ID generation with empty session ID."""
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
match=(
|
||||
"All parameters \\(app_name, user_id, session_id\\) must be"
|
||||
" non-empty"
|
||||
),
|
||||
):
|
||||
_to_a2a_context_id("app", "user", "")
|
||||
|
||||
def test_to_a2a_context_id_none_values(self):
|
||||
"""Test context ID generation with None values."""
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
match=(
|
||||
"All parameters \\(app_name, user_id, session_id\\) must be"
|
||||
" non-empty"
|
||||
),
|
||||
):
|
||||
_to_a2a_context_id(None, "user", "session")
|
||||
|
||||
def test_to_a2a_context_id_special_characters(self):
|
||||
"""Test context ID generation with special characters."""
|
||||
app_name = "test-app@2024"
|
||||
user_id = "user_123"
|
||||
session_id = "session-456"
|
||||
|
||||
result = _to_a2a_context_id(app_name, user_id, session_id)
|
||||
|
||||
expected = f"{ADK_CONTEXT_ID_PREFIX}/test-app@2024/user_123/session-456"
|
||||
assert result == expected
|
||||
|
||||
def test_from_a2a_context_id_success(self):
|
||||
"""Test successful context ID parsing."""
|
||||
context_id = f"{ADK_CONTEXT_ID_PREFIX}/test-app/test-user/test-session"
|
||||
|
||||
app_name, user_id, session_id = _from_a2a_context_id(context_id)
|
||||
|
||||
assert app_name == "test-app"
|
||||
assert user_id == "test-user"
|
||||
assert session_id == "test-session"
|
||||
|
||||
def test_from_a2a_context_id_none_input(self):
|
||||
"""Test context ID parsing with None input."""
|
||||
result = _from_a2a_context_id(None)
|
||||
assert result == (None, None, None)
|
||||
|
||||
def test_from_a2a_context_id_empty_string(self):
|
||||
"""Test context ID parsing with empty string."""
|
||||
result = _from_a2a_context_id("")
|
||||
assert result == (None, None, None)
|
||||
|
||||
def test_from_a2a_context_id_invalid_prefix(self):
|
||||
"""Test context ID parsing with invalid prefix."""
|
||||
context_id = "INVALID/test-app/test-user/test-session"
|
||||
|
||||
result = _from_a2a_context_id(context_id)
|
||||
|
||||
assert result == (None, None, None)
|
||||
|
||||
def test_from_a2a_context_id_too_few_parts(self):
|
||||
"""Test context ID parsing with too few parts."""
|
||||
context_id = f"{ADK_CONTEXT_ID_PREFIX}/test-app/test-user"
|
||||
|
||||
result = _from_a2a_context_id(context_id)
|
||||
|
||||
assert result == (None, None, None)
|
||||
|
||||
def test_from_a2a_context_id_too_many_parts(self):
|
||||
"""Test context ID parsing with too many parts."""
|
||||
context_id = (
|
||||
f"{ADK_CONTEXT_ID_PREFIX}/test-app/test-user/test-session/extra"
|
||||
)
|
||||
|
||||
result = _from_a2a_context_id(context_id)
|
||||
|
||||
assert result == (None, None, None)
|
||||
|
||||
def test_from_a2a_context_id_empty_components(self):
|
||||
"""Test context ID parsing with empty components."""
|
||||
context_id = f"{ADK_CONTEXT_ID_PREFIX}//test-user/test-session"
|
||||
|
||||
result = _from_a2a_context_id(context_id)
|
||||
|
||||
assert result == (None, None, None)
|
||||
|
||||
def test_from_a2a_context_id_no_dollar_separator(self):
|
||||
"""Test context ID parsing without dollar separators."""
|
||||
context_id = f"{ADK_CONTEXT_ID_PREFIX}-test-app-test-user-test-session"
|
||||
|
||||
result = _from_a2a_context_id(context_id)
|
||||
|
||||
assert result == (None, None, None)
|
||||
|
||||
def test_roundtrip_context_id(self):
|
||||
"""Test roundtrip conversion: to -> from."""
|
||||
app_name = "test-app"
|
||||
user_id = "test-user"
|
||||
session_id = "test-session"
|
||||
|
||||
# Convert to context ID
|
||||
context_id = _to_a2a_context_id(app_name, user_id, session_id)
|
||||
|
||||
# Convert back
|
||||
parsed_app, parsed_user, parsed_session = _from_a2a_context_id(context_id)
|
||||
|
||||
assert parsed_app == app_name
|
||||
assert parsed_user == user_id
|
||||
assert parsed_session == session_id
|
||||
|
||||
def test_from_a2a_context_id_special_characters(self):
|
||||
"""Test context ID parsing with special characters."""
|
||||
context_id = f"{ADK_CONTEXT_ID_PREFIX}/test-app@2024/user_123/session-456"
|
||||
|
||||
app_name, user_id, session_id = _from_a2a_context_id(context_id)
|
||||
|
||||
assert app_name == "test-app@2024"
|
||||
assert user_id == "user_123"
|
||||
assert session_id == "session-456"
|
||||
Reference in New Issue
Block a user