|
12 | 12 | from a2a.types import TaskState, TaskStatusUpdateEvent |
13 | 13 | from google.protobuf.json_format import MessageToDict as _MessageToDict |
14 | 14 |
|
| 15 | +from pydantic import BaseModel |
| 16 | + |
15 | 17 | from adcp.client import ADCPClient |
16 | 18 | from adcp.exceptions import ADCPWebhookSignatureError |
| 19 | +from adcp.types import GeneratedTaskStatus |
17 | 20 | from adcp.types.core import AgentConfig, Protocol, TaskStatus |
18 | | -from adcp.webhooks import extract_webhook_result_data, get_adcp_signed_headers_for_webhook |
| 21 | +from adcp.webhooks import ( |
| 22 | + create_a2a_webhook_payload, |
| 23 | + create_mcp_webhook_payload, |
| 24 | + extract_webhook_result_data, |
| 25 | + get_adcp_signed_headers_for_webhook, |
| 26 | +) |
19 | 27 | from tests.a2a_compat_shim import ( |
20 | 28 | Artifact, |
21 | 29 | DataPart, |
@@ -1186,6 +1194,79 @@ def test_extract_from_mcp_with_error_response(self): |
1186 | 1194 | assert result["errors"][0]["code"] == "INTERNAL_ERROR" |
1187 | 1195 |
|
1188 | 1196 |
|
| 1197 | +class _DeliveryResponse(BaseModel): |
| 1198 | + """Minimal Pydantic model for testing the BaseModel branch in payload builders.""" |
| 1199 | + |
| 1200 | + media_buy_id: str |
| 1201 | + buyer_ref: str |
| 1202 | + packages: list[str] = [] |
| 1203 | + |
| 1204 | + |
| 1205 | +class TestWebhookPayloadBuilderPydanticModel: |
| 1206 | + """Pydantic BaseModel inputs to create_mcp_webhook_payload / create_a2a_webhook_payload. |
| 1207 | +
|
| 1208 | + Regression guard for the PydanticBaseModel branch (model_dump path inside |
| 1209 | + both builders). Prior to the fix these functions were typed to accept only |
| 1210 | + AdcpAsyncResponseData, which is a narrow discriminated union — passing any |
| 1211 | + other BaseModel subclass required a type: ignore comment even though the |
| 1212 | + runtime hasattr(result, "model_dump") guard handled it correctly. |
| 1213 | + """ |
| 1214 | + |
| 1215 | + def test_create_mcp_payload_accepts_pydantic_model(self): |
| 1216 | + model = _DeliveryResponse(media_buy_id="mb_1", buyer_ref="ref_1") |
| 1217 | + payload = create_mcp_webhook_payload( |
| 1218 | + task_id="task_1", |
| 1219 | + task_type="media_buy_delivery", |
| 1220 | + status=GeneratedTaskStatus.completed, |
| 1221 | + result=model, |
| 1222 | + ) |
| 1223 | + assert payload["result"] == {"media_buy_id": "mb_1", "buyer_ref": "ref_1", "packages": []} |
| 1224 | + |
| 1225 | + def test_create_mcp_payload_pydantic_model_serialized_as_json(self): |
| 1226 | + model = _DeliveryResponse(media_buy_id="mb_2", buyer_ref="ref_2", packages=["pkg_a"]) |
| 1227 | + payload = create_mcp_webhook_payload( |
| 1228 | + task_id="task_2", |
| 1229 | + task_type="media_buy_delivery", |
| 1230 | + status=GeneratedTaskStatus.completed, |
| 1231 | + result=model, |
| 1232 | + ) |
| 1233 | + result = payload["result"] |
| 1234 | + assert isinstance(result, dict) |
| 1235 | + assert result["packages"] == ["pkg_a"] |
| 1236 | + |
| 1237 | + def test_create_a2a_payload_accepts_pydantic_model_completed(self): |
| 1238 | + from a2a.types import Task as A2ATask |
| 1239 | + |
| 1240 | + model = _DeliveryResponse(media_buy_id="mb_3", buyer_ref="ref_3") |
| 1241 | + task = create_a2a_webhook_payload( |
| 1242 | + task_id="task_3", |
| 1243 | + context_id="ctx_3", |
| 1244 | + status=GeneratedTaskStatus.completed, |
| 1245 | + result=model, |
| 1246 | + ) |
| 1247 | + assert isinstance(task, A2ATask) |
| 1248 | + task_dict = _MessageToDict(task, preserving_proto_field_name=False) |
| 1249 | + extracted = extract_webhook_result_data(task_dict) |
| 1250 | + assert extracted is not None |
| 1251 | + assert extracted["media_buy_id"] == "mb_3" |
| 1252 | + |
| 1253 | + def test_create_a2a_payload_accepts_pydantic_model_working(self): |
| 1254 | + from a2a.types import TaskStatusUpdateEvent as A2AEvent |
| 1255 | + |
| 1256 | + model = _DeliveryResponse(media_buy_id="mb_4", buyer_ref="ref_4") |
| 1257 | + event = create_a2a_webhook_payload( |
| 1258 | + task_id="task_4", |
| 1259 | + context_id="ctx_4", |
| 1260 | + status=GeneratedTaskStatus.working, |
| 1261 | + result=model, |
| 1262 | + ) |
| 1263 | + assert isinstance(event, A2AEvent) |
| 1264 | + event_dict = _MessageToDict(event, preserving_proto_field_name=False) |
| 1265 | + extracted = extract_webhook_result_data(event_dict) |
| 1266 | + assert extracted is not None |
| 1267 | + assert extracted["media_buy_id"] == "mb_4" |
| 1268 | + |
| 1269 | + |
1189 | 1270 | # Load official AdCP HMAC test vectors from fixtures. |
1190 | 1271 | # Source: adcontextprotocol/adcp PR #2478 (merged 2026-04-20), which pins the |
1191 | 1272 | # canonical on-wire JSON form (compact separators) and adds rejection vectors |
|
0 commit comments