diff --git a/src/adcp/canonical_formats/v1_to_v2.py b/src/adcp/canonical_formats/v1_to_v2.py index c8e320515..45f975f5c 100644 --- a/src/adcp/canonical_formats/v1_to_v2.py +++ b/src/adcp/canonical_formats/v1_to_v2.py @@ -49,7 +49,7 @@ from dataclasses import dataclass, field from typing import Any -from pydantic import ValidationError +from pydantic import BaseModel, ValidationError from adcp.canonical_formats.advisory import _echo_identifier, make_sdk_advisory from adcp.canonical_formats.registry import ( @@ -173,10 +173,13 @@ def _v1_version_constraints( """Collect VAST/DAAST version values from a v1 format. The version may live at the top level (a flat catalog entry) or - inside per-asset requirements. Walks both. Returns an empty list + inside per-asset requirements. Walks both dictionary and typed-model + inputs, including root-model version lists. Returns an empty list when none are declared. """ out: list[str] = [] + if isinstance(v1_format, BaseModel): + v1_format = v1_format.model_dump(exclude_none=True) src = v1_format if isinstance(v1_format, dict) else None if src is None: return out diff --git a/tests/integration/test_a2a_context_id.py b/tests/integration/test_a2a_context_id.py index 83551b926..28c7fbca6 100644 --- a/tests/integration/test_a2a_context_id.py +++ b/tests/integration/test_a2a_context_id.py @@ -60,7 +60,7 @@ ) -class _EchoHandler(ADCPHandler): +class _ContextIdEchoHandler(ADCPHandler): """Minimal handler for the happy-path tests — the assertions are at the protocol layer, not the handler. Returns spec-compliant empty payloads so the client's strict response validator passes.""" @@ -195,7 +195,7 @@ async def test_two_calls_share_server_assigned_context_id(): assertion that calls[0] == calls[1] catches that regression.""" observer = _Observer() wire_messages: list[dict[str, Any]] = [] - async with _running_server(_EchoHandler(), observer) as base_url: + async with _running_server(_ContextIdEchoHandler(), observer) as base_url: config = AgentConfig( id="ctx-test-agent", agent_uri=base_url, @@ -237,7 +237,7 @@ async def test_reset_context_produces_new_server_side_session(): different server-side context than the one before. If reset were a no-op the two server-observed ids would match and this would fire.""" observer = _Observer() - async with _running_server(_EchoHandler(), observer) as base_url: + async with _running_server(_ContextIdEchoHandler(), observer) as base_url: config = AgentConfig( id="ctx-test-agent", agent_uri=base_url, @@ -262,7 +262,7 @@ async def test_seeded_context_id_reaches_server_on_first_call(): state lands on the same server-side session.""" seed = f"buyer-seeded-{uuid4()}" observer = _Observer() - async with _running_server(_EchoHandler(), observer) as base_url: + async with _running_server(_ContextIdEchoHandler(), observer) as base_url: config = AgentConfig( id="ctx-test-agent", agent_uri=base_url, @@ -505,7 +505,7 @@ async def test_resume_across_simulated_restart_lands_on_same_session(): with the persisted id, and its first call must carry that id so the server can reattach it to the original session.""" observer = _Observer() - async with _running_server(_EchoHandler(), observer) as base_url: + async with _running_server(_ContextIdEchoHandler(), observer) as base_url: config = AgentConfig( id="ctx-test-agent", agent_uri=base_url, diff --git a/tests/test_canonical_formats_v1_to_v2.py b/tests/test_canonical_formats_v1_to_v2.py index 7c2fd2a3f..bc57a930a 100644 --- a/tests/test_canonical_formats_v1_to_v2.py +++ b/tests/test_canonical_formats_v1_to_v2.py @@ -18,6 +18,7 @@ ) from adcp.canonical_formats.advisory import SDK_ID from adcp.types import CanonicalFormatKind +from adcp.types.legacy import LegacyFormat _FIXTURES = Path(__file__).parent / "fixtures" / "canonical" @@ -245,6 +246,64 @@ def test_retina_rendition_set_registry_mapping_preserves_slot_contract( # --------------------------------------------------------------------------- +@pytest.mark.parametrize( + ("asset_type", "version_key", "version", "expected_kind"), + [ + ("vast", "vast_version", "4.2", CanonicalFormatKind.video_vast), + ("vast", "vast_version", "3.0", CanonicalFormatKind.video_vast), + ("vast", "vast_version", "2.0", CanonicalFormatKind.video_vast), + ("daast", "daast_version", "1.0", CanonicalFormatKind.audio_daast), + ("daast", "daast_versions", ["1.0", "1.1"], CanonicalFormatKind.audio_daast), + ], +) +def test_typed_versioned_format_matches_dict_projection( + asset_type: str, + version_key: str, + version: str | list[str], + expected_kind: CanonicalFormatKind, +) -> None: + raw = { + "format_id": {"agent_url": "https://creative.example", "id": "acme_preroll"}, + "name": "Acme preroll", + "assets": [ + { + "item_type": "individual", + "asset_id": "media", + "asset_type": asset_type, + "required": True, + "requirements": {version_key: version}, + } + ], + } + typed = LegacyFormat.model_validate(raw) + from_dict = project_v1_format_to_declaration(raw) + from_model = project_v1_format_to_declaration(typed) + assert from_dict.declaration is not None + assert from_dict.declaration.format_kind == expected_kind + assert from_model == from_dict + + +@pytest.mark.parametrize("asset_type", ["vast", "daast"]) +def test_typed_versionless_format_still_fails_closed(asset_type: str) -> None: + raw = { + "format_id": {"agent_url": "https://creative.example", "id": "acme_preroll"}, + "name": "Acme preroll", + "assets": [ + { + "item_type": "individual", + "asset_id": "media", + "asset_type": asset_type, + "required": True, + "requirements": {}, + } + ], + } + from_dict = project_v1_format_to_declaration(raw) + from_model = project_v1_format_to_declaration(LegacyFormat.model_validate(raw)) + assert from_dict.declaration is None + assert from_model == from_dict + + def test_structural_match_emits_ambiguous_advisory() -> None: v1 = { "format_id": {"agent_url": "https://x.example", "id": "weird_vast_format"},