Skip to content

Commit c7cfe6f

Browse files
bokelleyclaude
andauthored
fix(examples/multi_platform_seller): list_creatives populates query_summary (closes #510) (#521)
Both MockGuaranteedPlatform and MockNonGuaranteedPlatform were missing list_creatives entirely. The storyboard step ``creative_fate_after_cancellation/list_creatives_before_cancel`` fails for both tenants on the post-#508 artifact because the wire contract requires query_summary on every list_creatives response. Fix: each mock now persists creatives at sync_creatives time into an in-memory library keyed by creative_id, and list_creatives returns them with the required ``query_summary`` (total_matching, returned) and ``pagination`` (has_more=False, total_count) blocks. Per-item Creative shape projects to the schemas/3.0.6/creative/list-creatives-response.json contract: {creative_id, name, format_id, status: "approved", created_date, updated_date}. Auto-approval mirrors the existing sync_creatives policy on both mocks. Pagination is intentionally trivial (returns the full library) — the storyboard catalog is small and modeling cursor-based paging adds noise without illustrating new platform-shape concerns. Validated: ListCreativesResponse.model_validate accepts the output on both mocks; full pytest suite stays green (3760 passed). Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 863bb93 commit c7cfe6f

2 files changed

Lines changed: 118 additions & 0 deletions

File tree

examples/multi_platform_seller/src/mock_guaranteed.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,10 @@ def __init__(
155155
p.product_id: p.capacity_impressions for p in self._catalog.values()
156156
}
157157
self._buys: dict[str, _MediaBuy] = {}
158+
# Creative library — populated by sync_creatives, read by
159+
# list_creatives. Wire-shape dicts keyed by creative_id so
160+
# list_creatives can return them without re-projecting.
161+
self._creatives: dict[str, dict[str, Any]] = {}
158162

159163
# The router's AccountStore is what runtime dispatch threads
160164
# ctx.account through; this attribute exists only to satisfy
@@ -380,6 +384,9 @@ def sync_creatives(
380384
)
381385
buy.status = "pending_start"
382386
buy.creatives_attached += len(creatives)
387+
for i, c in enumerate(creatives):
388+
stored = _project_creative_to_wire(c, i)
389+
self._creatives[stored["creative_id"]] = stored
383390

384391
return {
385392
"creatives": [
@@ -392,6 +399,27 @@ def sync_creatives(
392399
],
393400
}
394401

402+
def list_creatives(
403+
self,
404+
req: Any,
405+
ctx: RequestContext[Any],
406+
) -> dict[str, Any]:
407+
"""Return the seller's view of buyer-uploaded creatives.
408+
409+
Returns the full library; pagination is not modeled (the mock
410+
runs against a small fixed-size storyboard catalog). The
411+
``query_summary`` block is required by
412+
``schemas/3.0.6/creative/list-creatives-response.json``.
413+
"""
414+
with self._lock:
415+
creatives = list(self._creatives.values())
416+
total = len(creatives)
417+
return {
418+
"query_summary": {"total_matching": total, "returned": total},
419+
"pagination": {"has_more": False, "total_count": total},
420+
"creatives": creatives,
421+
}
422+
395423
def get_media_buys(
396424
self,
397425
req: Any,
@@ -626,6 +654,37 @@ def _check_measurement_terms(terms: Any) -> None:
626654
)
627655

628656

657+
def _project_creative_to_wire(creative: Any, idx: int) -> dict[str, Any]:
658+
"""Project a sync_creatives input item to the
659+
``schemas/3.0.6/creative/list-creatives-response.json`` Creative
660+
shape. Auto-approval mirrors the sync_creatives policy: every
661+
submitted creative comes back as ``approved``."""
662+
creative_id = _creative_id(creative, idx)
663+
name = _attr(creative, "name", None) or creative_id
664+
raw_format = _attr(creative, "format_id", None)
665+
if isinstance(raw_format, dict):
666+
format_id = raw_format
667+
elif raw_format is not None:
668+
format_id = {
669+
"agent_url": "https://creative.adcontextprotocol.org/",
670+
"id": str(raw_format),
671+
}
672+
else:
673+
format_id = {
674+
"agent_url": "https://creative.adcontextprotocol.org/",
675+
"id": "display_300x250",
676+
}
677+
now_iso = datetime.now(timezone.utc).isoformat()
678+
return {
679+
"creative_id": creative_id,
680+
"name": str(name),
681+
"format_id": format_id,
682+
"status": "approved",
683+
"created_date": now_iso,
684+
"updated_date": now_iso,
685+
}
686+
687+
629688
def _project_media_buy_to_wire(buy: _MediaBuy) -> dict[str, Any]:
630689
"""Project an in-memory ``_MediaBuy`` to the
631690
``schemas/3.0.6/media-buy/get-media-buys-response.json`` MediaBuy

examples/multi_platform_seller/src/mock_non_guaranteed.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,10 @@ def __init__(
141141
# storyboard's delivery assertions stay deterministic.
142142
self._clearing_multiplier = clearing_multiplier
143143
self._buys: dict[str, _MediaBuy] = {}
144+
# Creative library — populated by sync_creatives, read by
145+
# list_creatives. Wire-shape dicts keyed by creative_id so
146+
# list_creatives can return them without re-projecting.
147+
self._creatives: dict[str, dict[str, Any]] = {}
144148

145149
accounts: Any = None # type: ignore[assignment]
146150

@@ -349,6 +353,9 @@ def sync_creatives(
349353
assert_media_buy_transition(buy.status, "active", media_buy_id=buy.media_buy_id)
350354
buy.status = "active"
351355
buy.creatives_attached += len(creatives)
356+
for i, c in enumerate(creatives):
357+
stored = _project_creative_to_wire(c, i)
358+
self._creatives[stored["creative_id"]] = stored
352359

353360
return {
354361
"creatives": [
@@ -361,6 +368,27 @@ def sync_creatives(
361368
],
362369
}
363370

371+
def list_creatives(
372+
self,
373+
req: Any,
374+
ctx: RequestContext[Any],
375+
) -> dict[str, Any]:
376+
"""Return the seller's view of buyer-uploaded creatives.
377+
378+
Returns the full library; pagination is not modeled (the mock
379+
runs against a small fixed-size storyboard catalog). The
380+
``query_summary`` block is required by
381+
``schemas/3.0.6/creative/list-creatives-response.json``.
382+
"""
383+
with self._lock:
384+
creatives = list(self._creatives.values())
385+
total = len(creatives)
386+
return {
387+
"query_summary": {"total_matching": total, "returned": total},
388+
"pagination": {"has_more": False, "total_count": total},
389+
"creatives": creatives,
390+
}
391+
364392
def get_media_buys(
365393
self,
366394
req: Any,
@@ -597,6 +625,37 @@ def _check_measurement_terms(terms: Any) -> None:
597625
)
598626

599627

628+
def _project_creative_to_wire(creative: Any, idx: int) -> dict[str, Any]:
629+
"""Project a sync_creatives input item to the
630+
``schemas/3.0.6/creative/list-creatives-response.json`` Creative
631+
shape. Auto-approval mirrors the sync_creatives policy: every
632+
submitted creative comes back as ``approved``."""
633+
creative_id = _creative_id(creative, idx)
634+
name = _attr(creative, "name", None) or creative_id
635+
raw_format = _attr(creative, "format_id", None)
636+
if isinstance(raw_format, dict):
637+
format_id = raw_format
638+
elif raw_format is not None:
639+
format_id = {
640+
"agent_url": "https://creative.adcontextprotocol.org/",
641+
"id": str(raw_format),
642+
}
643+
else:
644+
format_id = {
645+
"agent_url": "https://creative.adcontextprotocol.org/",
646+
"id": "display_300x250",
647+
}
648+
now_iso = datetime.now(timezone.utc).isoformat()
649+
return {
650+
"creative_id": creative_id,
651+
"name": str(name),
652+
"format_id": format_id,
653+
"status": "approved",
654+
"created_date": now_iso,
655+
"updated_date": now_iso,
656+
}
657+
658+
600659
def _project_media_buy_to_wire(buy: _MediaBuy) -> dict[str, Any]:
601660
"""Project an in-memory ``_MediaBuy`` to the
602661
``schemas/3.0.6/media-buy/get-media-buys-response.json`` shape."""

0 commit comments

Comments
 (0)