Skip to content

Commit a6a8c6a

Browse files
author
lkawka
committed
Resolve Mike's comments.
1 parent 852ef93 commit a6a8c6a

File tree

3 files changed

+114
-85
lines changed

3 files changed

+114
-85
lines changed

tests/e2e/push_notifications/notifications_app.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77

88
def create_notifications_app() -> FastAPI:
9-
"""Creates a simple push notification injesting HTTP+REST application."""
9+
"""Creates a simple push notification ingesting HTTP+REST application."""
1010
app = FastAPI()
1111
store_lock = asyncio.Lock()
1212
store: dict[str, list] = {}

tests/e2e/push_notifications/test_default_push_notification_support.py

Lines changed: 111 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,23 @@
1616
wait_for_server_ready,
1717
)
1818

19+
from a2a.client import (
20+
ClientConfig,
21+
ClientFactory,
22+
minimal_agent_card,
23+
)
24+
from a2a.types import (
25+
Message,
26+
Part,
27+
PushNotificationConfig,
28+
Role,
29+
Task,
30+
TaskPushNotificationConfig,
31+
TaskState,
32+
TextPart,
33+
TransportProtocol,
34+
)
35+
1936

2037
@pytest.fixture(scope='session')
2138
def port_lock():
@@ -90,42 +107,50 @@ async def http_client():
90107

91108
@pytest.mark.asyncio
92109
async def test_notification_triggering_with_in_message_config_e2e(
93-
notifications_server: str, agent_server: str, http_client: httpx.AsyncClient
110+
notifications_server: str,
111+
agent_server: str,
112+
http_client: httpx.AsyncClient,
94113
):
95114
"""
96115
Tests push notification triggering for in-message push notification config.
97116
"""
98-
# Send a message with a push notification config.
99-
response = await http_client.post(
100-
f'{agent_server}/v1/message:send',
101-
json={
102-
'configuration': {
103-
'pushNotification': {
104-
'id': 'n-1',
105-
'url': f'{notifications_server}/notifications',
106-
'token': uuid.uuid4().hex,
107-
},
108-
},
109-
'request': {
110-
'messageId': 'r-1',
111-
'role': 'ROLE_USER',
112-
'content': [{'text': 'Hello Agent!'}],
113-
},
114-
},
115-
)
116-
assert response.status_code == 200
117-
task_id = response.json()['task']['id']
118-
assert task_id is not None
117+
# Create an A2A client with a push notification config.
118+
a2a_client = ClientFactory(
119+
ClientConfig(
120+
supported_transports=[TransportProtocol.http_json],
121+
push_notification_configs=[
122+
PushNotificationConfig(
123+
id='in-message-config',
124+
url=f'{notifications_server}/notifications',
125+
token=uuid.uuid4().hex,
126+
)
127+
],
128+
)
129+
).create(minimal_agent_card(agent_server, [TransportProtocol.http_json]))
130+
131+
# Send a message and extract the returned task.
132+
responses = [
133+
response
134+
async for response in a2a_client.send_message(
135+
Message(
136+
message_id='hello-agent',
137+
parts=[Part(root=TextPart(text='Hello Agent!'))],
138+
role=Role.user,
139+
)
140+
)
141+
]
142+
assert len(responses) == 1
143+
assert isinstance(responses[0], tuple)
144+
assert isinstance(responses[0][0], Task)
145+
task = responses[0][0]
119146

120-
# Retrive and check notifcations.
147+
# Verify a single notification was sent.
121148
notifications = await wait_for_n_notifications(
122149
http_client,
123-
f'{notifications_server}/tasks/{task_id}/notifications',
124-
n=2,
150+
f'{notifications_server}/tasks/{task.id}/notifications',
151+
n=1,
125152
)
126-
states = [notification['status']['state'] for notification in notifications]
127-
assert 'completed' in states
128-
assert 'submitted' in states
153+
assert notifications[0].status.state == 'completed'
129154

130155

131156
@pytest.mark.asyncio
@@ -135,73 +160,78 @@ async def test_notification_triggering_after_config_change_e2e(
135160
"""
136161
Tests notification triggering after setting the push notificaiton config in a seperate call.
137162
"""
138-
# Send an initial message without the push notification config.
139-
response = await http_client.post(
140-
f'{agent_server}/v1/message:send',
141-
json={
142-
'request': {
143-
'messageId': 'r-1',
144-
'role': 'ROLE_USER',
145-
'content': [{'text': 'How are you?'}],
146-
},
147-
},
163+
# Configure an A2A client without a push notification config.
164+
a2a_client = ClientFactory(
165+
ClientConfig(
166+
supported_transports=[TransportProtocol.http_json],
167+
)
168+
).create(minimal_agent_card(agent_server, [TransportProtocol.http_json]))
169+
170+
# Send a message and extract the returned task.
171+
responses = [
172+
response
173+
async for response in a2a_client.send_message(
174+
Message(
175+
message_id='how-are-you',
176+
parts=[Part(root=TextPart(text='How are you?'))],
177+
role=Role.user,
178+
)
179+
)
180+
]
181+
assert len(responses) == 1
182+
assert isinstance(responses[0], tuple)
183+
assert isinstance(responses[0][0], Task)
184+
task = responses[0][0]
185+
assert task.status.state == TaskState.input_required
186+
187+
# Verify that no notification has been sent yet.
188+
response = await http_client.get(
189+
f'{notifications_server}/tasks/{task.id}/notifications'
148190
)
149191
assert response.status_code == 200
150-
assert response.json()['task']['id'] is not None
151-
task_id = response.json()['task']['id']
152-
153-
# Get the task to make sure that further input is required.
154-
response = await http_client.get(f'{agent_server}/v1/tasks/{task_id}')
155-
assert response.status_code == 200
156-
assert response.json()['status']['state'] == 'TASK_STATE_INPUT_REQUIRED'
157-
158-
# Set a push notification config.
159-
response = await http_client.post(
160-
f'{agent_server}/v1/tasks/{task_id}/pushNotificationConfigs',
161-
json={
162-
'parent': f'tasks/{task_id}',
163-
'configId': uuid.uuid4().hex,
164-
'config': {
165-
'name': 'test-config',
166-
'pushNotificationConfig': {
167-
'id': 'n-2',
168-
'url': f'{notifications_server}/notifications',
169-
'token': uuid.uuid4().hex,
170-
},
171-
},
172-
},
192+
assert len(response.json().get('notifications', [])) == 0
193+
194+
# Set the push notification config.
195+
await a2a_client.set_task_callback(
196+
TaskPushNotificationConfig(
197+
task_id=task.id,
198+
push_notification_config=PushNotificationConfig(
199+
id='after-config-change',
200+
url=f'{notifications_server}/notifications',
201+
token=uuid.uuid4().hex,
202+
),
203+
)
173204
)
174-
assert response.status_code == 200
175205

176-
# Send a follow-up message that should trigger a push notification.
177-
response = await http_client.post(
178-
f'{agent_server}/v1/message:send',
179-
json={
180-
'request': {
181-
'taskId': task_id,
182-
'messageId': 'r-2',
183-
'role': 'ROLE_USER',
184-
'content': [{'text': 'Good'}],
185-
},
186-
},
187-
)
188-
assert response.status_code == 200
206+
# Send another message that should trigger a push notification.
207+
responses = [
208+
response
209+
async for response in a2a_client.send_message(
210+
Message(
211+
task_id=task.id,
212+
message_id='good',
213+
parts=[Part(root=TextPart(text='Good'))],
214+
role=Role.user,
215+
)
216+
)
217+
]
218+
assert len(responses) == 1
189219

190-
# Retrive and check the notification.
220+
# Verify that the push notification was sent.
191221
notifications = await wait_for_n_notifications(
192222
http_client,
193-
f'{notifications_server}/tasks/{task_id}/notifications',
223+
f'{notifications_server}/tasks/{task.id}/notifications',
194224
n=1,
195225
)
196-
assert notifications[0]['status']['state'] == 'completed'
226+
assert notifications[0].status.state == 'completed'
197227

198228

199229
async def wait_for_n_notifications(
200230
http_client: httpx.AsyncClient,
201231
url: str,
202232
n: int,
203233
timeout: int = 3,
204-
):
234+
) -> list[Task]:
205235
"""
206236
Queries the notification URL until the desired number of notifications
207237
is received or the timeout is reached.
@@ -213,9 +243,9 @@ async def wait_for_n_notifications(
213243
assert response.status_code == 200
214244
notifications = response.json()['notifications']
215245
if len(notifications) == n:
216-
return notifications
246+
return [Task.model_validate(n) for n in notifications]
217247
if time.time() - start_time > timeout:
218248
raise TimeoutError(
219-
f'Notification retrieval timed out. Got {len(notifications)} notifications, want {n}.'
249+
f'Notification retrieval timed out. Got {len(notifications)} notification(s), want {n}. Retrieved notifications: {notifications}.'
220250
)
221251
await asyncio.sleep(0.1)

tests/e2e/push_notifications/utils.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import contextlib
12
import socket
23
import time
34

@@ -23,13 +24,11 @@ def wait_for_server_ready(url: str, timeout: int = 10) -> None:
2324
"""Polls the provided URL endpoint until the server is up."""
2425
start_time = time.time()
2526
while True:
26-
try:
27+
with contextlib.suppress(httpx.ConnectError):
2728
with httpx.Client() as client:
2829
response = client.get(url)
2930
if response.status_code == 200:
3031
return
31-
except httpx.ConnectError:
32-
pass
3332
if time.time() - start_time > timeout:
3433
raise TimeoutError(
3534
f'Server at {url} failed to start after {timeout}s'

0 commit comments

Comments
 (0)