From a227f4f59e0d3ca02f647adf4a09fc8581740f31 Mon Sep 17 00:00:00 2001 From: Shingo OKAWA Date: Tue, 15 Jul 2025 09:39:26 +0900 Subject: [PATCH 1/7] test: improve coverage Signed-off-by: Shingo OKAWA --- src/a2a/client/grpc_client.py | 4 +- src/a2a/utils/proto_utils.py | 7 +- tests/client/test_grpc_client.py | 270 ++++++++++++++++++++++++++++++- 3 files changed, 274 insertions(+), 7 deletions(-) diff --git a/src/a2a/client/grpc_client.py b/src/a2a/client/grpc_client.py index 5fc7cc99..b782bb18 100644 --- a/src/a2a/client/grpc_client.py +++ b/src/a2a/client/grpc_client.py @@ -64,7 +64,7 @@ async def send_message( metadata=proto_utils.ToProto.metadata(request.metadata), ) ) - if response.task: + if response.HasField('task'): return proto_utils.FromProto.task(response.task) return proto_utils.FromProto.message(response.msg) @@ -87,7 +87,7 @@ async def send_message_streaming( `TaskArtifactUpdateEvent` objects as they are received in the stream. """ - stream = self.stub.SendStreamingMessage( + stream = await self.stub.SendStreamingMessage( a2a_pb2.SendMessageRequest( request=proto_utils.ToProto.message(request.message), configuration=proto_utils.ToProto.message_send_configuration( diff --git a/src/a2a/utils/proto_utils.py b/src/a2a/utils/proto_utils.py index b67d6033..1eeafce9 100644 --- a/src/a2a/utils/proto_utils.py +++ b/src/a2a/utils/proto_utils.py @@ -14,8 +14,10 @@ # Regexp patterns for matching -_TASK_NAME_MATCH = r'tasks/(\w+)' -_TASK_PUSH_CONFIG_NAME_MATCH = r'tasks/(\w+)/pushNotificationConfigs/(\w+)' +_TASK_NAME_MATCH = r'tasks/([a-zA-Z0-9_.-]+)' +_TASK_PUSH_CONFIG_NAME_MATCH = ( + r'tasks/([a-zA-Z0-9_.-]+)/pushNotificationConfigs/([a-zA-Z0-9_.-]+)' +) class ToProto: @@ -631,6 +633,7 @@ def task_push_notification_config( request: a2a_pb2.CreateTaskPushNotificationConfigRequest, ) -> types.TaskPushNotificationConfig: m = re.match(_TASK_NAME_MATCH, request.parent) + print(m) if not m: raise ServerError( error=types.InvalidParamsError( diff --git a/tests/client/test_grpc_client.py b/tests/client/test_grpc_client.py index ff823342..5b5356b9 100644 --- a/tests/client/test_grpc_client.py +++ b/tests/client/test_grpc_client.py @@ -1,5 +1,6 @@ -from unittest.mock import AsyncMock +from unittest.mock import AsyncMock, MagicMock +import grpc import pytest from a2a.client import A2AGrpcClient @@ -7,18 +8,25 @@ from a2a.types import ( AgentCapabilities, AgentCard, + Artifact, Message, MessageSendParams, Part, + PushNotificationAuthenticationInfo, + PushNotificationConfig, Role, Task, + TaskArtifactUpdateEvent, TaskIdParams, + TaskPushNotificationConfig, TaskQueryParams, TaskState, TaskStatus, + TaskStatusUpdateEvent, TextPart, ) from a2a.utils import proto_utils +from a2a.utils.errors import ServerError # Fixtures @@ -30,8 +38,8 @@ def mock_grpc_stub() -> AsyncMock: stub.SendStreamingMessage = AsyncMock() stub.GetTask = AsyncMock() stub.CancelTask = AsyncMock() - stub.CreateTaskPushNotification = AsyncMock() - stub.GetTaskPushNotification = AsyncMock() + stub.CreateTaskPushNotificationConfig = AsyncMock() + stub.GetTaskPushNotificationConfig = AsyncMock() return stub @@ -90,6 +98,78 @@ def sample_message() -> Message: ) +@pytest.fixture +def sample_artifact() -> Artifact: + """Provides a sample Artifact object.""" + return Artifact( + artifactId='artifact-1', + name='example.txt', + description='An example artifact', + parts=[Part(root=TextPart(text='Hi there'))], + metadata={}, + extensions=[], + ) + + +@pytest.fixture +def sample_task_status_update_event() -> TaskStatusUpdateEvent: + """Provides a sample TaskStatusUpdateEvent.""" + return TaskStatusUpdateEvent( + taskId='task-1', + contextId='ctx-1', + status=TaskStatus(state=TaskState.working), + final=False, + metadata={}, + ) + + +@pytest.fixture +def sample_task_artifact_update_event( + sample_artifact, +) -> TaskArtifactUpdateEvent: + """Provides a sample TaskArtifactUpdateEvent.""" + return TaskArtifactUpdateEvent( + taskId='task-1', + contextId='ctx-1', + artifact=sample_artifact, + append=True, + last_chunk=True, + metadata={}, + ) + + +@pytest.fixture +def sample_authentication_info() -> PushNotificationAuthenticationInfo: + """Provides a sample AuthenticationInfo object.""" + return PushNotificationAuthenticationInfo( + schemes=['apikey', 'oauth2'], credentials='secret-token' + ) + + +@pytest.fixture +def sample_push_notification_config( + sample_authentication_info: PushNotificationAuthenticationInfo, +) -> PushNotificationConfig: + """Provides a sample PushNotificationConfig object.""" + return PushNotificationConfig( + id='config-1', + url='https://example.com/notify', + token='example-token', + authentication=sample_authentication_info, + ) + + +@pytest.fixture +def sample_task_push_notification_config( + sample_push_notification_config: PushNotificationConfig, +) -> TaskPushNotificationConfig: + """Provides a sample TaskPushNotificationConfig object.""" + return TaskPushNotificationConfig( + taskId='task-1', + pushNotificationConfig=sample_push_notification_config, + ) + + @pytest.mark.asyncio async def test_send_message_task_response( grpc_client: A2AGrpcClient, @@ -109,6 +189,76 @@ async def test_send_message_task_response( assert response.id == sample_task.id +@pytest.mark.asyncio +async def test_send_message_message_response( + grpc_client: A2AGrpcClient, + mock_grpc_stub: AsyncMock, + sample_message_send_params: MessageSendParams, + sample_message: Message, +): + """Test send_message that returns a Message.""" + mock_grpc_stub.SendMessage.return_value = a2a_pb2.SendMessageResponse( + msg=proto_utils.ToProto.message(sample_message) + ) + + response = await grpc_client.send_message(sample_message_send_params) + + mock_grpc_stub.SendMessage.assert_awaited_once() + assert isinstance(response, Message) + assert response.messageId == sample_message.messageId + + +@pytest.mark.asyncio +async def test_send_message_streaming( + grpc_client: A2AGrpcClient, + mock_grpc_stub: AsyncMock, + sample_message_send_params: MessageSendParams, + sample_message: Message, + sample_task: Task, + sample_task_status_update_event: TaskStatusUpdateEvent, + sample_task_artifact_update_event: TaskArtifactUpdateEvent, +): + """Test send_message_streaming that yields responses.""" + stream = MagicMock() + stream.read = AsyncMock( + side_effect=[ + a2a_pb2.StreamResponse( + msg=proto_utils.ToProto.message(sample_message) + ), + a2a_pb2.StreamResponse(task=proto_utils.ToProto.task(sample_task)), + a2a_pb2.StreamResponse( + status_update=proto_utils.ToProto.task_status_update_event( + sample_task_status_update_event + ) + ), + a2a_pb2.StreamResponse( + artifact_update=proto_utils.ToProto.task_artifact_update_event( + sample_task_artifact_update_event + ) + ), + grpc.aio.EOF, + ] + ) + mock_grpc_stub.SendStreamingMessage.return_value = stream + + responses = [ + response + async for response in grpc_client.send_message_streaming( + sample_message_send_params + ) + ] + + mock_grpc_stub.SendStreamingMessage.assert_awaited_once() + assert isinstance(responses[0], Message) + assert responses[0].messageId == sample_message.messageId + assert isinstance(responses[1], Task) + assert responses[1].id == sample_task.id + assert isinstance(responses[2], TaskStatusUpdateEvent) + assert responses[2].taskId == sample_task_status_update_event.taskId + assert isinstance(responses[3], TaskArtifactUpdateEvent) + assert responses[3].taskId == sample_task_artifact_update_event.taskId + + @pytest.mark.asyncio async def test_get_task( grpc_client: A2AGrpcClient, mock_grpc_stub: AsyncMock, sample_task: Task @@ -143,3 +293,117 @@ async def test_cancel_task( a2a_pb2.CancelTaskRequest(name=f'tasks/{sample_task.id}') ) assert response.status.state == TaskState.canceled + + +@pytest.mark.asyncio +async def test_set_task_callback_with_valid_task( + grpc_client: A2AGrpcClient, + mock_grpc_stub: AsyncMock, + sample_task_push_notification_config: TaskPushNotificationConfig, +): + """Test setting a task push notification config with a valid task id.""" + task_id = 'task-1' + config_id = 'config-1' + mock_grpc_stub.CreateTaskPushNotificationConfig.return_value = ( + a2a_pb2.CreateTaskPushNotificationConfigRequest( + parent=f'tasks/{task_id}', + config_id=config_id, + config=proto_utils.ToProto.task_push_notification_config( + sample_task_push_notification_config + ), + ) + ) + + response = await grpc_client.set_task_callback( + sample_task_push_notification_config + ) + + mock_grpc_stub.CreateTaskPushNotificationConfig.assert_awaited_once_with( + a2a_pb2.CreateTaskPushNotificationConfigRequest( + config=proto_utils.ToProto.task_push_notification_config( + sample_task_push_notification_config + ), + ) + ) + assert response.taskId == task_id + + +@pytest.mark.asyncio +async def test_set_task_callback_with_invalid_task( + grpc_client: A2AGrpcClient, + mock_grpc_stub: AsyncMock, + sample_task_push_notification_config: TaskPushNotificationConfig, +): + """Test setting a task push notification config with a invalid task id.""" + task_id = 'task-1' + config_id = 'config-1' + mock_grpc_stub.CreateTaskPushNotificationConfig.return_value = ( + a2a_pb2.CreateTaskPushNotificationConfigRequest( + parent=f'invalid-path-to-tasks/{task_id}', + config_id=config_id, + config=proto_utils.ToProto.task_push_notification_config( + sample_task_push_notification_config + ), + ) + ) + + with pytest.raises(ServerError) as exc_info: + await grpc_client.set_task_callback( + sample_task_push_notification_config + ) + assert 'No task for' in exc_info.value.error.message + + +@pytest.mark.asyncio +async def test_get_task_callback_with_valid_task( + grpc_client: A2AGrpcClient, + mock_grpc_stub: AsyncMock, + sample_task_push_notification_config: TaskPushNotificationConfig, +): + """Test retrieving a task push notification config with a valid task id.""" + task_id = 'task-1' + config_id = 'config-1' + mock_grpc_stub.GetTaskPushNotificationConfig.return_value = ( + a2a_pb2.CreateTaskPushNotificationConfigRequest( + parent=f'tasks/{task_id}', + config_id=config_id, + config=proto_utils.ToProto.task_push_notification_config( + sample_task_push_notification_config + ), + ) + ) + params = TaskIdParams(id=sample_task_push_notification_config.taskId) + + response = await grpc_client.get_task_callback(params) + + mock_grpc_stub.GetTaskPushNotificationConfig.assert_awaited_once_with( + a2a_pb2.GetTaskPushNotificationConfigRequest( + name=f'tasks/{params.id}/pushNotification/undefined', + ) + ) + assert response.taskId == task_id + + +@pytest.mark.asyncio +async def test_get_task_callback_with_invalid_task( + grpc_client: A2AGrpcClient, + mock_grpc_stub: AsyncMock, + sample_task_push_notification_config: TaskPushNotificationConfig, +): + """Test retrieving a task push notification config with a invalid task id.""" + task_id = 'task-1' + config_id = 'config-1' + mock_grpc_stub.GetTaskPushNotificationConfig.return_value = ( + a2a_pb2.CreateTaskPushNotificationConfigRequest( + parent=f'invalid-path-to-tasks/{task_id}', + config_id=config_id, + config=proto_utils.ToProto.task_push_notification_config( + sample_task_push_notification_config + ), + ) + ) + params = TaskIdParams(id=sample_task_push_notification_config.taskId) + + with pytest.raises(ServerError) as exc_info: + await grpc_client.get_task_callback(params) + assert 'No task for' in exc_info.value.error.message From de01d0fc0af22cddec3ee07f0f65fbe93965041a Mon Sep 17 00:00:00 2001 From: Shingo OKAWA Date: Wed, 16 Jul 2025 02:10:59 +0900 Subject: [PATCH 2/7] chore: remove print Signed-off-by: Shingo OKAWA --- src/a2a/utils/proto_utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/a2a/utils/proto_utils.py b/src/a2a/utils/proto_utils.py index 1eeafce9..b616c965 100644 --- a/src/a2a/utils/proto_utils.py +++ b/src/a2a/utils/proto_utils.py @@ -633,7 +633,6 @@ def task_push_notification_config( request: a2a_pb2.CreateTaskPushNotificationConfigRequest, ) -> types.TaskPushNotificationConfig: m = re.match(_TASK_NAME_MATCH, request.parent) - print(m) if not m: raise ServerError( error=types.InvalidParamsError( From 6541934ac78baa7a73dcaefae6edb9cc4a9ad54a Mon Sep 17 00:00:00 2001 From: Shingo OKAWA Date: Sat, 19 Jul 2025 08:48:21 +0900 Subject: [PATCH 3/7] fix: remove await Signed-off-by: Shingo OKAWA --- src/a2a/client/grpc_client.py | 2 +- tests/client/test_grpc_client.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/a2a/client/grpc_client.py b/src/a2a/client/grpc_client.py index b782bb18..8c4c47ee 100644 --- a/src/a2a/client/grpc_client.py +++ b/src/a2a/client/grpc_client.py @@ -87,7 +87,7 @@ async def send_message_streaming( `TaskArtifactUpdateEvent` objects as they are received in the stream. """ - stream = await self.stub.SendStreamingMessage( + stream = self.stub.SendStreamingMessage( a2a_pb2.SendMessageRequest( request=proto_utils.ToProto.message(request.message), configuration=proto_utils.ToProto.message_send_configuration( diff --git a/tests/client/test_grpc_client.py b/tests/client/test_grpc_client.py index 5b5356b9..cfd46d0a 100644 --- a/tests/client/test_grpc_client.py +++ b/tests/client/test_grpc_client.py @@ -35,7 +35,7 @@ def mock_grpc_stub() -> AsyncMock: """Provides a mock gRPC stub with methods mocked.""" stub = AsyncMock(spec=a2a_pb2_grpc.A2AServiceStub) stub.SendMessage = AsyncMock() - stub.SendStreamingMessage = AsyncMock() + stub.SendStreamingMessage = MagicMock() stub.GetTask = AsyncMock() stub.CancelTask = AsyncMock() stub.CreateTaskPushNotificationConfig = AsyncMock() @@ -248,7 +248,7 @@ async def test_send_message_streaming( ) ] - mock_grpc_stub.SendStreamingMessage.assert_awaited_once() + mock_grpc_stub.SendStreamingMessage.assert_called_once() assert isinstance(responses[0], Message) assert responses[0].messageId == sample_message.messageId assert isinstance(responses[1], Task) From c340c78ca52a6e0708dd7e8aa7de53419eff2eda Mon Sep 17 00:00:00 2001 From: Shingo OKAWA Date: Mon, 4 Aug 2025 10:09:54 +0900 Subject: [PATCH 4/7] fix: update unit tests regarding refactoring --- tests/client/test_grpc_client.py | 141 ++++++++++++++++--------------- 1 file changed, 73 insertions(+), 68 deletions(-) diff --git a/tests/client/test_grpc_client.py b/tests/client/test_grpc_client.py index e19f2cf0..fba4d754 100644 --- a/tests/client/test_grpc_client.py +++ b/tests/client/test_grpc_client.py @@ -9,6 +9,7 @@ AgentCapabilities, AgentCard, Artifact, + GetTaskPushNotificationConfigParams, Message, MessageSendParams, Part, @@ -29,7 +30,6 @@ from a2a.utils.errors import ServerError -# Fixtures @pytest.fixture def mock_grpc_stub() -> AsyncMock: """Provides a mock gRPC stub with methods mocked.""" @@ -105,7 +105,7 @@ def sample_message() -> Message: def sample_artifact() -> Artifact: """Provides a sample Artifact object.""" return Artifact( - artifactId='artifact-1', + artifact_id='artifact-1', name='example.txt', description='An example artifact', parts=[Part(root=TextPart(text='Hi there'))], @@ -118,8 +118,8 @@ def sample_artifact() -> Artifact: def sample_task_status_update_event() -> TaskStatusUpdateEvent: """Provides a sample TaskStatusUpdateEvent.""" return TaskStatusUpdateEvent( - taskId='task-1', - contextId='ctx-1', + task_id='task-1', + context_id='ctx-1', status=TaskStatus(state=TaskState.working), final=False, metadata={}, @@ -132,8 +132,8 @@ def sample_task_artifact_update_event( ) -> TaskArtifactUpdateEvent: """Provides a sample TaskArtifactUpdateEvent.""" return TaskArtifactUpdateEvent( - taskId='task-1', - contextId='ctx-1', + task_id='task-1', + context_id='ctx-1', artifact=sample_artifact, append=True, last_chunk=True, @@ -168,8 +168,8 @@ def sample_task_push_notification_config( ) -> TaskPushNotificationConfig: """Provides a sample TaskPushNotificationConfig object.""" return TaskPushNotificationConfig( - taskId='task-1', - pushNotificationConfig=sample_push_notification_config, + task_id='task-1', + push_notification_config=sample_push_notification_config, ) @@ -194,7 +194,7 @@ async def test_send_message_task_response( @pytest.mark.asyncio async def test_send_message_message_response( - grpc_client: A2AGrpcClient, + grpc_transport: GrpcTransport, mock_grpc_stub: AsyncMock, sample_message_send_params: MessageSendParams, sample_message: Message, @@ -204,16 +204,16 @@ async def test_send_message_message_response( msg=proto_utils.ToProto.message(sample_message) ) - response = await grpc_client.send_message(sample_message_send_params) + response = await grpc_transport.send_message(sample_message_send_params) mock_grpc_stub.SendMessage.assert_awaited_once() assert isinstance(response, Message) - assert response.messageId == sample_message.messageId + assert response.message_id == sample_message.message_id @pytest.mark.asyncio -async def test_send_message_streaming( - grpc_client: A2AGrpcClient, +async def test_send_message_streaming( # noqa: PLR0913 + grpc_transport: GrpcTransport, mock_grpc_stub: AsyncMock, sample_message_send_params: MessageSendParams, sample_message: Message, @@ -246,20 +246,20 @@ async def test_send_message_streaming( responses = [ response - async for response in grpc_client.send_message_streaming( + async for response in grpc_transport.send_message_streaming( sample_message_send_params ) ] mock_grpc_stub.SendStreamingMessage.assert_called_once() assert isinstance(responses[0], Message) - assert responses[0].messageId == sample_message.messageId + assert responses[0].message_id == sample_message.message_id assert isinstance(responses[1], Task) assert responses[1].id == sample_task.id assert isinstance(responses[2], TaskStatusUpdateEvent) - assert responses[2].taskId == sample_task_status_update_event.taskId + assert responses[2].task_id == sample_task_status_update_event.task_id assert isinstance(responses[3], TaskArtifactUpdateEvent) - assert responses[3].taskId == sample_task_artifact_update_event.taskId + assert responses[3].task_id == sample_task_artifact_update_event.task_id @pytest.mark.asyncio @@ -300,113 +300,118 @@ async def test_cancel_task( @pytest.mark.asyncio async def test_set_task_callback_with_valid_task( - grpc_client: A2AGrpcClient, + grpc_transport: GrpcTransport, mock_grpc_stub: AsyncMock, sample_task_push_notification_config: TaskPushNotificationConfig, ): """Test setting a task push notification config with a valid task id.""" - task_id = 'task-1' - config_id = 'config-1' mock_grpc_stub.CreateTaskPushNotificationConfig.return_value = ( - a2a_pb2.CreateTaskPushNotificationConfigRequest( - parent=f'tasks/{task_id}', - config_id=config_id, - config=proto_utils.ToProto.task_push_notification_config( - sample_task_push_notification_config - ), + proto_utils.ToProto.task_push_notification_config( + sample_task_push_notification_config ) ) - response = await grpc_client.set_task_callback( + response = await grpc_transport.set_task_callback( sample_task_push_notification_config ) mock_grpc_stub.CreateTaskPushNotificationConfig.assert_awaited_once_with( a2a_pb2.CreateTaskPushNotificationConfigRequest( + parent=f'tasks/{sample_task_push_notification_config.task_id}', + config_id=sample_task_push_notification_config.push_notification_config.id, config=proto_utils.ToProto.task_push_notification_config( sample_task_push_notification_config ), ) ) - assert response.taskId == task_id + assert response.task_id == sample_task_push_notification_config.task_id @pytest.mark.asyncio async def test_set_task_callback_with_invalid_task( - grpc_client: A2AGrpcClient, + grpc_transport: GrpcTransport, mock_grpc_stub: AsyncMock, sample_task_push_notification_config: TaskPushNotificationConfig, ): """Test setting a task push notification config with a invalid task id.""" - task_id = 'task-1' - config_id = 'config-1' - mock_grpc_stub.CreateTaskPushNotificationConfig.return_value = ( - a2a_pb2.CreateTaskPushNotificationConfigRequest( - parent=f'invalid-path-to-tasks/{task_id}', - config_id=config_id, - config=proto_utils.ToProto.task_push_notification_config( - sample_task_push_notification_config - ), - ) + mock_grpc_stub.CreateTaskPushNotificationConfig.return_value = a2a_pb2.TaskPushNotificationConfig( + name=( + f'invalid-path-to-tasks/{sample_task_push_notification_config.task_id}/' + f'pushNotificationConfigs/{sample_task_push_notification_config.push_notification_config.id}' + ), + push_notification_config=a2a_pb2.PushNotificationConfig( + id=sample_task_push_notification_config.push_notification_config.id, + url=sample_task_push_notification_config.push_notification_config.url, + token=sample_task_push_notification_config.push_notification_config.token, + ), ) with pytest.raises(ServerError) as exc_info: - await grpc_client.set_task_callback( + await grpc_transport.set_task_callback( sample_task_push_notification_config ) - assert 'No task for' in exc_info.value.error.message + assert ( + 'Bad TaskPushNotificationConfig resource name' + in exc_info.value.error.message + ) @pytest.mark.asyncio async def test_get_task_callback_with_valid_task( - grpc_client: A2AGrpcClient, + grpc_transport: GrpcTransport, mock_grpc_stub: AsyncMock, sample_task_push_notification_config: TaskPushNotificationConfig, ): """Test retrieving a task push notification config with a valid task id.""" - task_id = 'task-1' - config_id = 'config-1' mock_grpc_stub.GetTaskPushNotificationConfig.return_value = ( - a2a_pb2.CreateTaskPushNotificationConfigRequest( - parent=f'tasks/{task_id}', - config_id=config_id, - config=proto_utils.ToProto.task_push_notification_config( - sample_task_push_notification_config - ), + proto_utils.ToProto.task_push_notification_config( + sample_task_push_notification_config ) ) - params = TaskIdParams(id=sample_task_push_notification_config.taskId) + params = GetTaskPushNotificationConfigParams( + id=sample_task_push_notification_config.task_id, + push_notification_config_id=sample_task_push_notification_config.push_notification_config.id, + ) - response = await grpc_client.get_task_callback(params) + response = await grpc_transport.get_task_callback(params) mock_grpc_stub.GetTaskPushNotificationConfig.assert_awaited_once_with( a2a_pb2.GetTaskPushNotificationConfigRequest( - name=f'tasks/{params.id}/pushNotification/undefined', + name=( + f'tasks/{params.id}/' + f'pushNotificationConfigs/{params.push_notification_config_id}' + ), ) ) - assert response.taskId == task_id + assert response.task_id == sample_task_push_notification_config.task_id @pytest.mark.asyncio async def test_get_task_callback_with_invalid_task( - grpc_client: A2AGrpcClient, + grpc_transport: GrpcTransport, mock_grpc_stub: AsyncMock, sample_task_push_notification_config: TaskPushNotificationConfig, ): """Test retrieving a task push notification config with a invalid task id.""" - task_id = 'task-1' - config_id = 'config-1' - mock_grpc_stub.GetTaskPushNotificationConfig.return_value = ( - a2a_pb2.CreateTaskPushNotificationConfigRequest( - parent=f'invalid-path-to-tasks/{task_id}', - config_id=config_id, - config=proto_utils.ToProto.task_push_notification_config( - sample_task_push_notification_config - ), - ) + mock_grpc_stub.GetTaskPushNotificationConfig.return_value = a2a_pb2.TaskPushNotificationConfig( + name=( + f'invalid-path-to-tasks/{sample_task_push_notification_config.task_id}/' + f'pushNotificationConfigs/{sample_task_push_notification_config.push_notification_config.id}' + ), + push_notification_config=a2a_pb2.PushNotificationConfig( + id=sample_task_push_notification_config.push_notification_config.id, + url=sample_task_push_notification_config.push_notification_config.url, + token=sample_task_push_notification_config.push_notification_config.token, + ), + ) + params = GetTaskPushNotificationConfigParams( + id=sample_task_push_notification_config.task_id, + push_notification_config_id=sample_task_push_notification_config.push_notification_config.id, ) - params = TaskIdParams(id=sample_task_push_notification_config.taskId) with pytest.raises(ServerError) as exc_info: - await grpc_client.get_task_callback(params) - assert 'No task for' in exc_info.value.error.message + await grpc_transport.get_task_callback(params) + assert ( + 'Bad TaskPushNotificationConfig resource name' + in exc_info.value.error.message + ) From f5b23402ffdc994a96f05bb7fcab8bc70de76425 Mon Sep 17 00:00:00 2001 From: Shingo OKAWA Date: Thu, 14 Aug 2025 01:14:47 +0900 Subject: [PATCH 5/7] fix: allow any characters in task name match pattern Signed-off-by: Shingo OKAWA --- src/a2a/utils/proto_utils.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/a2a/utils/proto_utils.py b/src/a2a/utils/proto_utils.py index 4df9a882..920ac72c 100644 --- a/src/a2a/utils/proto_utils.py +++ b/src/a2a/utils/proto_utils.py @@ -18,10 +18,8 @@ # Regexp patterns for matching -_TASK_NAME_MATCH = r'tasks/([a-zA-Z0-9_.-]+)' -_TASK_PUSH_CONFIG_NAME_MATCH = ( - r'tasks/([a-zA-Z0-9_.-]+)/pushNotificationConfigs/([a-zA-Z0-9_.-]+)' -) +_TASK_NAME_MATCH = r'tasks/([^/]+)' +_TASK_PUSH_CONFIG_NAME_MATCH = r'tasks/([^/]+)/pushNotificationConfigs/([^/]+)' class ToProto: From 31daf62b7b0b6c705a50073a57e748283ff49681 Mon Sep 17 00:00:00 2001 From: Holt Skinner <13262395+holtskinner@users.noreply.github.com> Date: Tue, 9 Sep 2025 15:14:43 -0500 Subject: [PATCH 6/7] Apply suggestions from code review Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- tests/client/test_grpc_client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/client/test_grpc_client.py b/tests/client/test_grpc_client.py index 3dd5482c..43124be3 100644 --- a/tests/client/test_grpc_client.py +++ b/tests/client/test_grpc_client.py @@ -356,7 +356,7 @@ async def test_set_task_callback_with_invalid_task( mock_grpc_stub: AsyncMock, sample_task_push_notification_config: TaskPushNotificationConfig, ): - """Test setting a task push notification config with a invalid task id.""" + """Test setting a task push notification config with an invalid task id.""" mock_grpc_stub.CreateTaskPushNotificationConfig.return_value = a2a_pb2.TaskPushNotificationConfig( name=( f'invalid-path-to-tasks/{sample_task_push_notification_config.task_id}/' @@ -415,7 +415,7 @@ async def test_get_task_callback_with_invalid_task( mock_grpc_stub: AsyncMock, sample_task_push_notification_config: TaskPushNotificationConfig, ): - """Test retrieving a task push notification config with a invalid task id.""" + """Test retrieving a task push notification config with an invalid task id.""" mock_grpc_stub.GetTaskPushNotificationConfig.return_value = a2a_pb2.TaskPushNotificationConfig( name=( f'invalid-path-to-tasks/{sample_task_push_notification_config.task_id}/' From dcc1e9f282ab0a25abff749e23fb3895ab0d7580 Mon Sep 17 00:00:00 2001 From: Holt Skinner Date: Tue, 9 Sep 2025 15:18:55 -0500 Subject: [PATCH 7/7] Address Gemini code assist comments --- tests/client/test_grpc_client.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/tests/client/test_grpc_client.py b/tests/client/test_grpc_client.py index 43124be3..19f5abc1 100644 --- a/tests/client/test_grpc_client.py +++ b/tests/client/test_grpc_client.py @@ -362,10 +362,8 @@ async def test_set_task_callback_with_invalid_task( f'invalid-path-to-tasks/{sample_task_push_notification_config.task_id}/' f'pushNotificationConfigs/{sample_task_push_notification_config.push_notification_config.id}' ), - push_notification_config=a2a_pb2.PushNotificationConfig( - id=sample_task_push_notification_config.push_notification_config.id, - url=sample_task_push_notification_config.push_notification_config.url, - token=sample_task_push_notification_config.push_notification_config.token, + push_notification_config=proto_utils.ToProto.push_notification_config( + sample_task_push_notification_config.push_notification_config ), ) @@ -421,10 +419,8 @@ async def test_get_task_callback_with_invalid_task( f'invalid-path-to-tasks/{sample_task_push_notification_config.task_id}/' f'pushNotificationConfigs/{sample_task_push_notification_config.push_notification_config.id}' ), - push_notification_config=a2a_pb2.PushNotificationConfig( - id=sample_task_push_notification_config.push_notification_config.id, - url=sample_task_push_notification_config.push_notification_config.url, - token=sample_task_push_notification_config.push_notification_config.token, + push_notification_config=proto_utils.ToProto.push_notification_config( + sample_task_push_notification_config.push_notification_config ), ) params = GetTaskPushNotificationConfigParams(