Skip to content

Commit 00d93db

Browse files
claudebokelley
authored andcommitted
test(webhooks): add coverage for PydanticBaseModel branch in payload builders
Adds TestWebhookPayloadBuilderPydanticModel covering the model_dump path in create_mcp_webhook_payload and create_a2a_webhook_payload (both completed and working status paths). Also fixes the result param docstring in create_mcp_webhook_payload to match the widened type. https://claude.ai/code/session_013oiysa4CAeiSFFdnvFTHqh
1 parent fd703a6 commit 00d93db

2 files changed

Lines changed: 83 additions & 2 deletions

File tree

src/adcp/webhooks.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ def create_mcp_webhook_payload(
107107
status: Current task status
108108
task_type: Optionally type of AdCP operation (e.g., "get_products", "create_media_buy")
109109
timestamp: When the webhook was generated (defaults to current UTC time)
110-
result: Task-specific payload (AdCP response data)
110+
result: Task-specific payload — any Pydantic model or plain dict
111111
operation_id: Client-generated identifier the buyer embedded in the
112112
webhook URL when registering push-notification config. Publishers
113113
MUST echo this back in the payload so buyers correlate notifications

tests/test_webhook_handling.py

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,18 @@
1212
from a2a.types import TaskState, TaskStatusUpdateEvent
1313
from google.protobuf.json_format import MessageToDict as _MessageToDict
1414

15+
from pydantic import BaseModel
16+
1517
from adcp.client import ADCPClient
1618
from adcp.exceptions import ADCPWebhookSignatureError
19+
from adcp.types import GeneratedTaskStatus
1720
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+
)
1927
from tests.a2a_compat_shim import (
2028
Artifact,
2129
DataPart,
@@ -1186,6 +1194,79 @@ def test_extract_from_mcp_with_error_response(self):
11861194
assert result["errors"][0]["code"] == "INTERNAL_ERROR"
11871195

11881196

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+
11891270
# Load official AdCP HMAC test vectors from fixtures.
11901271
# Source: adcontextprotocol/adcp PR #2478 (merged 2026-04-20), which pins the
11911272
# canonical on-wire JSON form (compact separators) and adds rejection vectors

0 commit comments

Comments
 (0)