Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions src/adcp/canonical_formats/v1_to_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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
Expand Down
10 changes: 5 additions & 5 deletions tests/integration/test_a2a_context_id.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
59 changes: 59 additions & 0 deletions tests/test_canonical_formats_v1_to_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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"},
Expand Down
Loading