From 43402b04bd1914d10fe0d011f666791e2c68888f Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Tue, 21 Jul 2026 14:43:09 -0700 Subject: [PATCH] test(e2e): register the verbatim a2a-sdk 0.3.x card and guard malformed protocolVersion --- tests/e2e/a2a/a2a_client.py | 3 + tests/e2e/a2a/test_a2a_agent_e2e.py | 95 +++++++++++++++++++++----- tests/e2e/coverage_registry/other.yaml | 4 +- 3 files changed, 84 insertions(+), 18 deletions(-) diff --git a/tests/e2e/a2a/a2a_client.py b/tests/e2e/a2a/a2a_client.py index 5274ec15383..c1a1503ec96 100644 --- a/tests/e2e/a2a/a2a_client.py +++ b/tests/e2e/a2a/a2a_client.py @@ -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): diff --git a/tests/e2e/a2a/test_a2a_agent_e2e.py b/tests/e2e/a2a/test_a2a_agent_e2e.py index 823f6b9c001..8c19f97e64c 100644 --- a/tests/e2e/a2a/test_a2a_agent_e2e.py +++ b/tests/e2e/a2a/test_a2a_agent_e2e.py @@ -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}") diff --git a/tests/e2e/coverage_registry/other.yaml b/tests/e2e/coverage_registry/other.yaml index 63a626caab4..00a76cf511a 100644 --- a/tests/e2e/coverage_registry/other.yaml +++ b/tests/e2e/coverage_registry/other.yaml @@ -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)"}