test(e2e): register the verbatim a2a-sdk 0.3.x card and guard malformed protocolVersion

This commit is contained in:
mateo-berri 2026-07-21 14:43:09 -07:00
parent c8e0253cd5
commit 43402b04bd
3 changed files with 84 additions and 18 deletions

View file

@ -30,6 +30,7 @@ class A2ASkill(BaseModel):
name: str
description: str
tags: list[str]
examples: list[str] | None = None
class AgentCardParams(BaseModel):
@ -40,10 +41,12 @@ class AgentCardParams(BaseModel):
name: str
description: str
version: str
url: str | None = None
capabilities: A2ACapabilities = A2ACapabilities()
skills: list[A2ASkill]
default_input_modes: list[str] = Field(default=["text"], serialization_alias="defaultInputModes")
default_output_modes: list[str] = Field(default=["text"], serialization_alias="defaultOutputModes")
preferred_transport: str | None = Field(default=None, serialization_alias="preferredTransport")
class A2ABridgeParams(BaseModel):

View file

@ -14,6 +14,7 @@ import pytest
from a2a_client import (
A2ABridgeParams,
A2ACapabilities,
A2AClient,
A2AJsonRpcRequest,
A2AMessageSendParams,
@ -25,7 +26,7 @@ from a2a_client import (
AgentResponse,
)
from e2e_config import unique_marker
from e2e_http import UnknownApiError, unwrap
from e2e_http import Result, UnknownApiError, unwrap
from lifecycle import ResourceManager
BRIDGE = A2ABridgeParams(custom_llm_provider="anthropic", model="claude-haiku-4-5")
@ -51,6 +52,50 @@ def _register(client: A2AClient, resources: ResourceManager, protocol_version: s
return agent
def _google_sdk_default_card(marker: str) -> AgentCardParams:
"""Field-for-field the card the Google a2a-sdk 0.3.x line serves for its helloworld
sample, whose ``AgentCard`` defaults ``protocolVersion`` to the full semver "0.3.0"
(the shape the v1.92 regression rejected); ``url`` and ``name`` are the
deployment-specific fields the SDK requires callers to fill. The url points at a
reserved example host: a url-bearing card makes message/send dial that upstream
rather than the completion bridge, so the verbatim-card test asserts registration
and card serving only."""
return AgentCardParams(
protocol_version="0.3.0",
name=f"E2E A2A sdk {marker}",
description="Just a hello world agent",
version="1.0.0",
url="http://e2e-a2a-upstream.example/",
capabilities=A2ACapabilities(streaming=True),
skills=[
A2ASkill(
id="hello_world",
name="Returns hello world",
description="just returns hello world",
tags=["hello world"],
examples=["hi", "hello world"],
)
],
preferred_transport="JSONRPC",
)
def _register_rejection(client: A2AClient, protocol_version: str) -> Result[AgentResponse]:
marker = unique_marker()
body = AgentRegisterBody(
agent_name=f"e2e-a2a-bad-{marker}",
agent_card_params=AgentCardParams(
protocol_version=protocol_version,
name=f"E2E A2A bad {marker}",
description="rejected at registration",
version="1.0.0",
skills=[A2ASkill(id="chat", name="Chat", description="c", tags=["chat"])],
),
litellm_params=BRIDGE,
)
return client.register_agent(body)
def _ask(text: str) -> A2AJsonRpcRequest:
return A2AJsonRpcRequest(
id=f"e2e-{unique_marker()}",
@ -72,13 +117,31 @@ class TestA2AAgentLifecycle:
@pytest.mark.covers("other.a2a.register.semver_version_accepted")
def test_semver_protocol_version_registers_and_serves(self, client: A2AClient, resources: ResourceManager, scoped_key: str) -> None:
agent = _register(client, resources, "0.3.0")
assert agent.agent_card_params.protocol_version.startswith("0.3")
assert agent.agent_card_params.protocol_version == "0.3"
card = unwrap(client.agent_card(agent.agent_id, scoped_key))
assert card.protocol_version.startswith("0.3")
assert card.protocol_version == "0.3"
assert card.supported_interfaces is not None
assert card.supported_interfaces[0].protocol_version == "0.3"
result = unwrap(client.send_message(agent.agent_id, scoped_key, _ask("Say hi in one word"))).result
assert result is not None
assert result.text != ""
@pytest.mark.covers("other.a2a.register.sdk_default_card_accepted")
def test_google_sdk_default_card_registers_verbatim(self, client: A2AClient, resources: ResourceManager, scoped_key: str) -> None:
marker = unique_marker()
body = AgentRegisterBody(
agent_name=f"e2e-a2a-sdk-{marker}",
agent_card_params=_google_sdk_default_card(marker),
litellm_params=BRIDGE,
)
agent = unwrap(client.register_agent(body))
resources.defer(lambda: client.delete_agent(agent.agent_id))
assert agent.agent_card_params.protocol_version == "0.3"
card = unwrap(client.agent_card(agent.agent_id, scoped_key))
assert card.protocol_version == "0.3"
assert card.supported_interfaces is not None
assert card.supported_interfaces[0].protocol_version == "0.3"
@pytest.mark.covers("other.a2a.discovery.proxy_fronted_card")
def test_discovery_card_is_proxy_fronted(self, client: A2AClient, resources: ResourceManager, scoped_key: str) -> None:
agent = _register(client, resources, "0.3")
@ -127,23 +190,21 @@ class TestA2AAgentLifecycle:
assert result.text != ""
@pytest.mark.covers("other.a2a.register.unsupported_version_rejected")
def test_unsupported_protocol_version_rejected(self, client: A2AClient, resources: ResourceManager) -> None:
marker = unique_marker()
body = AgentRegisterBody(
agent_name=f"e2e-a2a-bad-{marker}",
agent_card_params=AgentCardParams(
protocol_version="9.9",
name=f"E2E A2A bad {marker}",
description="unsupported version",
version="1.0.0",
skills=[A2ASkill(id="chat", name="Chat", description="c", tags=["chat"])],
),
litellm_params=BRIDGE,
)
result = client.register_agent(body)
def test_unsupported_protocol_version_rejected(self, client: A2AClient) -> None:
result = _register_rejection(client, "9.9")
match result:
case UnknownApiError(status_code=status, body=detail):
assert status == 400
assert "protocolVersion" in detail
case _:
pytest.fail(f"expected 400 for unsupported protocolVersion, got {result}")
@pytest.mark.covers("other.a2a.register.malformed_version_rejected")
def test_malformed_protocol_version_rejected(self, client: A2AClient) -> None:
result = _register_rejection(client, "0.3.garbage")
match result:
case UnknownApiError(status_code=status, body=detail):
assert status == 400
assert "Unsupported protocolVersion '0.3.garbage'" in detail
case _:
pytest.fail(f"expected 400 for malformed protocolVersion, got {result}")

View file

@ -30,7 +30,9 @@
- {id: other.key_mgmt.spend_reset.resets_to_value, module: other, tier: P1, area: auth, assertions: [resets_to_value], source: "key_management_endpoints.py:4841", rationale: "reset_spend resets accumulated spend"}
- {id: other.a2a.register.persists, module: other, tier: P1, area: a2a, assertions: [persists], source: "agent_endpoints/endpoints.py:325-443", rationale: "POST /v1/agents registers an agent card; GET /v1/agents/{id} reads it back"}
- {id: other.a2a.register.unsupported_version_rejected, module: other, tier: P1, area: a2a, assertions: [unsupported_version_rejected], source: "agent_endpoints/endpoints.py _validate_protocol_version", rationale: "A card pinning a protocolVersion outside SUPPORTED_A2A_PROTOCOL_VERSIONS is refused with 400"}
- {id: other.a2a.register.semver_version_accepted, module: other, tier: P1, area: a2a, assertions: [semver_version_accepted], source: "agent_endpoints/endpoints.py _validate_protocol_version", rationale: "A card pinning a patch-level semver like 0.3.0 (what the Google A2A SDK emits) registers and serves as the 0.3 family rather than 400ing; regression guard for the v1.92 report"}
- {id: other.a2a.register.semver_version_accepted, module: other, tier: P1, area: a2a, assertions: [semver_version_accepted], source: "agent_endpoints/endpoints.py _validate_protocol_version", rationale: "A card pinning a patch-level semver like 0.3.0 (what the Google A2A SDK emits) registers, stores and serves the canonical 0.3 rather than 400ing; regression guard for the v1.92 report"}
- {id: other.a2a.register.sdk_default_card_accepted, module: other, tier: P1, area: a2a, assertions: [sdk_default_card_accepted], source: "agent_endpoints/endpoints.py _build_merged_agent_card", rationale: "The full field set the Google a2a-sdk 0.3.x emits (protocolVersion 0.3.0, url, preferredTransport, capabilities, skill examples) registers verbatim and serves the canonical 0.3"}
- {id: other.a2a.register.malformed_version_rejected, module: other, tier: P1, area: a2a, assertions: [malformed_version_rejected], source: "a2a/agent_card.py normalize_protocol_version", rationale: "A malformed protocolVersion like 0.3.garbage fails full-string semver validation and is refused with 400 instead of truncating to a supported family"}
- {id: other.a2a.discovery.proxy_fronted_card, module: other, tier: P1, area: a2a, assertions: [proxy_fronted_card], source: "agent_endpoints/a2a_endpoints.py get_agent_card", rationale: "/.well-known/agent-card.json serves the proxy url + supportedInterfaces and the LiteLLM virtual-key bearer scheme, not the upstream"}
- {id: other.a2a.message_send.bridge_invokes, module: other, tier: P1, area: a2a, assertions: [bridge_invokes], source: "a2a_protocol/litellm_completion_bridge/handler.py", rationale: "A2A message/send routes through the completion bridge to a real provider and logs an asend_message spend row"}
- {id: other.a2a.version.serves_pinned_0_3, module: other, tier: P1, area: a2a, assertions: [serves_pinned_0_3], source: "agent_endpoints/a2a_endpoints.py _served_version", rationale: "An agent pinning 0.3 returns the flat 0.3 message shape (parts on the result)"}