Skip to content

Commit aa8e966

Browse files
fix(compat): get a v0.3 push config by task id alone (#1380)
## Problem A v0.3 `pushNotificationConfig/get` with only a task id failed validation, because the v1.0 request had no config id. ## Fix When no config id is given, list the task's configs and return the one stored under the task id, otherwise the first. This matches v0.3. ## Tests `test_on_get_task_push_notification_config_by_task_id` runs through `DefaultRequestHandlerV2` and `InMemoryPushNotificationConfigStore`. It covers a set with and without an id and fails on main. 2329 passed. Fixes #1379 --------- Co-authored-by: mykytanetipa <mykytanetipa@google.com>
1 parent 4e916d8 commit aa8e966

2 files changed

Lines changed: 96 additions & 1 deletion

File tree

‎src/a2a/compat/v0_3/request_handler.py‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from a2a.compat.v0_3 import types as types_v03
77
from a2a.server.context import ServerCallContext
88
from a2a.server.request_handlers.request_handler import RequestHandler
9-
from a2a.types.a2a_pb2 import Task
9+
from a2a.types.a2a_pb2 import ListTaskPushNotificationConfigsRequest, Task
1010
from a2a.utils import proto_utils as core_proto_utils
1111
from a2a.utils.errors import TaskNotFoundError
1212

@@ -85,6 +85,24 @@ async def on_get_task_push_notification_config(
8585
v10_req = conversions.to_core_get_task_push_notification_config_request(
8686
request
8787
)
88+
# v0.3 allows a get by task id alone. Prefer the config stored under
89+
# the task id, which is where a config created without an id goes.
90+
# Otherwise take the first, like v0.3 did.
91+
if not v10_req.id:
92+
v10_resp = await self.request_handler.on_list_task_push_notification_configs(
93+
ListTaskPushNotificationConfigsRequest(task_id=v10_req.task_id),
94+
context,
95+
)
96+
configs = list(v10_resp.configs)
97+
v10_config = next(
98+
(c for c in configs if c.id == v10_req.task_id),
99+
configs[0] if configs else None,
100+
)
101+
if v10_config is None:
102+
raise TaskNotFoundError
103+
return conversions.to_compat_task_push_notification_config(
104+
v10_config
105+
)
88106
v10_config = (
89107
await self.request_handler.on_get_task_push_notification_config(
90108
v10_req, context

‎tests/compat/v0_3/test_request_handler.py‎

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@
22

33
import pytest
44

5+
from a2a.auth.user import UnauthenticatedUser
56
from a2a.compat.v0_3 import types as types_v03
67
from a2a.compat.v0_3.request_handler import RequestHandler03
8+
from a2a.server.agent_execution import AgentExecutor
79
from a2a.server.context import ServerCallContext
10+
from a2a.server.request_handlers import DefaultRequestHandlerV2
811
from a2a.server.request_handlers.request_handler import RequestHandler
12+
from a2a.server.tasks import InMemoryPushNotificationConfigStore, TaskStore
913
from a2a.types.a2a_pb2 import (
1014
AgentCapabilities,
1115
AgentCard,
@@ -241,6 +245,79 @@ async def test_on_get_task_push_notification_config(
241245
assert result.push_notification_config.url == 'http://example.com'
242246

243247

248+
@pytest.fixture
249+
def v03_handler_with_push_store():
250+
task_store = AsyncMock(spec=TaskStore)
251+
task_store.get.return_value = V10Task(id='task-1', context_id='ctx-1')
252+
core_handler = DefaultRequestHandlerV2(
253+
agent_executor=MagicMock(spec=AgentExecutor),
254+
task_store=task_store,
255+
push_config_store=InMemoryPushNotificationConfigStore(),
256+
agent_card=AgentCard(
257+
capabilities=AgentCapabilities(push_notifications=True)
258+
),
259+
)
260+
return RequestHandler03(request_handler=core_handler)
261+
262+
263+
@pytest.mark.anyio
264+
@pytest.mark.parametrize(
265+
('config_ids', 'expected_id'),
266+
[
267+
([None], 'task-1'),
268+
(['cfg-1'], 'cfg-1'),
269+
(['cfg-1', None], 'task-1'),
270+
],
271+
)
272+
async def test_on_get_task_push_notification_config_by_task_id(
273+
v03_handler_with_push_store, config_ids, expected_id
274+
):
275+
"""Without a config id, prefer the config stored under the task id."""
276+
context = ServerCallContext(user=UnauthenticatedUser())
277+
for config_id in config_ids:
278+
await v03_handler_with_push_store.on_create_task_push_notification_config(
279+
types_v03.SetTaskPushNotificationConfigRequest(
280+
id='req-1',
281+
method='tasks/pushNotificationConfig/set',
282+
params=types_v03.TaskPushNotificationConfig(
283+
task_id='task-1',
284+
push_notification_config=types_v03.PushNotificationConfig(
285+
id=config_id, url='http://example.com'
286+
),
287+
),
288+
),
289+
context,
290+
)
291+
292+
result = (
293+
await v03_handler_with_push_store.on_get_task_push_notification_config(
294+
types_v03.GetTaskPushNotificationConfigRequest(
295+
id='req-2',
296+
method='tasks/pushNotificationConfig/get',
297+
params=types_v03.TaskIdParams(id='task-1'),
298+
),
299+
context,
300+
)
301+
)
302+
303+
assert result.push_notification_config.id == expected_id
304+
305+
306+
@pytest.mark.anyio
307+
async def test_on_get_task_push_notification_config_by_task_id_none_set(
308+
v03_handler_with_push_store,
309+
):
310+
with pytest.raises(TaskNotFoundError):
311+
await v03_handler_with_push_store.on_get_task_push_notification_config(
312+
types_v03.GetTaskPushNotificationConfigRequest(
313+
id='req-1',
314+
method='tasks/pushNotificationConfig/get',
315+
params=types_v03.TaskIdParams(id='task-1'),
316+
),
317+
ServerCallContext(user=UnauthenticatedUser()),
318+
)
319+
320+
244321
@pytest.mark.anyio
245322
async def test_on_create_task_push_notification_config(
246323
v03_handler, mock_core_handler, mock_context

0 commit comments

Comments
 (0)