|
17 | 17 | PreviewCreativeResponse, |
18 | 18 | get_required_assets, |
19 | 19 | ) |
| 20 | +from adcp.types import GetAdcpCapabilitiesResponse |
20 | 21 |
|
21 | 22 | from creative_agent import server |
22 | 23 | from creative_agent.data.standard_formats import AGENT_URL |
|
25 | 26 | # Get actual functions from FastMCP wrappers |
26 | 27 | list_creative_formats = server.list_creative_formats.fn |
27 | 28 | preview_creative = server.preview_creative.fn |
| 29 | +get_adcp_capabilities = server.get_adcp_capabilities.fn |
28 | 30 |
|
29 | 31 |
|
30 | 32 | class TestListCreativeFormatsResponseFormat: |
@@ -134,6 +136,26 @@ def test_assets_have_asset_id(self): |
134 | 136 | assert "asset_id" in asset_dict, f"Format {fmt.format_id.id} has asset without asset_id: {asset_dict}" |
135 | 137 | assert asset_dict["asset_id"], f"Format {fmt.format_id.id} has empty asset_id: {asset_dict}" |
136 | 138 |
|
| 139 | + def test_backward_compat_assets_required_field(self): |
| 140 | + """For 2.5.x client compatibility, formats must include assets_required field.""" |
| 141 | + result = list_creative_formats() |
| 142 | + result_dict = result.structured_content |
| 143 | + |
| 144 | + # Find a format that has assets |
| 145 | + formats_with_assets = [f for f in result_dict["formats"] if f.get("assets")] |
| 146 | + assert len(formats_with_assets) > 0, "Should have formats with assets" |
| 147 | + |
| 148 | + for fmt in formats_with_assets: |
| 149 | + # assets_required must be present for backward compatibility |
| 150 | + assert "assets_required" in fmt, ( |
| 151 | + f"Format {fmt.get('format_id', {}).get('id')} missing assets_required for 2.5.x compatibility" |
| 152 | + ) |
| 153 | + # assets_required should only contain assets where required=True |
| 154 | + for asset in fmt["assets_required"]: |
| 155 | + assert asset.get("required", False) is True, ( |
| 156 | + f"assets_required should only contain required assets, got: {asset}" |
| 157 | + ) |
| 158 | + |
137 | 159 | def test_accepts_format_ids_as_dicts(self): |
138 | 160 | """Test that list_creative_formats accepts format_ids as FormatId objects (dicts).""" |
139 | 161 | # Filter by format_ids using dict representation |
@@ -333,3 +355,119 @@ def test_structured_content_not_double_encoded(self, mocker): |
333 | 355 | pytest.fail(f"Found double-encoded JSON in field '{key}': {value[:100]}") |
334 | 356 | except json.JSONDecodeError: |
335 | 357 | pass |
| 358 | + |
| 359 | + |
| 360 | +class TestGetAdcpCapabilitiesResponseFormat: |
| 361 | + """Test that get_adcp_capabilities returns valid ADCP GetAdcpCapabilitiesResponse. |
| 362 | +
|
| 363 | + Written by reading the ADCP spec - GetAdcpCapabilitiesResponse schema. |
| 364 | + NOT by looking at server.py code. |
| 365 | +
|
| 366 | + Required fields per spec: |
| 367 | + - adcp: object with major_versions array |
| 368 | + - supported_protocols: array of protocol names (media_buy, signals, etc.) |
| 369 | + """ |
| 370 | + |
| 371 | + def test_returns_tool_result_with_structured_content(self): |
| 372 | + """Tool must return ToolResult with structured_content.""" |
| 373 | + result = get_adcp_capabilities() |
| 374 | + |
| 375 | + # Verify ToolResult structure |
| 376 | + assert hasattr(result, "content"), "Must return ToolResult with content" |
| 377 | + assert hasattr(result, "structured_content"), "Must return ToolResult with structured_content" |
| 378 | + assert result.content, "Content must not be empty" |
| 379 | + assert result.structured_content, "Structured content must not be empty" |
| 380 | + |
| 381 | + # Verify content is human-readable message |
| 382 | + assert result.content[0].type == "text" |
| 383 | + assert "capabilit" in result.content[0].text.lower(), "Content should mention capabilities" |
| 384 | + |
| 385 | + def test_structured_content_matches_adcp_schema(self): |
| 386 | + """Structured content must validate against GetAdcpCapabilitiesResponse schema.""" |
| 387 | + result = get_adcp_capabilities() |
| 388 | + |
| 389 | + # Get structured_content (already a dict, no JSON parsing needed) |
| 390 | + result_dict = result.structured_content |
| 391 | + |
| 392 | + # This validates ALL fields, types, constraints per ADCP spec |
| 393 | + response = GetAdcpCapabilitiesResponse.model_validate(result_dict) |
| 394 | + |
| 395 | + # Verify required fields per spec |
| 396 | + assert response.adcp is not None, "'adcp' field is required per ADCP spec" |
| 397 | + assert response.supported_protocols is not None, "'supported_protocols' field is required per ADCP spec" |
| 398 | + |
| 399 | + def test_adcp_field_structure(self): |
| 400 | + """Per spec, adcp must have major_versions array with at least one version.""" |
| 401 | + result = get_adcp_capabilities() |
| 402 | + response = GetAdcpCapabilitiesResponse.model_validate(result.structured_content) |
| 403 | + |
| 404 | + assert response.adcp is not None, "adcp is required" |
| 405 | + assert response.adcp.major_versions is not None, "adcp.major_versions is required" |
| 406 | + assert isinstance(response.adcp.major_versions, list), "major_versions must be array" |
| 407 | + assert len(response.adcp.major_versions) >= 1, "must have at least one major version" |
| 408 | + # Version must be positive integer (may be wrapped in MajorVersion type) |
| 409 | + for version in response.adcp.major_versions: |
| 410 | + # Handle MajorVersion wrapper type that has .root attribute |
| 411 | + version_int = version.root if hasattr(version, "root") else version |
| 412 | + assert isinstance(version_int, int), f"major_version must be integer, got {type(version_int)}" |
| 413 | + assert version_int >= 1, "major_version must be >= 1" |
| 414 | + |
| 415 | + def test_supported_protocols_structure(self): |
| 416 | + """Per spec, supported_protocols must be array of valid protocol names.""" |
| 417 | + result = get_adcp_capabilities() |
| 418 | + response = GetAdcpCapabilitiesResponse.model_validate(result.structured_content) |
| 419 | + |
| 420 | + assert isinstance(response.supported_protocols, list), "supported_protocols must be array" |
| 421 | + assert len(response.supported_protocols) >= 1, "must support at least one protocol" |
| 422 | + |
| 423 | + # Valid protocols per ADCP spec |
| 424 | + valid_protocols = {"media_buy", "signals", "governance", "sponsored_intelligence", "creative"} |
| 425 | + for protocol in response.supported_protocols: |
| 426 | + # Handle both string and enum |
| 427 | + protocol_str = protocol.value if hasattr(protocol, "value") else str(protocol) |
| 428 | + assert protocol_str in valid_protocols, f"Invalid protocol: {protocol_str}" |
| 429 | + |
| 430 | + def test_creative_agent_supports_creative_protocol(self): |
| 431 | + """A creative agent must support the creative protocol.""" |
| 432 | + result = get_adcp_capabilities() |
| 433 | + response = GetAdcpCapabilitiesResponse.model_validate(result.structured_content) |
| 434 | + |
| 435 | + protocol_strs = [p.value if hasattr(p, "value") else str(p) for p in response.supported_protocols] |
| 436 | + assert "creative" in protocol_strs, "Creative agent must support creative protocol" |
| 437 | + |
| 438 | + def test_no_extra_wrapper_fields(self): |
| 439 | + """Structured content must match ADCP schema exactly with no wrappers.""" |
| 440 | + result = get_adcp_capabilities() |
| 441 | + result_dict = result.structured_content |
| 442 | + |
| 443 | + # These are common bugs - wrapping valid response in extra structure |
| 444 | + assert "result" not in result_dict or not isinstance(result_dict.get("result"), str), ( |
| 445 | + "structured_content must not have JSON string in 'result' field" |
| 446 | + ) |
| 447 | + assert "data" not in result_dict or result_dict.get("data") != result_dict, ( |
| 448 | + "structured_content must not be wrapped in 'data' field" |
| 449 | + ) |
| 450 | + |
| 451 | + # Top-level keys should include required schema keys |
| 452 | + required_keys = {"adcp", "supported_protocols"} |
| 453 | + actual_keys = set(result_dict.keys()) |
| 454 | + assert required_keys.issubset(actual_keys), ( |
| 455 | + f"Response must have required keys {required_keys}, got {actual_keys}" |
| 456 | + ) |
| 457 | + |
| 458 | + def test_protocols_filter_works(self): |
| 459 | + """If protocols param is provided, should filter to those protocols.""" |
| 460 | + result = get_adcp_capabilities(protocols=["creative"]) |
| 461 | + |
| 462 | + response = GetAdcpCapabilitiesResponse.model_validate(result.structured_content) |
| 463 | + # Should still have required fields |
| 464 | + assert response.adcp is not None |
| 465 | + assert response.supported_protocols is not None |
| 466 | + |
| 467 | + def test_protocols_filter_with_unsupported_protocol_returns_error(self): |
| 468 | + """If protocols param contains only unsupported protocols, returns error.""" |
| 469 | + result = get_adcp_capabilities(protocols=["media_buy"]) # This agent only supports creative |
| 470 | + |
| 471 | + # ADCP schema requires at least one protocol, so filtering to unsupported |
| 472 | + # protocols results in a validation error |
| 473 | + assert "error" in result.structured_content |
0 commit comments