From 86b9783f3bb0c6c463ca6c8f1f07175772576996 Mon Sep 17 00:00:00 2001 From: Ivan Shymko Date: Wed, 18 Feb 2026 12:24:12 +0000 Subject: [PATCH 1/4] WIP: implement missing push notifications APIs --- src/a2a/client/base_client.py | 42 +++++++ src/a2a/client/client.py | 23 ++++ src/a2a/client/transports/base.py | 23 ++++ src/a2a/client/transports/grpc.py | 29 +++++ src/a2a/client/transports/jsonrpc.py | 66 ++++++++++ src/a2a/client/transports/rest.py | 76 ++++++++++++ src/a2a/server/apps/rest/rest_adapter.py | 6 + .../server/request_handlers/grpc_handler.py | 51 ++++++++ .../server/request_handlers/rest_handler.py | 49 ++++++-- tests/client/transports/test_grpc_client.py | 69 ++++++++++- .../client/transports/test_jsonrpc_client.py | 72 ++++++++++- tests/client/transports/test_rest_client.py | 93 ++++++++++++++ .../test_client_server_integration.py | 115 ++++++++++++++++++ 13 files changed, 701 insertions(+), 13 deletions(-) diff --git a/src/a2a/client/base_client.py b/src/a2a/client/base_client.py index 657e78aca..976b12588 100644 --- a/src/a2a/client/base_client.py +++ b/src/a2a/client/base_client.py @@ -18,8 +18,11 @@ AgentCard, CancelTaskRequest, CreateTaskPushNotificationConfigRequest, + DeleteTaskPushNotificationConfigRequest, GetTaskPushNotificationConfigRequest, GetTaskRequest, + ListTaskPushNotificationConfigRequest, + ListTaskPushNotificationConfigResponse, ListTasksRequest, ListTasksResponse, Message, @@ -247,6 +250,45 @@ async def get_task_callback( request, context=context, extensions=extensions ) + async def list_task_callback( + self, + request: ListTaskPushNotificationConfigRequest, + *, + context: ClientCallContext | None = None, + extensions: list[str] | None = None, + ) -> ListTaskPushNotificationConfigResponse: + """Lists push notification configurations for a specific task. + + Args: + request: The `ListTaskPushNotificationConfigRequest` object specifying the request. + context: The client call context. + extensions: List of extensions to be activated. + + Returns: + A `ListTaskPushNotificationConfigResponse` object. + """ + return await self._transport.list_task_callback( + request, context=context, extensions=extensions + ) + + async def delete_task_callback( + self, + request: DeleteTaskPushNotificationConfigRequest, + *, + context: ClientCallContext | None = None, + extensions: list[str] | None = None, + ) -> None: + """Deletes the push notification configuration for a specific task. + + Args: + request: The `DeleteTaskPushNotificationConfigRequest` object specifying the request. + context: The client call context. + extensions: List of extensions to be activated. + """ + await self._transport.delete_task_callback( + request, context=context, extensions=extensions + ) + async def subscribe( self, request: SubscribeToTaskRequest, diff --git a/src/a2a/client/client.py b/src/a2a/client/client.py index cad49173d..8ac77e118 100644 --- a/src/a2a/client/client.py +++ b/src/a2a/client/client.py @@ -13,8 +13,11 @@ AgentCard, CancelTaskRequest, CreateTaskPushNotificationConfigRequest, + DeleteTaskPushNotificationConfigRequest, GetTaskPushNotificationConfigRequest, GetTaskRequest, + ListTaskPushNotificationConfigRequest, + ListTaskPushNotificationConfigResponse, ListTasksRequest, ListTasksResponse, Message, @@ -175,6 +178,26 @@ async def get_task_callback( ) -> TaskPushNotificationConfig: """Retrieves the push notification configuration for a specific task.""" + @abstractmethod + async def list_task_callback( + self, + request: ListTaskPushNotificationConfigRequest, + *, + context: ClientCallContext | None = None, + extensions: list[str] | None = None, + ) -> ListTaskPushNotificationConfigResponse: + """Lists push notification configurations for a specific task.""" + + @abstractmethod + async def delete_task_callback( + self, + request: DeleteTaskPushNotificationConfigRequest, + *, + context: ClientCallContext | None = None, + extensions: list[str] | None = None, + ) -> None: + """Deletes the push notification configuration for a specific task.""" + @abstractmethod async def subscribe( self, diff --git a/src/a2a/client/transports/base.py b/src/a2a/client/transports/base.py index 933b10c66..6c8506408 100644 --- a/src/a2a/client/transports/base.py +++ b/src/a2a/client/transports/base.py @@ -9,8 +9,11 @@ AgentCard, CancelTaskRequest, CreateTaskPushNotificationConfigRequest, + DeleteTaskPushNotificationConfigRequest, GetTaskPushNotificationConfigRequest, GetTaskRequest, + ListTaskPushNotificationConfigRequest, + ListTaskPushNotificationConfigResponse, ListTasksRequest, ListTasksResponse, SendMessageRequest, @@ -110,6 +113,26 @@ async def get_task_callback( ) -> TaskPushNotificationConfig: """Retrieves the push notification configuration for a specific task.""" + @abstractmethod + async def list_task_callback( + self, + request: ListTaskPushNotificationConfigRequest, + *, + context: ClientCallContext | None = None, + extensions: list[str] | None = None, + ) -> ListTaskPushNotificationConfigResponse: + """Lists push notification configurations for a specific task.""" + + @abstractmethod + async def delete_task_callback( + self, + request: DeleteTaskPushNotificationConfigRequest, + *, + context: ClientCallContext | None = None, + extensions: list[str] | None = None, + ) -> None: + """Deletes the push notification configuration for a specific task.""" + @abstractmethod async def subscribe( self, diff --git a/src/a2a/client/transports/grpc.py b/src/a2a/client/transports/grpc.py index c73cf8faa..47df4958d 100644 --- a/src/a2a/client/transports/grpc.py +++ b/src/a2a/client/transports/grpc.py @@ -23,8 +23,11 @@ AgentCard, CancelTaskRequest, CreateTaskPushNotificationConfigRequest, + DeleteTaskPushNotificationConfigRequest, GetTaskPushNotificationConfigRequest, GetTaskRequest, + ListTaskPushNotificationConfigRequest, + ListTaskPushNotificationConfigResponse, ListTasksRequest, ListTasksResponse, SendMessageRequest, @@ -198,6 +201,32 @@ async def get_task_callback( metadata=self._get_grpc_metadata(extensions), ) + async def list_task_callback( + self, + request: ListTaskPushNotificationConfigRequest, + *, + context: ClientCallContext | None = None, + extensions: list[str] | None = None, + ) -> ListTaskPushNotificationConfigResponse: + """Lists push notification configurations for a specific task.""" + return await self.stub.ListTaskPushNotificationConfig( + request, + metadata=self._get_grpc_metadata(extensions), + ) + + async def delete_task_callback( + self, + request: DeleteTaskPushNotificationConfigRequest, + *, + context: ClientCallContext | None = None, + extensions: list[str] | None = None, + ) -> None: + """Deletes the push notification configuration for a specific task.""" + await self.stub.DeleteTaskPushNotificationConfig( + request, + metadata=self._get_grpc_metadata(extensions), + ) + async def get_extended_agent_card( self, *, diff --git a/src/a2a/client/transports/jsonrpc.py b/src/a2a/client/transports/jsonrpc.py index 9dea30ba3..8f32cb4c6 100644 --- a/src/a2a/client/transports/jsonrpc.py +++ b/src/a2a/client/transports/jsonrpc.py @@ -25,9 +25,12 @@ AgentCard, CancelTaskRequest, CreateTaskPushNotificationConfigRequest, + DeleteTaskPushNotificationConfigRequest, GetExtendedAgentCardRequest, GetTaskPushNotificationConfigRequest, GetTaskRequest, + ListTaskPushNotificationConfigRequest, + ListTaskPushNotificationConfigResponse, ListTasksRequest, ListTasksResponse, SendMessageRequest, @@ -378,6 +381,69 @@ async def get_task_callback( ) return response + async def list_task_callback( + self, + request: ListTaskPushNotificationConfigRequest, + *, + context: ClientCallContext | None = None, + extensions: list[str] | None = None, + ) -> ListTaskPushNotificationConfigResponse: + """Lists push notification configurations for a specific task.""" + rpc_request = JSONRPC20Request( + method='ListTaskPushNotificationConfig', + params=json_format.MessageToDict(request), + _id=str(uuid4()), + ) + modified_kwargs = update_extension_header( + self._get_http_args(context), + extensions if extensions is not None else self.extensions, + ) + payload, modified_kwargs = await self._apply_interceptors( + 'ListTaskPushNotificationConfig', + cast('dict[str, Any]', rpc_request.data), + modified_kwargs, + context, + ) + response_data = await self._send_request(payload, modified_kwargs) + json_rpc_response = JSONRPC20Response(**response_data) + if json_rpc_response.error: + raise A2AClientJSONRPCError(json_rpc_response.error) + response: ListTaskPushNotificationConfigResponse = ( + json_format.ParseDict( + json_rpc_response.result, + ListTaskPushNotificationConfigResponse(), + ) + ) + return response + + async def delete_task_callback( + self, + request: DeleteTaskPushNotificationConfigRequest, + *, + context: ClientCallContext | None = None, + extensions: list[str] | None = None, + ) -> None: + """Deletes the push notification configuration for a specific task.""" + rpc_request = JSONRPC20Request( + method='DeleteTaskPushNotificationConfig', + params=json_format.MessageToDict(request), + _id=str(uuid4()), + ) + modified_kwargs = update_extension_header( + self._get_http_args(context), + extensions if extensions is not None else self.extensions, + ) + payload, modified_kwargs = await self._apply_interceptors( + 'DeleteTaskPushNotificationConfig', + cast('dict[str, Any]', rpc_request.data), + modified_kwargs, + context, + ) + response_data = await self._send_request(payload, modified_kwargs) + json_rpc_response = JSONRPC20Response(**response_data) + if json_rpc_response.error: + raise A2AClientJSONRPCError(json_rpc_response.error) + async def subscribe( self, request: SubscribeToTaskRequest, diff --git a/src/a2a/client/transports/rest.py b/src/a2a/client/transports/rest.py index 316231c4a..b93ecfc5d 100644 --- a/src/a2a/client/transports/rest.py +++ b/src/a2a/client/transports/rest.py @@ -23,8 +23,11 @@ AgentCard, CancelTaskRequest, CreateTaskPushNotificationConfigRequest, + DeleteTaskPushNotificationConfigRequest, GetTaskPushNotificationConfigRequest, GetTaskRequest, + ListTaskPushNotificationConfigRequest, + ListTaskPushNotificationConfigResponse, ListTasksRequest, ListTasksResponse, SendMessageRequest, @@ -224,6 +227,21 @@ async def _send_get_request( ) ) + async def _send_delete_request( + self, + target: str, + query_params: dict[str, Any], + http_kwargs: dict[str, Any] | None = None, + ) -> dict[str, Any]: + return await self._send_request( + self.httpx_client.build_request( + 'DELETE', + f'{self.url}{target}', + params=query_params, + **(http_kwargs or {}), + ) + ) + async def get_task( self, request: GetTaskRequest, @@ -363,6 +381,64 @@ async def get_task_callback( ) return response + async def list_task_callback( + self, + request: ListTaskPushNotificationConfigRequest, + *, + context: ClientCallContext | None = None, + extensions: list[str] | None = None, + ) -> ListTaskPushNotificationConfigResponse: + """Lists push notification configurations for a specific task.""" + params = MessageToDict(request) + modified_kwargs = update_extension_header( + self._get_http_args(context), + extensions if extensions is not None else self.extensions, + ) + params, modified_kwargs = await self._apply_interceptors( + params, + modified_kwargs, + context, + ) + if 'task_id' in params: + del params['task_id'] + response_data = await self._send_get_request( + f'/v1/tasks/{request.task_id}/pushNotificationConfigs', + params, + modified_kwargs, + ) + response: ListTaskPushNotificationConfigResponse = ParseDict( + response_data, ListTaskPushNotificationConfigResponse() + ) + return response + + async def delete_task_callback( + self, + request: DeleteTaskPushNotificationConfigRequest, + *, + context: ClientCallContext | None = None, + extensions: list[str] | None = None, + ) -> None: + """Deletes the push notification configuration for a specific task.""" + params = MessageToDict(request) + modified_kwargs = update_extension_header( + self._get_http_args(context), + extensions if extensions is not None else self.extensions, + ) + params, modified_kwargs = await self._apply_interceptors( + params, + modified_kwargs, + context, + ) + if 'id' in params: + del params['id'] + if 'task_id' in params: + del params['task_id'] + await self._send_delete_request( + f'/v1/tasks/{request.task_id}/pushNotificationConfigs/{request.id}', + params, + modified_kwargs, + ) + async def subscribe( self, request: SubscribeToTaskRequest, diff --git a/src/a2a/server/apps/rest/rest_adapter.py b/src/a2a/server/apps/rest/rest_adapter.py index 8807f7ef5..3c1d1fc35 100644 --- a/src/a2a/server/apps/rest/rest_adapter.py +++ b/src/a2a/server/apps/rest/rest_adapter.py @@ -234,6 +234,12 @@ def routes(self) -> dict[tuple[str, str], Callable[[Request], Any]]: ): functools.partial( self._handle_request, self.handler.get_push_notification ), + ( + '/v1/tasks/{id}/pushNotificationConfigs/{push_id}', + 'DELETE', + ): functools.partial( + self._handle_request, self.handler.delete_push_notification + ), ( '/v1/tasks/{id}/pushNotificationConfigs', 'POST', diff --git a/src/a2a/server/request_handlers/grpc_handler.py b/src/a2a/server/request_handlers/grpc_handler.py index aab011357..9ddd83797 100644 --- a/src/a2a/server/request_handlers/grpc_handler.py +++ b/src/a2a/server/request_handlers/grpc_handler.py @@ -18,6 +18,8 @@ from collections.abc import Callable +from google.protobuf import empty_pb2 + import a2a.types.a2a_pb2_grpc as a2a_grpc from a2a import types @@ -293,6 +295,55 @@ async def CreateTaskPushNotificationConfig( await self.abort_context(e, context) return a2a_pb2.TaskPushNotificationConfig() + async def ListTaskPushNotificationConfig( + self, + request: a2a_pb2.ListTaskPushNotificationConfigRequest, + context: grpc.aio.ServicerContext, + ) -> a2a_pb2.ListTaskPushNotificationConfigResponse: + """Handles the 'ListTaskPushNotificationConfig' gRPC method. + + Args: + request: The incoming `ListTaskPushNotificationConfigRequest` object. + context: Context provided by the server. + + Returns: + A `ListTaskPushNotificationConfigResponse` object containing the configs. + """ + try: + server_context = self.context_builder.build(context) + return await self.request_handler.on_list_task_push_notification_config( + request, + server_context, + ) + except ServerError as e: + await self.abort_context(e, context) + return a2a_pb2.ListTaskPushNotificationConfigResponse() + + async def DeleteTaskPushNotificationConfig( + self, + request: a2a_pb2.DeleteTaskPushNotificationConfigRequest, + context: grpc.aio.ServicerContext, + ) -> empty_pb2.Empty: + """Handles the 'DeleteTaskPushNotificationConfig' gRPC method. + + Args: + request: The incoming `DeleteTaskPushNotificationConfigRequest` object. + context: Context provided by the server. + + Returns: + An empty `Empty` object. + """ + try: + server_context = self.context_builder.build(context) + await self.request_handler.on_delete_task_push_notification_config( + request, + server_context, + ) + return empty_pb2.Empty() + except ServerError as e: + await self.abort_context(e, context) + return empty_pb2.Empty() + async def GetTask( self, request: a2a_pb2.GetTaskRequest, diff --git a/src/a2a/server/request_handlers/rest_handler.py b/src/a2a/server/request_handlers/rest_handler.py index afa362147..69ffce12a 100644 --- a/src/a2a/server/request_handlers/rest_handler.py +++ b/src/a2a/server/request_handlers/rest_handler.py @@ -257,6 +257,30 @@ async def on_get_task( return MessageToDict(task) raise ServerError(error=TaskNotFoundError()) + async def delete_push_notification( + self, + request: Request, + context: ServerCallContext, + ) -> dict[str, Any]: + """Handles the 'tasks/pushNotificationConfig/delete' REST method. + + Args: + request: The incoming `Request` object. + context: Context provided by the server. + + Returns: + An empty `dict` representing the empty response. + """ + task_id = request.path_params['id'] + push_id = request.path_params['push_id'] + params = a2a_pb2.DeleteTaskPushNotificationConfigRequest( + task_id=task_id, id=push_id + ) + await self.request_handler.on_delete_task_push_notification_config( + params, context + ) + return {} + async def list_tasks( self, request: Request, @@ -264,17 +288,12 @@ async def list_tasks( ) -> dict[str, Any]: """Handles the 'tasks/list' REST method. - This method is currently not implemented. - Args: request: The incoming `Request` object. context: Context provided by the server. Returns: A list of `dict` representing the `Task` objects. - - Raises: - NotImplementedError: This method is not yet implemented. """ params = a2a_pb2.ListTasksRequest() # Parse query params, keeping arrays/repeated fields in mind if there are any @@ -293,16 +312,24 @@ async def list_push_notifications( ) -> dict[str, Any]: """Handles the 'tasks/pushNotificationConfig/list' REST method. - This method is currently not implemented. - Args: request: The incoming `Request` object. context: Context provided by the server. Returns: A list of `dict` representing the `TaskPushNotificationConfig` objects. - - Raises: - NotImplementedError: This method is not yet implemented. """ - raise NotImplementedError('list notifications not implemented') + task_id = request.path_params['id'] + params = a2a_pb2.ListTaskPushNotificationConfigRequest(task_id=task_id) + + # Parse query params, keeping arrays/repeated fields in mind if there are any + ParseDict( + dict(request.query_params), params, ignore_unknown_fields=True + ) + + result = ( + await self.request_handler.on_list_task_push_notification_config( + params, context + ) + ) + return MessageToDict(result) diff --git a/tests/client/transports/test_grpc_client.py b/tests/client/transports/test_grpc_client.py index 9632a335f..7b887ee1d 100644 --- a/tests/client/transports/test_grpc_client.py +++ b/tests/client/transports/test_grpc_client.py @@ -12,14 +12,17 @@ AgentCard, Artifact, AuthenticationInfo, + CreateTaskPushNotificationConfigRequest, + DeleteTaskPushNotificationConfigRequest, GetTaskPushNotificationConfigRequest, + ListTaskPushNotificationConfigRequest, + ListTaskPushNotificationConfigResponse, GetTaskRequest, Message, Part, PushNotificationConfig, Role, SendMessageRequest, - CreateTaskPushNotificationConfigRequest, Task, TaskArtifactUpdateEvent, TaskPushNotificationConfig, @@ -42,6 +45,8 @@ def mock_grpc_stub() -> AsyncMock: stub.CancelTask = AsyncMock() stub.CreateTaskPushNotificationConfig = AsyncMock() stub.GetTaskPushNotificationConfig = AsyncMock() + stub.ListTaskPushNotificationConfig = AsyncMock() + stub.DeleteTaskPushNotificationConfig = AsyncMock() return stub @@ -531,6 +536,68 @@ async def test_get_task_callback_with_invalid_task( assert response.task_id == 'invalid-path-to-task-1' +@pytest.mark.asyncio +async def test_list_task_callback( + grpc_transport: GrpcTransport, + mock_grpc_stub: AsyncMock, + sample_task_push_notification_config: TaskPushNotificationConfig, +) -> None: + """Test retrieving task push notification configs.""" + mock_grpc_stub.ListTaskPushNotificationConfig.return_value = ( + a2a_pb2.ListTaskPushNotificationConfigResponse( + configs=[sample_task_push_notification_config] + ) + ) + + response = await grpc_transport.list_task_callback( + ListTaskPushNotificationConfigRequest(task_id='task-1') + ) + + mock_grpc_stub.ListTaskPushNotificationConfig.assert_awaited_once_with( + a2a_pb2.ListTaskPushNotificationConfigRequest(task_id='task-1'), + metadata=[ + ( + HTTP_EXTENSION_HEADER.lower(), + 'https://example.com/test-ext/v1,https://example.com/test-ext/v2', + ) + ], + ) + assert len(response.configs) == 1 + assert response.configs[0].task_id == 'task-1' + + +@pytest.mark.asyncio +async def test_delete_task_callback( + grpc_transport: GrpcTransport, + mock_grpc_stub: AsyncMock, + sample_task_push_notification_config: TaskPushNotificationConfig, +) -> None: + """Test deleting task push notification config.""" + mock_grpc_stub.DeleteTaskPushNotificationConfig.return_value = ( + sample_task_push_notification_config + ) + + await grpc_transport.delete_task_callback( + DeleteTaskPushNotificationConfigRequest( + task_id='task-1', + id='config-1', + ) + ) + + mock_grpc_stub.DeleteTaskPushNotificationConfig.assert_awaited_once_with( + a2a_pb2.DeleteTaskPushNotificationConfigRequest( + task_id='task-1', + id='config-1', + ), + metadata=[ + ( + HTTP_EXTENSION_HEADER.lower(), + 'https://example.com/test-ext/v1,https://example.com/test-ext/v2', + ) + ], + ) + + @pytest.mark.parametrize( 'initial_extensions, input_extensions, expected_metadata', [ diff --git a/tests/client/transports/test_jsonrpc_client.py b/tests/client/transports/test_jsonrpc_client.py index f14ab9fa3..b6758ecad 100644 --- a/tests/client/transports/test_jsonrpc_client.py +++ b/tests/client/transports/test_jsonrpc_client.py @@ -23,14 +23,17 @@ AgentInterface, AgentCard, CancelTaskRequest, + CreateTaskPushNotificationConfigRequest, + DeleteTaskPushNotificationConfigRequest, GetTaskPushNotificationConfigRequest, + ListTaskPushNotificationConfigRequest, + ListTaskPushNotificationConfigResponse, GetTaskRequest, Message, Part, SendMessageConfiguration, SendMessageRequest, SendMessageResponse, - CreateTaskPushNotificationConfigRequest, Task, TaskPushNotificationConfig, TaskState, @@ -375,6 +378,73 @@ async def test_get_task_callback_success( payload = call_args[1]['json'] assert payload['method'] == 'GetTaskPushNotificationConfig' + @pytest.mark.asyncio + async def test_list_task_callback_success( + self, transport, mock_httpx_client + ): + """Test successful task multiple callbacks retrieval.""" + task_id = str(uuid4()) + mock_response = MagicMock() + mock_response.json.return_value = { + 'jsonrpc': '2.0', + 'id': '1', + 'result': { + 'configs': [ + { + 'task_id': f'{task_id}', + 'id': 'config-1', + 'push_notification_config': { + 'id': 'config-1', + 'url': 'https://example.com', + }, + } + ] + }, + } + mock_response.raise_for_status = MagicMock() + mock_httpx_client.post.return_value = mock_response + + request = ListTaskPushNotificationConfigRequest( + task_id=f'{task_id}', + ) + response = await transport.list_task_callback(request) + + assert len(response.configs) == 1 + assert response.configs[0].task_id == task_id + call_args = mock_httpx_client.post.call_args + payload = call_args[1]['json'] + assert payload['method'] == 'ListTaskPushNotificationConfig' + + @pytest.mark.asyncio + async def test_delete_task_callback_success( + self, transport, mock_httpx_client + ): + """Test successful task callback deletion.""" + task_id = str(uuid4()) + mock_response = MagicMock() + mock_response.json.return_value = { + 'jsonrpc': '2.0', + 'id': '1', + 'result': { + 'task_id': f'{task_id}', + 'id': 'config-1', + }, + } + mock_response.raise_for_status = MagicMock() + mock_httpx_client.post.return_value = mock_response + + request = DeleteTaskPushNotificationConfigRequest( + task_id=f'{task_id}', + id='config-1', + ) + response = await transport.delete_task_callback(request) + + mock_httpx_client.post.assert_called_once() # Assuming mock_send_request was a typo for mock_httpx_client.post + assert response is None + call_args = mock_httpx_client.post.call_args + payload = call_args[1]['json'] + assert payload['method'] == 'DeleteTaskPushNotificationConfig' + class TestClose: """Tests for the close method.""" diff --git a/tests/client/transports/test_rest_client.py b/tests/client/transports/test_rest_client.py index 8a5f3c620..eac58728a 100644 --- a/tests/client/transports/test_rest_client.py +++ b/tests/client/transports/test_rest_client.py @@ -15,8 +15,12 @@ AgentCapabilities, AgentCard, AgentInterface, + DeleteTaskPushNotificationConfigRequest, + ListTaskPushNotificationConfigRequest, + ListTaskPushNotificationConfigResponse, Role, SendMessageRequest, + TaskPushNotificationConfig, ) from a2a.utils.constants import TRANSPORT_HTTP_JSON @@ -309,3 +313,92 @@ async def test_get_card_with_extended_card_support_with_extensions( 'https://example.com/test-ext/v2', }, ) + + +class TestTaskCallback: + """Tests for the task callback methods.""" + + @pytest.mark.asyncio + async def test_list_task_callback_success( + self, mock_httpx_client: AsyncMock, mock_agent_card: MagicMock + ): + """Test successful task multiple callbacks retrieval.""" + client = RestTransport( + httpx_client=mock_httpx_client, agent_card=mock_agent_card + ) + task_id = 'task-1' + mock_response = AsyncMock(spec=httpx.Response) + mock_response.status_code = 200 + mock_response.json.return_value = { + 'configs': [ + { + 'taskId': task_id, + 'id': 'config-1', + 'pushNotificationConfig': { + 'id': 'config-1', + 'url': 'https://example.com', + }, + } + ] + } + mock_httpx_client.send.return_value = mock_response + + # Mock the build_request method to capture its inputs + mock_build_request = MagicMock( + return_value=AsyncMock(spec=httpx.Request) + ) + mock_httpx_client.build_request = mock_build_request + + request = ListTaskPushNotificationConfigRequest( + task_id=task_id, + ) + response = await client.list_task_callback(request) + + assert len(response.configs) == 1 + assert response.configs[0].task_id == task_id + + mock_build_request.assert_called_once() + call_args = mock_build_request.call_args + assert call_args[0][0] == 'GET' + assert f'/v1/tasks/{task_id}/pushNotificationConfigs' in call_args[0][1] + + @pytest.mark.asyncio + async def test_delete_task_callback_success( + self, mock_httpx_client: AsyncMock, mock_agent_card: MagicMock + ): + """Test successful task callback deletion.""" + client = RestTransport( + httpx_client=mock_httpx_client, agent_card=mock_agent_card + ) + task_id = 'task-1' + mock_response = AsyncMock(spec=httpx.Response) + mock_response.status_code = 200 + mock_response.json.return_value = { + 'taskId': task_id, + 'id': 'config-1', + 'pushNotificationConfig': { + 'id': 'config-1', + 'url': 'https://example.com', + }, + } + mock_httpx_client.send.return_value = mock_response + + # Mock the build_request method to capture its inputs + mock_build_request = MagicMock( + return_value=AsyncMock(spec=httpx.Request) + ) + mock_httpx_client.build_request = mock_build_request + + request = DeleteTaskPushNotificationConfigRequest( + task_id=task_id, + id='config-1', + ) + await client.delete_task_callback(request) + + mock_build_request.assert_called_once() + call_args = mock_build_request.call_args + assert call_args[0][0] == 'DELETE' + assert ( + f'/v1/tasks/{task_id}/pushNotificationConfigs/config-1' + in call_args[0][1] + ) diff --git a/tests/integration/test_client_server_integration.py b/tests/integration/test_client_server_integration.py index 011359fc3..08bea5bb7 100644 --- a/tests/integration/test_client_server_integration.py +++ b/tests/integration/test_client_server_integration.py @@ -41,6 +41,9 @@ Role, SendMessageRequest, CreateTaskPushNotificationConfigRequest, + DeleteTaskPushNotificationConfigRequest, + ListTaskPushNotificationConfigRequest, + ListTaskPushNotificationConfigResponse, SubscribeToTaskRequest, Task, TaskPushNotificationConfig, @@ -133,6 +136,12 @@ async def stream_side_effect(*args, **kwargs): CALLBACK_CONFIG ) handler.on_get_task_push_notification_config.return_value = CALLBACK_CONFIG + handler.on_list_task_push_notification_config.return_value = ( + ListTaskPushNotificationConfigResponse(configs=[CALLBACK_CONFIG]) + ) + handler.on_delete_task_push_notification_config.return_value = ( + CALLBACK_CONFIG + ) async def resubscribe_side_effect(*args, **kwargs): yield RESUBSCRIBE_EVENT @@ -710,6 +719,112 @@ def channel_factory(address: str) -> Channel: await transport.close() +@pytest.mark.asyncio +@pytest.mark.parametrize( + 'transport_setup_fixture', + [ + pytest.param('jsonrpc_setup', id='JSON-RPC'), + pytest.param('rest_setup', id='REST'), + ], +) +async def test_http_transport_list_task_callback( + transport_setup_fixture: str, request +) -> None: + transport_setup: TransportSetup = request.getfixturevalue( + transport_setup_fixture + ) + transport = transport_setup.transport + handler = transport_setup.handler + + params = ListTaskPushNotificationConfigRequest( + task_id=f'{CALLBACK_CONFIG.task_id}', + ) + result = await transport.list_task_callback(request=params) + + assert len(result.configs) == 1 + assert result.configs[0].task_id == CALLBACK_CONFIG.task_id + handler.on_list_task_push_notification_config.assert_awaited_once() + + if hasattr(transport, 'close'): + await transport.close() + + +@pytest.mark.asyncio +async def test_grpc_transport_list_task_callback( + grpc_server_and_handler: tuple[str, AsyncMock], + agent_card: AgentCard, +) -> None: + server_address, handler = grpc_server_and_handler + + def channel_factory(address: str) -> Channel: + return grpc.aio.insecure_channel(address) + + channel = channel_factory(server_address) + transport = GrpcTransport(channel=channel, agent_card=agent_card) + + params = ListTaskPushNotificationConfigRequest( + task_id=f'{CALLBACK_CONFIG.task_id}', + ) + result = await transport.list_task_callback(request=params) + + assert len(result.configs) == 1 + assert result.configs[0].task_id == CALLBACK_CONFIG.task_id + handler.on_list_task_push_notification_config.assert_awaited_once() + + await transport.close() + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + 'transport_setup_fixture', + [ + pytest.param('jsonrpc_setup', id='JSON-RPC'), + pytest.param('rest_setup', id='REST'), + ], +) +async def test_http_transport_delete_task_callback( + transport_setup_fixture: str, request +) -> None: + transport_setup: TransportSetup = request.getfixturevalue( + transport_setup_fixture + ) + transport = transport_setup.transport + handler = transport_setup.handler + + params = DeleteTaskPushNotificationConfigRequest( + task_id=f'{CALLBACK_CONFIG.task_id}', id=CALLBACK_CONFIG.id + ) + await transport.delete_task_callback(request=params) + + handler.on_delete_task_push_notification_config.assert_awaited_once() + + if hasattr(transport, 'close'): + await transport.close() + + +@pytest.mark.asyncio +async def test_grpc_transport_delete_task_callback( + grpc_server_and_handler: tuple[str, AsyncMock], + agent_card: AgentCard, +) -> None: + server_address, handler = grpc_server_and_handler + + def channel_factory(address: str) -> Channel: + return grpc.aio.insecure_channel(address) + + channel = channel_factory(server_address) + transport = GrpcTransport(channel=channel, agent_card=agent_card) + + params = DeleteTaskPushNotificationConfigRequest( + task_id=f'{CALLBACK_CONFIG.task_id}', id=CALLBACK_CONFIG.id + ) + await transport.delete_task_callback(request=params) + + handler.on_delete_task_push_notification_config.assert_awaited_once() + + await transport.close() + + @pytest.mark.asyncio @pytest.mark.parametrize( 'transport_setup_fixture', From d16a1f8624db82139b537983bfaee1eb5119ad4e Mon Sep 17 00:00:00 2001 From: Ivan Shymko Date: Fri, 27 Feb 2026 15:41:37 +0000 Subject: [PATCH 2/4] Fixes after merge --- src/a2a/client/base_client.py | 12 +++++----- src/a2a/client/client.py | 8 +++---- src/a2a/client/transports/base.py | 8 +++---- src/a2a/client/transports/grpc.py | 10 ++++----- src/a2a/client/transports/jsonrpc.py | 16 +++++++------- src/a2a/client/transports/rest.py | 12 +++++----- .../server/request_handlers/grpc_handler.py | 14 ++++++------ .../server/request_handlers/rest_handler.py | 4 ++-- tests/client/transports/test_grpc_client.py | 16 +++++++------- .../client/transports/test_jsonrpc_client.py | 10 ++++----- tests/client/transports/test_rest_client.py | 16 ++++++++------ .../test_client_server_integration.py | 22 ++++++++++--------- .../test_default_request_handler.py | 12 +++++----- tests/test_types.py | 4 +--- 14 files changed, 82 insertions(+), 82 deletions(-) diff --git a/src/a2a/client/base_client.py b/src/a2a/client/base_client.py index 976b12588..5654f1fa4 100644 --- a/src/a2a/client/base_client.py +++ b/src/a2a/client/base_client.py @@ -21,8 +21,8 @@ DeleteTaskPushNotificationConfigRequest, GetTaskPushNotificationConfigRequest, GetTaskRequest, - ListTaskPushNotificationConfigRequest, - ListTaskPushNotificationConfigResponse, + ListTaskPushNotificationConfigsRequest, + ListTaskPushNotificationConfigsResponse, ListTasksRequest, ListTasksResponse, Message, @@ -252,20 +252,20 @@ async def get_task_callback( async def list_task_callback( self, - request: ListTaskPushNotificationConfigRequest, + request: ListTaskPushNotificationConfigsRequest, *, context: ClientCallContext | None = None, extensions: list[str] | None = None, - ) -> ListTaskPushNotificationConfigResponse: + ) -> ListTaskPushNotificationConfigsResponse: """Lists push notification configurations for a specific task. Args: - request: The `ListTaskPushNotificationConfigRequest` object specifying the request. + request: The `ListTaskPushNotificationConfigsRequest` object specifying the request. context: The client call context. extensions: List of extensions to be activated. Returns: - A `ListTaskPushNotificationConfigResponse` object. + A `ListTaskPushNotificationConfigsResponse` object. """ return await self._transport.list_task_callback( request, context=context, extensions=extensions diff --git a/src/a2a/client/client.py b/src/a2a/client/client.py index 8ac77e118..ef9d8199f 100644 --- a/src/a2a/client/client.py +++ b/src/a2a/client/client.py @@ -16,8 +16,8 @@ DeleteTaskPushNotificationConfigRequest, GetTaskPushNotificationConfigRequest, GetTaskRequest, - ListTaskPushNotificationConfigRequest, - ListTaskPushNotificationConfigResponse, + ListTaskPushNotificationConfigsRequest, + ListTaskPushNotificationConfigsResponse, ListTasksRequest, ListTasksResponse, Message, @@ -181,11 +181,11 @@ async def get_task_callback( @abstractmethod async def list_task_callback( self, - request: ListTaskPushNotificationConfigRequest, + request: ListTaskPushNotificationConfigsRequest, *, context: ClientCallContext | None = None, extensions: list[str] | None = None, - ) -> ListTaskPushNotificationConfigResponse: + ) -> ListTaskPushNotificationConfigsResponse: """Lists push notification configurations for a specific task.""" @abstractmethod diff --git a/src/a2a/client/transports/base.py b/src/a2a/client/transports/base.py index 6c8506408..f578ba3e3 100644 --- a/src/a2a/client/transports/base.py +++ b/src/a2a/client/transports/base.py @@ -12,8 +12,8 @@ DeleteTaskPushNotificationConfigRequest, GetTaskPushNotificationConfigRequest, GetTaskRequest, - ListTaskPushNotificationConfigRequest, - ListTaskPushNotificationConfigResponse, + ListTaskPushNotificationConfigsRequest, + ListTaskPushNotificationConfigsResponse, ListTasksRequest, ListTasksResponse, SendMessageRequest, @@ -116,11 +116,11 @@ async def get_task_callback( @abstractmethod async def list_task_callback( self, - request: ListTaskPushNotificationConfigRequest, + request: ListTaskPushNotificationConfigsRequest, *, context: ClientCallContext | None = None, extensions: list[str] | None = None, - ) -> ListTaskPushNotificationConfigResponse: + ) -> ListTaskPushNotificationConfigsResponse: """Lists push notification configurations for a specific task.""" @abstractmethod diff --git a/src/a2a/client/transports/grpc.py b/src/a2a/client/transports/grpc.py index 47df4958d..eb201ae96 100644 --- a/src/a2a/client/transports/grpc.py +++ b/src/a2a/client/transports/grpc.py @@ -26,8 +26,8 @@ DeleteTaskPushNotificationConfigRequest, GetTaskPushNotificationConfigRequest, GetTaskRequest, - ListTaskPushNotificationConfigRequest, - ListTaskPushNotificationConfigResponse, + ListTaskPushNotificationConfigsRequest, + ListTaskPushNotificationConfigsResponse, ListTasksRequest, ListTasksResponse, SendMessageRequest, @@ -203,13 +203,13 @@ async def get_task_callback( async def list_task_callback( self, - request: ListTaskPushNotificationConfigRequest, + request: ListTaskPushNotificationConfigsRequest, *, context: ClientCallContext | None = None, extensions: list[str] | None = None, - ) -> ListTaskPushNotificationConfigResponse: + ) -> ListTaskPushNotificationConfigsResponse: """Lists push notification configurations for a specific task.""" - return await self.stub.ListTaskPushNotificationConfig( + return await self.stub.ListTaskPushNotificationConfigs( request, metadata=self._get_grpc_metadata(extensions), ) diff --git a/src/a2a/client/transports/jsonrpc.py b/src/a2a/client/transports/jsonrpc.py index 405f6d08a..6ee5dd79a 100644 --- a/src/a2a/client/transports/jsonrpc.py +++ b/src/a2a/client/transports/jsonrpc.py @@ -28,8 +28,8 @@ GetExtendedAgentCardRequest, GetTaskPushNotificationConfigRequest, GetTaskRequest, - ListTaskPushNotificationConfigRequest, - ListTaskPushNotificationConfigResponse, + ListTaskPushNotificationConfigsRequest, + ListTaskPushNotificationConfigsResponse, ListTasksRequest, ListTasksResponse, SendMessageRequest, @@ -369,14 +369,14 @@ async def get_task_callback( async def list_task_callback( self, - request: ListTaskPushNotificationConfigRequest, + request: ListTaskPushNotificationConfigsRequest, *, context: ClientCallContext | None = None, extensions: list[str] | None = None, - ) -> ListTaskPushNotificationConfigResponse: + ) -> ListTaskPushNotificationConfigsResponse: """Lists push notification configurations for a specific task.""" rpc_request = JSONRPC20Request( - method='ListTaskPushNotificationConfig', + method='ListTaskPushNotificationConfigs', params=json_format.MessageToDict(request), _id=str(uuid4()), ) @@ -385,7 +385,7 @@ async def list_task_callback( extensions if extensions is not None else self.extensions, ) payload, modified_kwargs = await self._apply_interceptors( - 'ListTaskPushNotificationConfig', + 'ListTaskPushNotificationConfigs', cast('dict[str, Any]', rpc_request.data), modified_kwargs, context, @@ -394,10 +394,10 @@ async def list_task_callback( json_rpc_response = JSONRPC20Response(**response_data) if json_rpc_response.error: raise A2AClientJSONRPCError(json_rpc_response.error) - response: ListTaskPushNotificationConfigResponse = ( + response: ListTaskPushNotificationConfigsResponse = ( json_format.ParseDict( json_rpc_response.result, - ListTaskPushNotificationConfigResponse(), + ListTaskPushNotificationConfigsResponse(), ) ) return response diff --git a/src/a2a/client/transports/rest.py b/src/a2a/client/transports/rest.py index 536ffa747..3699f9feb 100644 --- a/src/a2a/client/transports/rest.py +++ b/src/a2a/client/transports/rest.py @@ -25,8 +25,8 @@ DeleteTaskPushNotificationConfigRequest, GetTaskPushNotificationConfigRequest, GetTaskRequest, - ListTaskPushNotificationConfigRequest, - ListTaskPushNotificationConfigResponse, + ListTaskPushNotificationConfigsRequest, + ListTaskPushNotificationConfigsResponse, ListTasksRequest, ListTasksResponse, SendMessageRequest, @@ -358,11 +358,11 @@ async def get_task_callback( async def list_task_callback( self, - request: ListTaskPushNotificationConfigRequest, + request: ListTaskPushNotificationConfigsRequest, *, context: ClientCallContext | None = None, extensions: list[str] | None = None, - ) -> ListTaskPushNotificationConfigResponse: + ) -> ListTaskPushNotificationConfigsResponse: """Lists push notification configurations for a specific task.""" params = MessageToDict(request) modified_kwargs = update_extension_header( @@ -381,8 +381,8 @@ async def list_task_callback( params, modified_kwargs, ) - response: ListTaskPushNotificationConfigResponse = ParseDict( - response_data, ListTaskPushNotificationConfigResponse() + response: ListTaskPushNotificationConfigsResponse = ParseDict( + response_data, ListTaskPushNotificationConfigsResponse() ) return response diff --git a/src/a2a/server/request_handlers/grpc_handler.py b/src/a2a/server/request_handlers/grpc_handler.py index 07bad1807..f8624a7c6 100644 --- a/src/a2a/server/request_handlers/grpc_handler.py +++ b/src/a2a/server/request_handlers/grpc_handler.py @@ -294,29 +294,29 @@ async def CreateTaskPushNotificationConfig( await self.abort_context(e, context) return a2a_pb2.TaskPushNotificationConfig() - async def ListTaskPushNotificationConfig( + async def ListTaskPushNotificationConfigs( self, - request: a2a_pb2.ListTaskPushNotificationConfigRequest, + request: a2a_pb2.ListTaskPushNotificationConfigsRequest, context: grpc.aio.ServicerContext, - ) -> a2a_pb2.ListTaskPushNotificationConfigResponse: + ) -> a2a_pb2.ListTaskPushNotificationConfigsResponse: """Handles the 'ListTaskPushNotificationConfig' gRPC method. Args: - request: The incoming `ListTaskPushNotificationConfigRequest` object. + request: The incoming `ListTaskPushNotificationConfigsRequest` object. context: Context provided by the server. Returns: - A `ListTaskPushNotificationConfigResponse` object containing the configs. + A `ListTaskPushNotificationConfigsResponse` object containing the configs. """ try: server_context = self.context_builder.build(context) - return await self.request_handler.on_list_task_push_notification_config( + return await self.request_handler.on_list_task_push_notification_configs( request, server_context, ) except ServerError as e: await self.abort_context(e, context) - return a2a_pb2.ListTaskPushNotificationConfigResponse() + return a2a_pb2.ListTaskPushNotificationConfigsResponse() async def DeleteTaskPushNotificationConfig( self, diff --git a/src/a2a/server/request_handlers/rest_handler.py b/src/a2a/server/request_handlers/rest_handler.py index aae6a25d2..3f7ce6b5c 100644 --- a/src/a2a/server/request_handlers/rest_handler.py +++ b/src/a2a/server/request_handlers/rest_handler.py @@ -319,7 +319,7 @@ async def list_push_notifications( A list of `dict` representing the `TaskPushNotificationConfig` objects. """ task_id = request.path_params['id'] - params = a2a_pb2.ListTaskPushNotificationConfigRequest(task_id=task_id) + params = a2a_pb2.ListTaskPushNotificationConfigsRequest(task_id=task_id) # Parse query params, keeping arrays/repeated fields in mind if there are any ParseDict( @@ -327,7 +327,7 @@ async def list_push_notifications( ) result = ( - await self.request_handler.on_list_task_push_notification_config( + await self.request_handler.on_list_task_push_notification_configs( params, context ) ) diff --git a/tests/client/transports/test_grpc_client.py b/tests/client/transports/test_grpc_client.py index 6699ea95a..60d01958d 100644 --- a/tests/client/transports/test_grpc_client.py +++ b/tests/client/transports/test_grpc_client.py @@ -15,8 +15,8 @@ CreateTaskPushNotificationConfigRequest, DeleteTaskPushNotificationConfigRequest, GetTaskPushNotificationConfigRequest, - ListTaskPushNotificationConfigRequest, - ListTaskPushNotificationConfigResponse, + ListTaskPushNotificationConfigsRequest, + ListTaskPushNotificationConfigsResponse, GetTaskRequest, Message, Part, @@ -45,7 +45,7 @@ def mock_grpc_stub() -> AsyncMock: stub.CancelTask = AsyncMock() stub.CreateTaskPushNotificationConfig = AsyncMock() stub.GetTaskPushNotificationConfig = AsyncMock() - stub.ListTaskPushNotificationConfig = AsyncMock() + stub.ListTaskPushNotificationConfigs = AsyncMock() stub.DeleteTaskPushNotificationConfig = AsyncMock() return stub @@ -538,18 +538,18 @@ async def test_list_task_callback( sample_task_push_notification_config: TaskPushNotificationConfig, ) -> None: """Test retrieving task push notification configs.""" - mock_grpc_stub.ListTaskPushNotificationConfig.return_value = ( - a2a_pb2.ListTaskPushNotificationConfigResponse( + mock_grpc_stub.ListTaskPushNotificationConfigs.return_value = ( + a2a_pb2.ListTaskPushNotificationConfigsResponse( configs=[sample_task_push_notification_config] ) ) response = await grpc_transport.list_task_callback( - ListTaskPushNotificationConfigRequest(task_id='task-1') + ListTaskPushNotificationConfigsRequest(task_id='task-1') ) - mock_grpc_stub.ListTaskPushNotificationConfig.assert_awaited_once_with( - a2a_pb2.ListTaskPushNotificationConfigRequest(task_id='task-1'), + mock_grpc_stub.ListTaskPushNotificationConfigs.assert_awaited_once_with( + a2a_pb2.ListTaskPushNotificationConfigsRequest(task_id='task-1'), metadata=[ ( HTTP_EXTENSION_HEADER.lower(), diff --git a/tests/client/transports/test_jsonrpc_client.py b/tests/client/transports/test_jsonrpc_client.py index 9536babe1..14d27e862 100644 --- a/tests/client/transports/test_jsonrpc_client.py +++ b/tests/client/transports/test_jsonrpc_client.py @@ -26,8 +26,8 @@ CreateTaskPushNotificationConfigRequest, DeleteTaskPushNotificationConfigRequest, GetTaskPushNotificationConfigRequest, - ListTaskPushNotificationConfigRequest, - ListTaskPushNotificationConfigResponse, + ListTaskPushNotificationConfigsRequest, + ListTaskPushNotificationConfigsResponse, GetTaskRequest, Message, Part, @@ -371,7 +371,6 @@ async def test_list_task_callback_success( 'configs': [ { 'task_id': f'{task_id}', - 'id': 'config-1', 'push_notification_config': { 'id': 'config-1', 'url': 'https://example.com', @@ -383,7 +382,7 @@ async def test_list_task_callback_success( mock_response.raise_for_status = MagicMock() mock_httpx_client.post.return_value = mock_response - request = ListTaskPushNotificationConfigRequest( + request = ListTaskPushNotificationConfigsRequest( task_id=f'{task_id}', ) response = await transport.list_task_callback(request) @@ -392,7 +391,7 @@ async def test_list_task_callback_success( assert response.configs[0].task_id == task_id call_args = mock_httpx_client.post.call_args payload = call_args[1]['json'] - assert payload['method'] == 'ListTaskPushNotificationConfig' + assert payload['method'] == 'ListTaskPushNotificationConfigs' @pytest.mark.asyncio async def test_delete_task_callback_success( @@ -406,7 +405,6 @@ async def test_delete_task_callback_success( 'id': '1', 'result': { 'task_id': f'{task_id}', - 'id': 'config-1', }, } mock_response.raise_for_status = MagicMock() diff --git a/tests/client/transports/test_rest_client.py b/tests/client/transports/test_rest_client.py index f2b5e2d8d..29d546cae 100644 --- a/tests/client/transports/test_rest_client.py +++ b/tests/client/transports/test_rest_client.py @@ -16,8 +16,8 @@ AgentCard, AgentInterface, DeleteTaskPushNotificationConfigRequest, - ListTaskPushNotificationConfigRequest, - ListTaskPushNotificationConfigResponse, + ListTaskPushNotificationConfigsRequest, + ListTaskPushNotificationConfigsResponse, SendMessageRequest, TaskPushNotificationConfig, ) @@ -288,7 +288,9 @@ async def test_list_task_callback_success( ): """Test successful task multiple callbacks retrieval.""" client = RestTransport( - httpx_client=mock_httpx_client, agent_card=mock_agent_card + httpx_client=mock_httpx_client, + agent_card=mock_agent_card, + url='http://agent.example.com/api', ) task_id = 'task-1' mock_response = AsyncMock(spec=httpx.Response) @@ -297,7 +299,6 @@ async def test_list_task_callback_success( 'configs': [ { 'taskId': task_id, - 'id': 'config-1', 'pushNotificationConfig': { 'id': 'config-1', 'url': 'https://example.com', @@ -313,7 +314,7 @@ async def test_list_task_callback_success( ) mock_httpx_client.build_request = mock_build_request - request = ListTaskPushNotificationConfigRequest( + request = ListTaskPushNotificationConfigsRequest( task_id=task_id, ) response = await client.list_task_callback(request) @@ -332,14 +333,15 @@ async def test_delete_task_callback_success( ): """Test successful task callback deletion.""" client = RestTransport( - httpx_client=mock_httpx_client, agent_card=mock_agent_card + httpx_client=mock_httpx_client, + agent_card=mock_agent_card, + url='http://agent.example.com/api', ) task_id = 'task-1' mock_response = AsyncMock(spec=httpx.Response) mock_response.status_code = 200 mock_response.json.return_value = { 'taskId': task_id, - 'id': 'config-1', 'pushNotificationConfig': { 'id': 'config-1', 'url': 'https://example.com', diff --git a/tests/integration/test_client_server_integration.py b/tests/integration/test_client_server_integration.py index f90562697..8fbe03c1a 100644 --- a/tests/integration/test_client_server_integration.py +++ b/tests/integration/test_client_server_integration.py @@ -39,8 +39,8 @@ SendMessageRequest, CreateTaskPushNotificationConfigRequest, DeleteTaskPushNotificationConfigRequest, - ListTaskPushNotificationConfigRequest, - ListTaskPushNotificationConfigResponse, + ListTaskPushNotificationConfigsRequest, + ListTaskPushNotificationConfigsResponse, SubscribeToTaskRequest, Task, TaskPushNotificationConfig, @@ -134,8 +134,8 @@ async def stream_side_effect(*args, **kwargs): CALLBACK_CONFIG ) handler.on_get_task_push_notification_config.return_value = CALLBACK_CONFIG - handler.on_list_task_push_notification_config.return_value = ( - ListTaskPushNotificationConfigResponse(configs=[CALLBACK_CONFIG]) + handler.on_list_task_push_notification_configs.return_value = ( + ListTaskPushNotificationConfigsResponse(configs=[CALLBACK_CONFIG]) ) handler.on_delete_task_push_notification_config.return_value = ( CALLBACK_CONFIG @@ -750,14 +750,14 @@ async def test_http_transport_list_task_callback( transport = transport_setup.transport handler = transport_setup.handler - params = ListTaskPushNotificationConfigRequest( + params = ListTaskPushNotificationConfigsRequest( task_id=f'{CALLBACK_CONFIG.task_id}', ) result = await transport.list_task_callback(request=params) assert len(result.configs) == 1 assert result.configs[0].task_id == CALLBACK_CONFIG.task_id - handler.on_list_task_push_notification_config.assert_awaited_once() + handler.on_list_task_push_notification_configs.assert_awaited_once() if hasattr(transport, 'close'): await transport.close() @@ -776,14 +776,14 @@ def channel_factory(address: str) -> Channel: channel = channel_factory(server_address) transport = GrpcTransport(channel=channel, agent_card=agent_card) - params = ListTaskPushNotificationConfigRequest( + params = ListTaskPushNotificationConfigsRequest( task_id=f'{CALLBACK_CONFIG.task_id}', ) result = await transport.list_task_callback(request=params) assert len(result.configs) == 1 assert result.configs[0].task_id == CALLBACK_CONFIG.task_id - handler.on_list_task_push_notification_config.assert_awaited_once() + handler.on_list_task_push_notification_configs.assert_awaited_once() await transport.close() @@ -806,7 +806,8 @@ async def test_http_transport_delete_task_callback( handler = transport_setup.handler params = DeleteTaskPushNotificationConfigRequest( - task_id=f'{CALLBACK_CONFIG.task_id}', id=CALLBACK_CONFIG.id + task_id=f'{CALLBACK_CONFIG.task_id}', + id=CALLBACK_CONFIG.push_notification_config.id, ) await transport.delete_task_callback(request=params) @@ -830,7 +831,8 @@ def channel_factory(address: str) -> Channel: transport = GrpcTransport(channel=channel, agent_card=agent_card) params = DeleteTaskPushNotificationConfigRequest( - task_id=f'{CALLBACK_CONFIG.task_id}', id=CALLBACK_CONFIG.id + task_id=f'{CALLBACK_CONFIG.task_id}', + id=CALLBACK_CONFIG.push_notification_config.id, ) await transport.delete_task_callback(request=params) diff --git a/tests/server/request_handlers/test_default_request_handler.py b/tests/server/request_handlers/test_default_request_handler.py index 42b60e682..0c2d1f724 100644 --- a/tests/server/request_handlers/test_default_request_handler.py +++ b/tests/server/request_handlers/test_default_request_handler.py @@ -2265,7 +2265,7 @@ async def consume_stream(): @pytest.mark.asyncio async def test_list_task_push_notification_config_no_store(): - """Test on_list_task_push_notification_config when _push_config_store is None.""" + """Test on_list_task_push_notification_configs when _push_config_store is None.""" request_handler = DefaultRequestHandler( agent_executor=MockAgentExecutor(), task_store=AsyncMock(spec=TaskStore), @@ -2282,7 +2282,7 @@ async def test_list_task_push_notification_config_no_store(): @pytest.mark.asyncio async def test_list_task_push_notification_config_task_not_found(): - """Test on_list_task_push_notification_config when task is not found.""" + """Test on_list_task_push_notification_configs when task is not found.""" mock_task_store = AsyncMock(spec=TaskStore) mock_task_store.get.return_value = None # Task not found mock_push_store = AsyncMock(spec=PushNotificationConfigStore) @@ -2330,7 +2330,7 @@ async def test_list_no_task_push_notification_config_info(): @pytest.mark.asyncio async def test_list_task_push_notification_config_info_with_config(): - """Test on_list_task_push_notification_config with push config+id""" + """Test on_list_task_push_notification_configs with push config+id""" mock_task_store = AsyncMock(spec=TaskStore) sample_task = create_sample_task(task_id='non_existent_task') @@ -2367,7 +2367,7 @@ async def test_list_task_push_notification_config_info_with_config(): @pytest.mark.asyncio async def test_list_task_push_notification_config_info_with_config_and_no_id(): - """Test on_list_task_push_notification_config with no push config id""" + """Test on_list_task_push_notification_configs with no push config id""" mock_task_store = AsyncMock(spec=TaskStore) mock_task_store.get.return_value = Task(id='task_1', context_id='ctx_1') @@ -2497,7 +2497,7 @@ async def test_delete_no_task_push_notification_config_info(): @pytest.mark.asyncio async def test_delete_task_push_notification_config_info_with_config(): - """Test on_list_task_push_notification_config with push config+id""" + """Test on_list_task_push_notification_configs with push config+id""" mock_task_store = AsyncMock(spec=TaskStore) sample_task = create_sample_task(task_id='non_existent_task') @@ -2542,7 +2542,7 @@ async def test_delete_task_push_notification_config_info_with_config(): @pytest.mark.asyncio async def test_delete_task_push_notification_config_info_with_config_and_no_id(): - """Test on_list_task_push_notification_config with no push config id""" + """Test on_list_task_push_notification_configs with no push config id""" mock_task_store = AsyncMock(spec=TaskStore) sample_task = create_sample_task(task_id='non_existent_task') diff --git a/tests/test_types.py b/tests/test_types.py index fe37c32e2..99850468b 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -337,9 +337,7 @@ def test_set_task_push_notification_config_request(): def test_get_task_push_notification_config_request(): """Test GetTaskPushNotificationConfigRequest proto construction.""" - request = GetTaskPushNotificationConfigRequest( - task_id='task-123', id='config-1' - ) + request = GetTaskPushNotificationConfigRequest(task_id='task-123') assert request.task_id == 'task-123' From 1876aef9cbf61a1587a4d6af6dbec54a5fe709fa Mon Sep 17 00:00:00 2001 From: Ivan Shymko Date: Fri, 27 Feb 2026 16:04:16 +0000 Subject: [PATCH 3/4] Gemini fixes --- tests/client/transports/test_grpc_client.py | 4 +--- tests/client/transports/test_jsonrpc_client.py | 2 +- tests/client/transports/test_rest_client.py | 8 +------- tests/integration/test_client_server_integration.py | 4 +--- 4 files changed, 4 insertions(+), 14 deletions(-) diff --git a/tests/client/transports/test_grpc_client.py b/tests/client/transports/test_grpc_client.py index 60d01958d..a4a85a202 100644 --- a/tests/client/transports/test_grpc_client.py +++ b/tests/client/transports/test_grpc_client.py @@ -568,9 +568,7 @@ async def test_delete_task_callback( sample_task_push_notification_config: TaskPushNotificationConfig, ) -> None: """Test deleting task push notification config.""" - mock_grpc_stub.DeleteTaskPushNotificationConfig.return_value = ( - sample_task_push_notification_config - ) + mock_grpc_stub.DeleteTaskPushNotificationConfig.return_value = None await grpc_transport.delete_task_callback( DeleteTaskPushNotificationConfigRequest( diff --git a/tests/client/transports/test_jsonrpc_client.py b/tests/client/transports/test_jsonrpc_client.py index 14d27e862..e2f64f7e7 100644 --- a/tests/client/transports/test_jsonrpc_client.py +++ b/tests/client/transports/test_jsonrpc_client.py @@ -416,7 +416,7 @@ async def test_delete_task_callback_success( ) response = await transport.delete_task_callback(request) - mock_httpx_client.post.assert_called_once() # Assuming mock_send_request was a typo for mock_httpx_client.post + mock_httpx_client.post.assert_called_once() assert response is None call_args = mock_httpx_client.post.call_args payload = call_args[1]['json'] diff --git a/tests/client/transports/test_rest_client.py b/tests/client/transports/test_rest_client.py index 29d546cae..663d13284 100644 --- a/tests/client/transports/test_rest_client.py +++ b/tests/client/transports/test_rest_client.py @@ -340,13 +340,7 @@ async def test_delete_task_callback_success( task_id = 'task-1' mock_response = AsyncMock(spec=httpx.Response) mock_response.status_code = 200 - mock_response.json.return_value = { - 'taskId': task_id, - 'pushNotificationConfig': { - 'id': 'config-1', - 'url': 'https://example.com', - }, - } + mock_response.json.return_value = {} mock_httpx_client.send.return_value = mock_response # Mock the build_request method to capture its inputs diff --git a/tests/integration/test_client_server_integration.py b/tests/integration/test_client_server_integration.py index 8fbe03c1a..8284f1d07 100644 --- a/tests/integration/test_client_server_integration.py +++ b/tests/integration/test_client_server_integration.py @@ -137,9 +137,7 @@ async def stream_side_effect(*args, **kwargs): handler.on_list_task_push_notification_configs.return_value = ( ListTaskPushNotificationConfigsResponse(configs=[CALLBACK_CONFIG]) ) - handler.on_delete_task_push_notification_config.return_value = ( - CALLBACK_CONFIG - ) + handler.on_delete_task_push_notification_config.return_value = None async def resubscribe_side_effect(*args, **kwargs): yield RESUBSCRIBE_EVENT From 5218e6d46367bb8c70e85c89447213941b7ab110 Mon Sep 17 00:00:00 2001 From: Ivan Shymko Date: Mon, 2 Mar 2026 09:27:24 +0000 Subject: [PATCH 4/4] Revert not needed --- tests/test_types.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_types.py b/tests/test_types.py index 99850468b..fe37c32e2 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -337,7 +337,9 @@ def test_set_task_push_notification_config_request(): def test_get_task_push_notification_config_request(): """Test GetTaskPushNotificationConfigRequest proto construction.""" - request = GetTaskPushNotificationConfigRequest(task_id='task-123') + request = GetTaskPushNotificationConfigRequest( + task_id='task-123', id='config-1' + ) assert request.task_id == 'task-123'