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' )
2138def port_lock ():
@@ -90,42 +107,50 @@ async def http_client():
90107
91108@pytest .mark .asyncio
92109async 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
199229async 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 )
0 commit comments