@@ -157,6 +157,110 @@ def _make_ctx(account: Account[Any]) -> RequestContext[Any]:
157157 return RequestContext (account = account )
158158
159159
160+ class _CanonicalRouterChild (DecisioningPlatform ):
161+ capabilities = _capabilities (["sales-non-guaranteed" ])
162+ accounts = _SyncSalesPlatform .accounts
163+ __init__ = _SyncSalesPlatform .__init__
164+ get_products = _SyncSalesPlatform .get_products
165+ create_media_buy = _SyncSalesPlatform .create_media_buy
166+ update_media_buy = _SyncSalesPlatform .update_media_buy
167+ sync_creatives = _SyncSalesPlatform .sync_creatives
168+ get_media_buy_delivery = _SyncSalesPlatform .get_media_buy_delivery
169+
170+
171+ class _OptionalRouterChild (_CanonicalRouterChild ):
172+ def list_creative_formats_legacy (self , req : Any , ctx : Any ) -> Any :
173+ return {"formats" : []}
174+
175+ def preview_creative_legacy (self , req : Any , ctx : Any ) -> Any :
176+ return {"previews" : []}
177+
178+ def list_products (self , req : Any , ctx : Any ) -> Any :
179+ return {"products" : []}
180+
181+
182+ @pytest .mark .asyncio
183+ @pytest .mark .parametrize ("router_kind" , ["eager" , "lazy" , "registry" , "nested" ])
184+ @pytest .mark .parametrize ("implemented" , [False , True ])
185+ async def test_router_optional_advertisement_and_tenant_refusal (
186+ router_kind : str , implemented : bool
187+ ) -> None :
188+ from concurrent .futures import ThreadPoolExecutor
189+
190+ from adcp .decisioning import LazyPlatformRouter
191+ from adcp .decisioning .capabilities import MediaBuy
192+ from adcp .decisioning .handler import PlatformHandler
193+ from adcp .decisioning .task_registry import InMemoryTaskRegistry
194+ from adcp .server .tenant_registry import TenantRegistry
195+
196+ accounts = _make_routing_account_store ({"acct_a" : "tenant-a" , "acct_b" : "tenant-b" })
197+ caps = _capabilities (["sales-non-guaranteed" , "creative-template" ])
198+ caps .media_buy = MediaBuy (lifecycle_tools = ["list_products" , "buy_products" ])
199+ optional = {"list_creative_formats_legacy" , "preview_creative_legacy" , "list_products" }
200+ declaration = optional if implemented else set ()
201+ child = _OptionalRouterChild ("a" ) if implemented else _CanonicalRouterChild ("a" )
202+ canonical = _CanonicalRouterChild ("b" )
203+ constructed : list [str ] = []
204+
205+ async def factory (tenant_id : str ) -> DecisioningPlatform :
206+ constructed .append (tenant_id )
207+ return child if tenant_id == "tenant-a" else canonical
208+
209+ if router_kind == "eager" :
210+ router = PlatformRouter (
211+ accounts = accounts ,
212+ platforms = {"tenant-a" : child , "tenant-b" : canonical },
213+ capabilities = caps ,
214+ )
215+ elif router_kind in {"lazy" , "nested" }:
216+ lazy = LazyPlatformRouter (
217+ accounts = accounts , factory = factory , capabilities = caps , optional_methods = declaration
218+ )
219+ router = (
220+ lazy
221+ if router_kind == "lazy"
222+ else PlatformRouter (
223+ accounts = accounts , platforms = {"tenant-a" : lazy , "tenant-b" : lazy }, capabilities = caps
224+ )
225+ )
226+ else :
227+ registry = TenantRegistry (validator = None )
228+ for tenant_id in ("tenant-a" , "tenant-b" ):
229+ await registry .register_lazy (
230+ tenant_id , agent_url = f"https://{ tenant_id } .example.com" , factory = factory
231+ )
232+ router = registry .as_platform (
233+ accounts = accounts , capabilities = caps , optional_methods = declaration
234+ )
235+ with ThreadPoolExecutor () as executor :
236+ handler = PlatformHandler (router , executor = executor , registry = InMemoryTaskRegistry ())
237+ tools = handler .advertised_tools_for_instance ()
238+ assert constructed == []
239+ expected = {"list_creative_formats" , "preview_creative" , "list_products" }
240+ assert tools & (expected | {"buy_products" }) == (expected if implemented else set ())
241+ assert isinstance (router .optional_methods , frozenset )
242+
243+ if implemented :
244+ ctx = _make_ctx (Account (id = "acct_a" , metadata = {"tenant_id" : "tenant-a" }))
245+ ctx .tenant_id = "tenant-a"
246+ assert await router .list_creative_formats_legacy ({}, ctx ) == {"formats" : []}
247+ ctx = _make_ctx (Account (id = "acct_b" , metadata = {"tenant_id" : "tenant-b" }))
248+ ctx .tenant_id = "tenant-b"
249+ with pytest .raises (AdcpError ) as caught :
250+ await router .list_creative_formats_legacy ({}, ctx )
251+ assert caught .value .code == "UNSUPPORTED_FEATURE"
252+ assert "because at least one" not in str (caught .value )
253+
254+
255+ def test_eager_optional_declaration_excludes_inherited_protocol_stubs () -> None :
256+ router = PlatformRouter (
257+ accounts = _make_routing_account_store ({"acct_a" : "tenant-a" }),
258+ platforms = {"tenant-a" : _SyncSalesPlatform ("a" )},
259+ capabilities = _capabilities (["sales-non-guaranteed" ]),
260+ )
261+ assert "list_creative_formats_legacy" not in router .optional_methods
262+
263+
160264# ---------------------------------------------------------------------------
161265# Tests
162266# ---------------------------------------------------------------------------
@@ -515,107 +619,3 @@ def test_account_store_methods_denylist_matches_protocols() -> None:
515619 f"AccountStore Protocol method drift. Update _ACCOUNT_STORE_METHODS "
516620 f"in platform_router.py: { sorted (drift )} "
517621 )
518-
519-
520- class _CanonicalRouterChild (DecisioningPlatform ):
521- capabilities = _capabilities (["sales-non-guaranteed" ])
522- accounts = _SyncSalesPlatform .accounts
523- __init__ = _SyncSalesPlatform .__init__
524- get_products = _SyncSalesPlatform .get_products
525- create_media_buy = _SyncSalesPlatform .create_media_buy
526- update_media_buy = _SyncSalesPlatform .update_media_buy
527- sync_creatives = _SyncSalesPlatform .sync_creatives
528- get_media_buy_delivery = _SyncSalesPlatform .get_media_buy_delivery
529-
530-
531- class _OptionalRouterChild (_CanonicalRouterChild ):
532- def list_creative_formats_legacy (self , req : Any , ctx : Any ) -> Any :
533- return {"formats" : []}
534-
535- def preview_creative_legacy (self , req : Any , ctx : Any ) -> Any :
536- return {"previews" : []}
537-
538- def list_products (self , req : Any , ctx : Any ) -> Any :
539- return {"products" : []}
540-
541-
542- @pytest .mark .asyncio
543- @pytest .mark .parametrize ("router_kind" , ["eager" , "lazy" , "registry" , "nested" ])
544- @pytest .mark .parametrize ("implemented" , [False , True ])
545- async def test_router_optional_advertisement_and_tenant_refusal (
546- router_kind : str , implemented : bool
547- ) -> None :
548- from concurrent .futures import ThreadPoolExecutor
549-
550- from adcp .decisioning import LazyPlatformRouter
551- from adcp .decisioning .capabilities import MediaBuy
552- from adcp .decisioning .handler import PlatformHandler
553- from adcp .decisioning .task_registry import InMemoryTaskRegistry
554- from adcp .server .tenant_registry import TenantRegistry
555-
556- accounts = _make_routing_account_store ({"acct_a" : "tenant-a" , "acct_b" : "tenant-b" })
557- caps = _capabilities (["sales-non-guaranteed" , "creative-template" ])
558- caps .media_buy = MediaBuy (lifecycle_tools = ["list_products" , "buy_products" ])
559- optional = {"list_creative_formats_legacy" , "preview_creative_legacy" , "list_products" }
560- declaration = optional if implemented else set ()
561- child = _OptionalRouterChild ("a" ) if implemented else _CanonicalRouterChild ("a" )
562- canonical = _CanonicalRouterChild ("b" )
563- constructed : list [str ] = []
564-
565- async def factory (tenant_id : str ) -> DecisioningPlatform :
566- constructed .append (tenant_id )
567- return child if tenant_id == "tenant-a" else canonical
568-
569- if router_kind == "eager" :
570- router = PlatformRouter (
571- accounts = accounts ,
572- platforms = {"tenant-a" : child , "tenant-b" : canonical },
573- capabilities = caps ,
574- )
575- elif router_kind in {"lazy" , "nested" }:
576- lazy = LazyPlatformRouter (
577- accounts = accounts , factory = factory , capabilities = caps , optional_methods = declaration
578- )
579- router = (
580- lazy
581- if router_kind == "lazy"
582- else PlatformRouter (
583- accounts = accounts , platforms = {"tenant-a" : lazy , "tenant-b" : lazy }, capabilities = caps
584- )
585- )
586- else :
587- registry = TenantRegistry (validator = None )
588- for tenant_id in ("tenant-a" , "tenant-b" ):
589- await registry .register_lazy (
590- tenant_id , agent_url = f"https://{ tenant_id } .example.com" , factory = factory
591- )
592- router = registry .as_platform (
593- accounts = accounts , capabilities = caps , optional_methods = declaration
594- )
595- with ThreadPoolExecutor () as executor :
596- handler = PlatformHandler (router , executor = executor , registry = InMemoryTaskRegistry ())
597- tools = handler .advertised_tools_for_instance ()
598- assert constructed == []
599- expected = {"list_creative_formats" , "preview_creative" , "list_products" }
600- assert tools & (expected | {"buy_products" }) == (expected if implemented else set ())
601- assert isinstance (router .optional_methods , frozenset )
602-
603- if implemented :
604- ctx = _make_ctx (Account (id = "acct_a" , metadata = {"tenant_id" : "tenant-a" }))
605- ctx .tenant_id = "tenant-a"
606- assert await router .list_creative_formats_legacy ({}, ctx ) == {"formats" : []}
607- ctx = _make_ctx (Account (id = "acct_b" , metadata = {"tenant_id" : "tenant-b" }))
608- ctx .tenant_id = "tenant-b"
609- with pytest .raises (AdcpError ) as caught :
610- await router .list_creative_formats_legacy ({}, ctx )
611- assert caught .value .code == "UNSUPPORTED_FEATURE"
612- assert "because at least one" not in str (caught .value )
613-
614-
615- def test_eager_optional_declaration_excludes_inherited_protocol_stubs () -> None :
616- router = PlatformRouter (
617- accounts = _make_routing_account_store ({"acct_a" : "tenant-a" }),
618- platforms = {"tenant-a" : _SyncSalesPlatform ("a" )},
619- capabilities = _capabilities (["sales-non-guaranteed" ]),
620- )
621- assert "list_creative_formats_legacy" not in router .optional_methods
0 commit comments