-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathtest_decisioning_advertised_per_specialism.py
More file actions
335 lines (263 loc) · 11.5 KB
/
Copy pathtest_decisioning_advertised_per_specialism.py
File metadata and controls
335 lines (263 loc) · 11.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
"""Per-specialism advertised-tools filter (Emma cross-cutting P1).
Three Emma backend tests independently flagged the same bug: a sales-only
or signals-only adopter advertises all 40+ shims via ``tools/list``.
Buyers see ``acquire_rights``, ``build_creative``, ``check_governance``
on a sales-only seller; on every call they get NOT_SUPPORTED. The fix
hooks ``advertised_tools_for_instance()`` on :class:`PlatformHandler`,
which intersects the universe of shim coverage with the platform's
claimed specialisms via :data:`SPECIALISM_TO_ADVERTISED_TOOLS`.
This file pins the post-fix behavior so a future refactor can't
re-broaden the surface silently.
"""
from __future__ import annotations
from concurrent.futures import ThreadPoolExecutor
import pytest
from adcp.decisioning import (
DecisioningCapabilities,
DecisioningPlatform,
InMemoryTaskRegistry,
SingletonAccounts,
)
from adcp.decisioning.handler import (
SPECIALISM_TO_ADVERTISED_TOOLS,
PlatformHandler,
)
from adcp.server.mcp_tools import get_tools_for_handler
@pytest.fixture
def executor():
pool = ThreadPoolExecutor(max_workers=2, thread_name_prefix="per-spec-")
yield pool
pool.shutdown(wait=True)
# ---- specialism map drift guard ----
def test_specialism_map_keys_subset_of_spec_enum() -> None:
"""Every key in SPECIALISM_TO_ADVERTISED_TOOLS MUST be in the
canonical SPEC_SPECIALISM_ENUM. Drift here means the framework
advertises tools for a slug that isn't a real specialism."""
from adcp.decisioning.dispatch import SPEC_SPECIALISM_ENUM
extra = set(SPECIALISM_TO_ADVERTISED_TOOLS.keys()) - SPEC_SPECIALISM_ENUM
assert not extra, f"unknown specialism slugs in map: {sorted(extra)}"
def test_specialism_map_covers_every_protocol_family_slug() -> None:
"""Every spec slug that has a Protocol implementation in the
framework MUST appear in the map. Meta-claims (signed-requests,
governance-aware-seller) are documented exclusions — they compose
with another non-meta claim."""
from adcp.decisioning.dispatch import SPEC_SPECIALISM_ENUM
meta_claims = {"signed-requests", "governance-aware-seller"}
expected = SPEC_SPECIALISM_ENUM - meta_claims
missing = expected - set(SPECIALISM_TO_ADVERTISED_TOOLS.keys())
assert not missing, (
f"specialisms missing from map: {sorted(missing)}; "
"every Protocol-backed slug must declare its tool set"
)
# ---- per-instance filter ----
class _SalesOnlyPlatform(DecisioningPlatform):
capabilities = DecisioningCapabilities(specialisms=["sales-non-guaranteed"])
accounts = SingletonAccounts(account_id="sales-only")
def get_products(self, req, ctx):
return {"products": []}
def create_media_buy(self, req, ctx):
return {"media_buy_id": "x", "status": "active"}
def update_media_buy(self, media_buy_id, patch, ctx):
return {"media_buy_id": media_buy_id, "status": "active"}
def sync_creatives(self, req, ctx):
return {"creatives": []}
def get_media_buy_delivery(self, req, ctx):
return {"media_buy_deliveries": []}
class _SignalsOnlyPlatform(DecisioningPlatform):
capabilities = DecisioningCapabilities(specialisms=["signal-marketplace"])
accounts = SingletonAccounts(account_id="signals-only")
def get_signals(self, req, ctx):
return {"signals": []}
def activate_signal(self, req, ctx):
return {}
class _OwnedSignalsOnlyPlatform(DecisioningPlatform):
capabilities = DecisioningCapabilities(specialisms=["signal-owned"])
accounts = SingletonAccounts(account_id="owned-signals-only")
def get_signals(self, req, ctx):
return {"signals": []}
class _CreativeOnlyPlatform(DecisioningPlatform):
capabilities = DecisioningCapabilities(specialisms=["creative-generative"])
accounts = SingletonAccounts(account_id="creative-only")
def build_creative(self, req, ctx):
return {"creative_manifest": {"creative_id": "cr_1"}}
def test_sales_only_does_not_advertise_creative_or_signals_tools(executor) -> None:
"""Regression: sales-only adopter saw acquire_rights, build_creative,
check_governance, etc. in tools/list. After the per-specialism
filter, only sales tools advertise."""
handler = PlatformHandler(
_SalesOnlyPlatform(),
executor=executor,
registry=InMemoryTaskRegistry(),
)
tools = {tool["name"] for tool in get_tools_for_handler(handler)}
# Sales surface present.
assert "get_products" in tools
assert "create_media_buy" in tools
assert "sync_creatives" in tools
# Non-sales tools MUST NOT appear.
forbidden = {
"acquire_rights",
"build_creative",
"preview_creative",
"check_governance",
"sync_plans",
"get_signals",
"activate_signal",
"sync_audiences",
"list_content_standards",
"create_property_list",
"create_collection_list",
}
leaked = forbidden & tools
assert not leaked, (
f"sales-only adopter leaked non-sales tools to tools/list: " f"{sorted(leaked)}"
)
def test_signals_only_does_not_advertise_sales_tools(executor) -> None:
"""Mirror test for the signals path."""
handler = PlatformHandler(
_SignalsOnlyPlatform(),
executor=executor,
registry=InMemoryTaskRegistry(),
)
tools = {tool["name"] for tool in get_tools_for_handler(handler)}
assert "get_signals" in tools
assert "activate_signal" in tools
forbidden = {
"get_products",
"create_media_buy",
"build_creative",
"acquire_rights",
"check_governance",
}
leaked = forbidden & tools
assert not leaked, f"signals-only leaked: {sorted(leaked)}"
def test_owned_signals_only_advertises_discovery_not_activation(executor) -> None:
handler = PlatformHandler(
_OwnedSignalsOnlyPlatform(),
executor=executor,
registry=InMemoryTaskRegistry(),
)
tools = {tool["name"] for tool in get_tools_for_handler(handler)}
assert "get_signals" in tools
assert "activate_signal" not in tools
forbidden = {
"get_products",
"create_media_buy",
"build_creative",
"acquire_rights",
"check_governance",
}
leaked = forbidden & tools
assert not leaked, f"owned-signals-only leaked: {sorted(leaked)}"
def test_creative_only_does_not_advertise_sales_or_signals_tools(executor) -> None:
"""Mirror test for the creative path — AudioStack/Stability AI shape."""
handler = PlatformHandler(
_CreativeOnlyPlatform(),
executor=executor,
registry=InMemoryTaskRegistry(),
)
tools = {tool["name"] for tool in get_tools_for_handler(handler)}
assert "build_creative" in tools
forbidden = {
"get_products",
"create_media_buy",
"sync_creatives",
"get_signals",
"activate_signal",
"acquire_rights",
"check_governance",
}
leaked = forbidden & tools
assert not leaked, f"creative-only leaked: {sorted(leaked)}"
def test_multi_specialism_unions_both_surfaces(executor) -> None:
"""An adopter claiming both ``sales-non-guaranteed`` AND
``creative-generative`` advertises BOTH surfaces."""
class _HybridPlatform(DecisioningPlatform):
capabilities = DecisioningCapabilities(
specialisms=["sales-non-guaranteed", "creative-generative"]
)
accounts = SingletonAccounts(account_id="hybrid")
def get_products(self, req, ctx):
return {"products": []}
def create_media_buy(self, req, ctx):
return {"media_buy_id": "x", "status": "active"}
def update_media_buy(self, media_buy_id, patch, ctx):
return {"media_buy_id": media_buy_id, "status": "active"}
def sync_creatives(self, req, ctx):
return {"creatives": []}
def get_media_buy_delivery(self, req, ctx):
return {"media_buy_deliveries": []}
def build_creative(self, req, ctx):
return {"creative_manifest": {"creative_id": "cr_1"}}
handler = PlatformHandler(
_HybridPlatform(),
executor=executor,
registry=InMemoryTaskRegistry(),
)
tools = {tool["name"] for tool in get_tools_for_handler(handler)}
assert "get_products" in tools # sales
assert "build_creative" in tools # creative
# But no audience/signals/governance leaks.
forbidden = {"sync_audiences", "get_signals", "check_governance"}
leaked = forbidden & tools
assert not leaked, f"hybrid leaked: {sorted(leaked)}"
def test_novel_specialism_falls_back_to_class_level_advertisement(
executor,
) -> None:
"""Adopter piloting a novel slug (not in
SPECIALISM_TO_ADVERTISED_TOOLS) → empty per-instance set →
fall back to class-level union (preserve existing
``warnings.warn(novel)`` semantics from validate_platform).
Muting the handler entirely would be a worse foot-gun than
over-advertising."""
class _NovelPlatform(DecisioningPlatform):
# Bypass validate_platform's typo guard with a slug that's
# genuinely far from any spec slug.
capabilities = DecisioningCapabilities(specialisms=["xyzzy-experimental"])
accounts = SingletonAccounts(account_id="novel")
def get_products(self, req, ctx):
return {"products": []}
handler = PlatformHandler(
_NovelPlatform(),
executor=executor,
registry=InMemoryTaskRegistry(),
)
tools = {tool["name"] for tool in get_tools_for_handler(handler)}
# Override-detection still applies (only get_products implemented),
# so we get sales' overridden subset, but the universe includes all
# protocol families pre-filter — this is the documented
# forward-compat fallback.
assert "get_products" in tools
def test_advertise_all_bypasses_per_specialism_filter(executor) -> None:
"""Storyboard / spec-conformance test escape hatch — when caller
passes ``advertise_all=True``, every shim (regardless of claimed
specialism) is in the result."""
handler = PlatformHandler(
_SalesOnlyPlatform(),
executor=executor,
registry=InMemoryTaskRegistry(),
)
tools = {tool["name"] for tool in get_tools_for_handler(handler, advertise_all=True)}
# Sales-only stub still has only sales methods, but advertise_all
# bypasses the override filter — wait, advertise_all bypasses
# _is_method_overridden but the per-instance filter still trims.
# Verify: per-instance filter applies UNCONDITIONALLY (it represents
# what the platform's claimed specialisms cover; that's the same
# "did you sign up for this" semantic regardless of advertise_all).
assert "get_products" in tools
# build_creative is NOT in the universe-for-this-platform's
# specialisms, so it stays out.
assert "build_creative" not in tools
def test_class_level_inspection_preserves_full_universe() -> None:
"""When ``get_tools_for_handler`` is called with the class (not an
instance), we have no platform to read specialisms from. Falls back
to the class-level ``advertised_tools`` universe so static
introspection (storyboard tests, spec-conformance docs) keeps
seeing the full surface."""
tools = {tool["name"] for tool in get_tools_for_handler(PlatformHandler)}
# Static inspection sees ALL the shims because override-detection
# at the class level shows every shim as implemented (PlatformHandler
# itself defines them).
assert "get_products" in tools
assert "build_creative" in tools
assert "acquire_rights" in tools