litellm/tests/integration/compatibility/test_a2a_protocol_version_override.py
yuneng-jiang b602209ca6
test(integration): hand every harness server a port from one per-worker block (#46761)
* test(integration): hand every harness server a port from one per-worker block

About 45 places in the integration harness picked a port by binding port 0,
reading the number back, closing the socket and starting a server on that
number later. Another process could take the port in between, which surfaced
as "Address already in use" flakes. Earlier fixes retried around the race in
single call sites without removing it.

tests/integration/_support/ports.py now owns every port the harness picks
ahead of a server. Each pytest process claims a 200-port block between 10000
and 32767 by taking an exclusive flock on a per-block lock file in the system
temp directory, so parallel runs on one machine never share a block and the
lock goes away with the process. The range is above the fixed service ports
and below the Linux and macOS ephemeral ranges. Ports go out in order, are
never reused in a run, skip anything an outside process already holds, and
a process that exhausts its block claims the next free one.

The three _free_port copies, worker_endpoints' port-0 binds,
free_loopback_port, the owned proxy's lost-port-race retry and the owned
Redis retry loop are gone. Tests that restart a server on the same port now
allocate that port up front instead of reading it back from the first
server. TQ012 in scripts/check_test_quality.py rejects a port-0 bind read
back with getsockname anywhere under tests/integration.

* ci(integration): record listening sockets and the ephemeral port range

* Revert "ci(integration): record listening sockets and the ephemeral port range"

This reverts commit d3fafc3da2.
2026-10-15 16:56:40 -07:00

298 lines
16 KiB
Python

"""Per-agent ``litellm_params.a2a_protocol_version`` pins the A2A transport the proxy speaks to the agent.
langgraph-api 0.15 serves cards declaring ``JSONRPC``/``1.0`` while answering in the 0.3 ``kind`` dialect, so
a2a-sdk picks its strict v1 transport and rejects every reply. Pinning "0.3" routes the SDK onto its v0.3
compat transport; pinning "1.0" keeps v1 even where the mis-cased-binding fingerprint would downgrade.
Every cell asserts the caller's response, the exact request the scripted agent received, and Postgres.
"""
from __future__ import annotations
import json
import os
import time
from typing import Final
import pytest
from integration._support.client import Gateway, eventually
from integration._support.database import read_rows
from integration._support.ports import allocate_port
from integration.compatibility._a2a_peers import (
HINT,
KIND_ERROR,
legacy_send,
peer,
posts,
register,
spend_rows,
sse_frames,
)
from pydantic import JsonValue
from litellm.constants import PROXY_CONFIG_RELOAD_INTERVAL_SECONDS
PROXY_WORKERS: Final = int(os.environ.get("INTEGRATION_PROXY_WORKERS", "1"))
WORKER_SYNC_SECONDS: Final = 0.0 if PROXY_WORKERS == 1 else PROXY_CONFIG_RELOAD_INTERVAL_SECONDS + 5.0
FRESH_CONNECTION: Final = {"Connection": "close"}
PINNED: Final = {"a2a_protocol_version": "0.3"}
ROUTES: Final = ("/a2a/{id}", "/a2a/{id}/message/send", "/v1/a2a/{id}/message/send")
def _artifact_text(result: dict[str, JsonValue]) -> str:
task: Final = result.get("task", result)
assert isinstance(task, dict), result
artifacts: Final = task["artifacts"]
assert isinstance(artifacts, list) and len(artifacts) == 1, result
parts: Final = artifacts[0]["parts"] # type: ignore[index]
return str(parts[0]["text"]) # type: ignore[index]
def _assert_legacy_wire(sent: tuple[dict[str, JsonValue], ...], marker: str, method: str = "message/send") -> None:
assert len(sent) == 1, sent
assert sent[0]["method"] == method, sent
message: Final = sent[0]["params"]["message"] # type: ignore[index]
assert message["kind"] == "message" and message["role"] == "user", sent
assert message["parts"] == [{"kind": "text", "text": "synthetic ping"}], sent
assert message["messageId"] == marker + "-in", sent
def _assert_v1_wire(sent: tuple[dict[str, JsonValue], ...], marker: str, method: str = "SendMessage") -> None:
assert len(sent) == 1, sent
assert sent[0]["method"] == method, sent
message: Final = sent[0]["params"]["message"] # type: ignore[index]
assert "kind" not in message and message["role"] == "ROLE_USER", sent
assert message["parts"] == [{"text": "synthetic ping"}], sent
assert message["messageId"] == marker + "-in", sent
def _one_spend_row(request_id: str, call_type: str, status: str) -> None:
rows: Final = eventually(lambda: spend_rows(request_id), lambda values: len(values) == 1, seconds=70)
assert rows[0]["call_type"] == call_type and rows[0]["status"] == status, rows
def _send_ok(gateway: Gateway, identity: str, marker: str, path: str = "/a2a/{id}") -> dict[str, JsonValue]:
response: Final = gateway.request("POST", path.format(id=identity), legacy_send(marker))
assert response.status_code == 200, response.text
body: Final = response.json()
assert body["id"] == marker and "error" not in body, response.text
assert _artifact_text(body["result"]) == "echo: synthetic ping", response.text
return body
def _send_kind_error(gateway: Gateway, identity: str, marker: str) -> str:
response: Final = gateway.request("POST", f"/a2a/{identity}", legacy_send(marker))
assert response.status_code == 500, response.text
message: Final = str(response.json()["error"]["message"])
assert KIND_ERROR in message, response.text
return message
@pytest.mark.parametrize("route", ROUTES)
def test_pinned_langgraph_agent_answers_message_send_in_the_legacy_dialect(gateway: Gateway, route: str) -> None:
with peer("lgsend", "langgraph") as agent, gateway.scenario() as scenario:
identity: Final = register(gateway, scenario.cleanups.callback, agent.marker, agent.url, PINNED)
_send_ok(gateway, identity, agent.marker, route)
_assert_legacy_wire(posts(agent.wire), agent.marker) # type: ignore[arg-type]
_one_spend_row(agent.marker, "asend_message", "success")
def test_pinned_langgraph_agent_streams_message_stream_to_the_final_frame(gateway: Gateway) -> None:
with peer("lgstream", "langgraph") as agent, gateway.scenario() as scenario:
identity: Final = register(gateway, scenario.cleanups.callback, agent.marker, agent.url, PINNED)
response: Final = gateway.request(
"POST", f"/a2a/{identity}", legacy_send(agent.marker, method="message/stream")
)
assert response.status_code == 200, response.text
assert response.headers["content-type"].startswith("text/event-stream"), response.headers
frames: Final = sse_frames(response.text)
assert all(frame["id"] == agent.marker and "error" not in frame for frame in frames), response.text
results: Final = [frame["result"] for frame in frames]
assert len(results) == 3, response.text
assert results[0]["statusUpdate"]["status"]["state"] == "TASK_STATE_WORKING", response.text # type: ignore[index]
assert results[0]["statusUpdate"]["status"]["message"]["parts"] == [{"text": "echo: synthetic ping"}] # type: ignore[index]
assert results[1]["artifactUpdate"]["artifact"]["parts"] == [{"text": "echo: synthetic ping"}] # type: ignore[index]
assert results[2]["statusUpdate"]["status"]["state"] == "TASK_STATE_COMPLETED", response.text # type: ignore[index]
_assert_legacy_wire(posts(agent.wire), agent.marker, "message/stream") # type: ignore[arg-type]
_one_spend_row(agent.marker, "asend_message_streaming", "success")
def test_v1_caller_reaches_pinned_legacy_agent_in_its_own_envelope(gateway: Gateway) -> None:
with peer("lgv1caller", "langgraph") as agent, gateway.scenario() as scenario:
identity: Final = register(gateway, scenario.cleanups.callback, agent.marker, agent.url, PINNED)
response: Final = gateway.client.post(
f"/a2a/{identity}",
headers={"Authorization": f"Bearer {gateway.key}", "a2a-version": "1.0"},
json={
"jsonrpc": "2.0",
"id": agent.marker,
"method": "SendMessage",
"params": {
"message": {
"role": "ROLE_USER",
"messageId": agent.marker + "-in",
"parts": [{"text": "synthetic ping"}],
}
},
},
)
assert response.status_code == 200, response.text
body: Final = response.json()
assert body["id"] == agent.marker and "error" not in body, response.text
task: Final = body["result"]["task"]
assert "kind" not in task and task["status"]["state"] == "TASK_STATE_COMPLETED", response.text
assert task["artifacts"][0]["parts"] == [{"text": "echo: synthetic ping"}], response.text
_assert_legacy_wire(posts(agent.wire), agent.marker) # type: ignore[arg-type]
_one_spend_row(agent.marker, "asend_message", "success")
@pytest.mark.parametrize("value", ("0.3.0", 0.3, " 0.3 "), ids=("dotted", "number", "spaced"))
def test_protocol_version_aliases_pin_the_legacy_transport(gateway: Gateway, value: JsonValue) -> None:
with peer("lgalias", "langgraph") as agent, gateway.scenario() as scenario:
identity: Final = register(
gateway, scenario.cleanups.callback, agent.marker, agent.url, {"a2a_protocol_version": value}
)
_send_ok(gateway, identity, agent.marker)
_assert_legacy_wire(posts(agent.wire), agent.marker) # type: ignore[arg-type]
_one_spend_row(agent.marker, "asend_message", "success")
def test_genuine_v1_agent_pinned_one_point_zero_is_unchanged(gateway: Gateway) -> None:
with peer("v1pinned", "v1") as agent, gateway.scenario() as scenario:
identity: Final = register(
gateway, scenario.cleanups.callback, agent.marker, agent.url, {"a2a_protocol_version": "1.0"}
)
_send_ok(gateway, identity, agent.marker)
_assert_v1_wire(posts(agent.wire), agent.marker) # type: ignore[arg-type]
_one_spend_row(agent.marker, "asend_message", "success")
def test_genuine_v1_agent_unpinned_is_unchanged(gateway: Gateway) -> None:
with peer("v1plain", "v1") as agent, gateway.scenario() as scenario:
identity: Final = register(gateway, scenario.cleanups.callback, agent.marker, agent.url, None)
_send_ok(gateway, identity, agent.marker)
_assert_v1_wire(posts(agent.wire), agent.marker) # type: ignore[arg-type]
_one_spend_row(agent.marker, "asend_message", "success")
@pytest.mark.parametrize("pin", (None, "0.3"), ids=("unpinned", "pinned-legacy"))
def test_lowercase_binding_still_downgrades_to_the_legacy_transport(gateway: Gateway, pin: str | None) -> None:
with (
peer("lglower", "langgraph", interfaces=[{"protocolBinding": "jsonrpc"}]) as agent,
gateway.scenario() as scenario,
):
params: Final = None if pin is None else {"a2a_protocol_version": pin}
identity: Final = register(gateway, scenario.cleanups.callback, agent.marker, agent.url, params)
_send_ok(gateway, identity, agent.marker)
_assert_legacy_wire(posts(agent.wire), agent.marker) # type: ignore[arg-type]
_one_spend_row(agent.marker, "asend_message", "success")
def test_explicit_one_point_zero_opts_out_of_the_lowercase_downgrade(gateway: Gateway) -> None:
with peer("v1lower", "v1", interfaces=[{"protocolBinding": "jsonrpc"}]) as agent, gateway.scenario() as scenario:
identity: Final = register(
gateway, scenario.cleanups.callback, agent.marker, agent.url, {"a2a_protocol_version": "1.0"}
)
_send_ok(gateway, identity, agent.marker)
_assert_v1_wire(posts(agent.wire), agent.marker) # type: ignore[arg-type]
_one_spend_row(agent.marker, "asend_message", "success")
def test_one_point_zero_pin_on_a_legacy_card_sends_v1(gateway: Gateway) -> None:
legacy_card: Final = [{"protocolBinding": "JSONRPC", "protocolVersion": "0.3"}]
with peer("v1oncard03", "v1", interfaces=legacy_card) as agent, gateway.scenario() as scenario:
identity: Final = register(
gateway, scenario.cleanups.callback, agent.marker, agent.url, {"a2a_protocol_version": "1.0"}
)
_send_ok(gateway, identity, agent.marker)
_assert_v1_wire(posts(agent.wire), agent.marker) # type: ignore[arg-type]
_one_spend_row(agent.marker, "asend_message", "success")
def test_override_rewrites_only_canonical_interfaces(gateway: Gateway) -> None:
with peer("lgmulti", "langgraph") as agent, peer("lgdecoy", "langgraph") as decoy, gateway.scenario() as scenario:
agent.interfaces = [
{"protocolBinding": "JSONRPC", "url": agent.url},
{"protocolBinding": "custom-ws", "url": decoy.url},
{"protocolBinding": "HTTP+JSON", "url": decoy.url},
]
identity: Final = register(gateway, scenario.cleanups.callback, agent.marker, agent.url, PINNED)
_send_ok(gateway, identity, agent.marker)
_assert_legacy_wire(posts(agent.wire), agent.marker) # type: ignore[arg-type]
assert decoy.wire.drain() == (), "the HTTP+JSON and custom bindings must not be selected" # type: ignore[union-attr]
_one_spend_row(agent.marker, "asend_message", "success")
def test_override_added_and_removed_by_patch_takes_effect_on_both_workers(gateway: Gateway) -> None:
with peer("lgpatch", "langgraph") as agent, gateway.scenario() as scenario:
identity: Final = register(gateway, scenario.cleanups.callback, agent.marker, agent.url, None)
assert HINT in _send_kind_error(gateway, identity, agent.marker + "-before")
before: Final = posts(agent.wire) # type: ignore[arg-type]
assert [item["method"] for item in before] == ["SendMessage"], before
def send(suffix: str) -> int:
response: Final = gateway.request(
"POST", f"/a2a/{identity}", legacy_send(agent.marker + suffix), headers=FRESH_CONNECTION
)
return response.status_code
def settle(params: dict[str, JsonValue], expected: int) -> None:
patched: Final = gateway.request("PATCH", f"/v1/agents/{identity}", {"litellm_params": params})
assert patched.status_code == 200, patched.text
assert patched.json()["litellm_params"].get("a2a_protocol_version") == params.get("a2a_protocol_version")
written_at: Final = time.monotonic()
counter = iter(range(10_000))
eventually(
lambda: (time.monotonic() - written_at, tuple(send(f"-{next(counter)}") for _ in range(4))),
lambda stamped: stamped[0] >= WORKER_SYNC_SECONDS and set(stamped[1]) == {expected},
seconds=WORKER_SYNC_SECONDS + 30,
)
agent.wire.drain() # type: ignore[union-attr]
settle(dict(PINNED), 200)
response: Final = gateway.request("POST", f"/a2a/{identity}", legacy_send(agent.marker))
assert response.status_code == 200, response.text
_assert_legacy_wire(posts(agent.wire), agent.marker) # type: ignore[arg-type]
settle({}, 500)
assert HINT in _send_kind_error(gateway, identity, agent.marker + "-after")
def test_repeated_pinned_sends_log_one_row_each(gateway: Gateway) -> None:
with peer("lgrepeat", "langgraph") as agent, gateway.scenario() as scenario:
identity: Final = register(gateway, scenario.cleanups.callback, agent.marker, agent.url, PINNED)
markers: Final = tuple(f"{agent.marker}-{index}" for index in range(10))
for marker in markers:
response = gateway.request("POST", f"/a2a/{identity}", legacy_send(marker))
assert response.status_code == 200 and response.json()["id"] == marker, response.text
sent: Final = posts(agent.wire) # type: ignore[arg-type]
assert [item["params"]["message"]["messageId"] for item in sent] == [m + "-in" for m in markers], sent # type: ignore[index]
assert len({item["id"] for item in sent}) == len(markers), sent
assert {item["method"] for item in sent} == {"message/send"}, sent
for marker in markers:
_one_spend_row(marker, "asend_message", "success")
def test_registered_agent_persists_protocol_version(gateway: Gateway) -> None:
with peer("lgstored", "langgraph") as agent, gateway.scenario() as scenario:
identity: Final = register(gateway, scenario.cleanups.callback, agent.marker, agent.url, PINNED)
assert gateway.get(f"/v1/agents/{identity}")["litellm_params"]["a2a_protocol_version"] == "0.3" # type: ignore[index]
rows: Final = read_rows('SELECT litellm_params FROM "LiteLLM_AgentsTable" WHERE agent_id=%s', (identity,))
assert len(rows) == 1, rows
stored: Final = rows[0]["litellm_params"]
stored_params: Final = json.loads(stored) if isinstance(stored, str) else stored
assert stored_params["a2a_protocol_version"] == "0.3", rows # type: ignore[index]
_send_ok(gateway, identity, agent.marker)
_assert_legacy_wire(posts(agent.wire), agent.marker) # type: ignore[arg-type]
def test_pinned_agent_survives_localhost_retry(gateway: Gateway) -> None:
dead: Final = f"http://localhost:{allocate_port()}/a2a/unreachable"
with peer("lgretry", "langgraph", card_url=dead) as agent, gateway.scenario() as scenario:
identity: Final = register(gateway, scenario.cleanups.callback, agent.marker, agent.url, PINNED)
_send_ok(gateway, identity, agent.marker)
traffic: Final = agent.wire.drain() # type: ignore[union-attr]
sent: Final = tuple(json.loads(item.body) for item in traffic if item.method == "POST")
assert tuple(item.target for item in traffic if item.method == "POST") == ("/a2a/" + agent.marker + "/",), (
traffic
)
_assert_legacy_wire(sent, agent.marker)
_one_spend_row(agent.marker, "asend_message", "success")