From 982f7adbd8bf6214250f2c3fbeee623e336614fc Mon Sep 17 00:00:00 2001 From: Connor Moss Date: Thu, 10 Sep 2026 16:42:59 -0400 Subject: [PATCH] fix(server): return the stored id when creating a push notification config Both create handlers returned the caller's request object rather than what the store persisted. The in-memory store defaults an empty id to the task id on the caller's object, so the id survived; the database store copies first and defaults on its private copy, so the response carried no id and reading the config back with it failed validation. Normalize the id in the handler before set_info so the response matches what was stored on every backend. --- .../default_request_handler.py | 6 +++ .../default_request_handler_v2.py | 6 +++ .../test_default_request_handler.py | 44 +++++++++++++++++++ .../test_default_request_handler_v2.py | 43 ++++++++++++++++++ 4 files changed, 99 insertions(+) diff --git a/src/a2a/server/request_handlers/default_request_handler.py b/src/a2a/server/request_handlers/default_request_handler.py index 384fd5e85..883e8558d 100644 --- a/src/a2a/server/request_handlers/default_request_handler.py +++ b/src/a2a/server/request_handlers/default_request_handler.py @@ -547,6 +547,12 @@ async def on_create_task_push_notification_config( await self._reject_unsafe_push_url(params.url) + # Stores default an empty id to the task id, but only the in-memory + # store does so on the caller's object. Normalize here so the returned + # config carries the id that was persisted, on every store. + if not params.id: + params.id = task_id + await self._push_config_store.set_info( task_id, params, diff --git a/src/a2a/server/request_handlers/default_request_handler_v2.py b/src/a2a/server/request_handlers/default_request_handler_v2.py index 59996236e..5c067b067 100644 --- a/src/a2a/server/request_handlers/default_request_handler_v2.py +++ b/src/a2a/server/request_handlers/default_request_handler_v2.py @@ -385,6 +385,12 @@ async def on_create_task_push_notification_config( # noqa: D102 await self._reject_unsafe_push_url(params.url) + # Stores default an empty id to the task id, but only the in-memory + # store does so on the caller's object. Normalize here so the returned + # config carries the id that was persisted, on every store. + if not params.id: + params.id = task_id + await self._push_config_store.set_info( task_id, params, diff --git a/tests/server/request_handlers/test_default_request_handler.py b/tests/server/request_handlers/test_default_request_handler.py index f9a2a0fb3..b13a6a1e2 100644 --- a/tests/server/request_handlers/test_default_request_handler.py +++ b/tests/server/request_handlers/test_default_request_handler.py @@ -1935,6 +1935,50 @@ async def test_set_task_push_notification_config_task_not_found(agent_card): mock_push_store.set_info.assert_not_awaited() +@pytest.mark.asyncio +@pytest.mark.parametrize('store_kind', ['inmemory', 'database']) +async def test_create_task_push_notification_config_returns_stored_id( + agent_card, store_kind +): + """Test on_create_task_push_notification_config returns the id that was stored.""" + if store_kind == 'database': + from a2a.server.tasks.database_push_notification_config_store import ( + DatabasePushNotificationConfigStore, + ) + from sqlalchemy.ext.asyncio import create_async_engine + + engine = create_async_engine( + 'sqlite+aiosqlite:///file:pushid?mode=memory&cache=shared&uri=true' + ) + push_config_store = DatabasePushNotificationConfigStore(engine=engine) + else: + push_config_store = InMemoryPushNotificationConfigStore() + + task = create_sample_task() + task_store = InMemoryTaskStore() + context = create_server_call_context() + await task_store.save(task, context) + + request_handler = DefaultRequestHandler( + agent_executor=MockAgentExecutor(), + task_store=task_store, + push_config_store=push_config_store, + agent_card=agent_card, + ) + params = TaskPushNotificationConfig( + task_id=task.id, + url='http://example.com', + ) + + response = await request_handler.on_create_task_push_notification_config( + params, context + ) + + stored = await push_config_store.get_info(task.id, context) + assert response.id == task.id + assert [config.id for config in stored] == [response.id] + + @pytest.mark.asyncio async def test_get_task_push_notification_config_no_store(agent_card): """Test on_get_task_push_notification_config when _push_config_store is None.""" diff --git a/tests/server/request_handlers/test_default_request_handler_v2.py b/tests/server/request_handlers/test_default_request_handler_v2.py index 2eb7e4725..08c91d424 100644 --- a/tests/server/request_handlers/test_default_request_handler_v2.py +++ b/tests/server/request_handlers/test_default_request_handler_v2.py @@ -511,6 +511,49 @@ async def test_set_task_push_notification_config_task_not_found(): mock_push_store.set_info.assert_not_awaited() +@pytest.mark.asyncio +@pytest.mark.parametrize('store_kind', ['inmemory', 'database']) +async def test_create_task_push_notification_config_returns_stored_id( + store_kind, +): + """Test on_create_task_push_notification_config returns the id that was stored.""" + if store_kind == 'database': + from a2a.server.tasks.database_push_notification_config_store import ( + DatabasePushNotificationConfigStore, + ) + from sqlalchemy.ext.asyncio import create_async_engine + + engine = create_async_engine( + 'sqlite+aiosqlite:///file:pushidv2?mode=memory&cache=shared&uri=true' + ) + push_config_store = DatabasePushNotificationConfigStore(engine=engine) + else: + push_config_store = InMemoryPushNotificationConfigStore() + + task = create_sample_task() + task_store = InMemoryTaskStore() + context = create_server_call_context() + await task_store.save(task, context) + + request_handler = DefaultRequestHandlerV2( + agent_executor=MockAgentExecutor(), + task_store=task_store, + push_config_store=push_config_store, + agent_card=create_default_agent_card(), + ) + params = TaskPushNotificationConfig( + task_id=task.id, url='http://example.com' + ) + + response = await request_handler.on_create_task_push_notification_config( + params, context + ) + + stored = await push_config_store.get_info(task.id, context) + assert response.id == task.id + assert [config.id for config in stored] == [response.id] + + @pytest.mark.asyncio async def test_get_task_push_notification_config_no_store(): """Test on_get_task_push_notification_config when _push_config_store is None."""