test: fill A2A adapter unit test gaps - #605
Open
AlexanderZ-Band wants to merge 5 commits into
Open
Conversation
Adds coverage for 10 undertested paths across the outbound A2AAdapter and A2AGatewayAdapter: peer-not-found resolution, peer-list pagination, artifact overwrite reduction, TASK_STATE_AUTH_REQUIRED handling, and several gateway JSON-RPC route edge cases (malformed body, non-scalar request id, SendStreamingMessage, missing-id task methods, partial room-creation state). Two of these gaps were real bugs surfaced while writing their regression tests, fixed alongside the new coverage: - A2AGatewayAdapter._execute_a2a: a REST failure while posting to Band left the remote A2A caller stuck on a WORKING task forever, since no terminal FAILED event was ever published on that path. - A2AAdapter.cleanup_all: an exception from the A2A client's close() skipped closing the owned httpx transport, leaking it on shutdown. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017iDgB8tuW6tPom4PFomMzr
Part 2 of INT-1357: three bespoke baseline smokes (A2A is a protocol bridge, not a matrix adapter) covering the outbound A2AAdapter, the A2AGatewayAdapter's JSON-RPC server, and a full Band-to-Band round trip over real A2A wire traffic. - a2aServer.py: a minimal, scripted A2A counterparty server built on a2a-sdk's own primitives (not Band) -- no LLM key needed. - test_a2a.py: a live A2AAdapter against that fixture server, covering both a canned-reply happy path and a scripted remote task failure. - test_a2a_gateway.py: the official a2a-sdk reference client driving a live A2AGatewayAdapter exposing a real Anthropic-backed Band peer -- validates the gateway independent of our own A2AAdapter. - test_a2a_roundtrip.py: Band Agent A (A2AAdapter) -> gateway -> Band Agent B (Anthropic) and back, the realistic Band-to-Band usage shape. Manually verified the fixture server and the gateway's JSON-RPC surface against real a2a-sdk clients (no Band platform credentials available in this session); the three smokes themselves need a live platform + an Anthropic key to actually run. Also fixes a race surfaced while building these: GatewayServer.start() returned as soon as serve() was scheduled, before uvicorn was actually listening, so a caller dialing in immediately after (a real A2A client, or these new smokes) could hit connection-refused. start() now waits for uvicorn's ready signal, bounded by a timeout. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017iDgB8tuW6tPom4PFomMzr
httpx.AsyncClient's default 5s read timeout fires on the normal multi-second gap between SSE events during a live remote turn (an LLM call, a tool loop), not just a genuine hang. Leave read unbounded while keeping connect/write/pool bounded, so a dead peer still fails promptly. Applies the same fix to the raw a2a-sdk client built in the test_a2a_gateway.py live smoke, and adds a regression test asserting the owned httpx client has no read timeout.
sse_starlette's shutdown watcher polls whichever uvicorn.Server owns the process's SIGTERM slot and promotes its should_exit to the process-global AppStatus.should_exit. GatewayServer.stop() sets should_exit directly (never via a signal), so once any gateway in a process stops, every later GatewayServer's SSE streams are cancelled immediately -- reproduced live as the A2A round-trip E2E smoke failing right after the gateway smoke (each running its own GatewayServer in the same pytest session) with "ASGI callable returned without completing response." / an incomplete chunked read on the client side. band.integrations.mcp.local_server already disables sse_starlette's automatic graceful drain for the identical bug; the A2A gateway now does the same directly rather than depending on that module happening to be imported first. Adds a regression test that reproduces the real failure with two live GatewayServer instances in one process (confirmed it fails with the fix reverted, with the exact same error signature seen in the live E2E run).
AlexanderZ-Band
enabled auto-merge
September 2, 2026 11:18
amit-gazal-band
requested changes
Sep 4, 2026
amit-gazal-band
left a comment
Collaborator
There was a problem hiding this comment.
Flagging three issues from review, focused on the SSE-timeout fix and the new gateway startup-wait logic.
…artup wait A2AAdapter's httpx client left its read timeout fully unbounded to survive normal multi-second gaps between SSE events -- but that also meant a remote peer that accepts the connection and then genuinely hangs would never time out, since nothing else bounds an in-flight adapter cycle. Swap read=None for a generous-but-finite bound (httpx resets it on every chunk, so it still tolerates a slow live turn). GatewayServer._wait_until_started only polled uvicorn's `started` flag and never checked whether its serve task had already failed, so a fast startup failure (e.g. a port already in use) busy-waited the full timeout and raised a generic error instead of the real one -- a bug the correct version in mcp/local_server.py already avoided, and a third copy in the A2A baseline test fixture repeated. Extract the correct version into one shared band.integrations.uvicorn_server.wait_until_started, used by all three. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YDYPGiaDSYNZnyC5mg18ch
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Part 1 — unit test gaps:
A2AAdapterandA2AGatewayAdapter: peer-not-found resolution, peer-list pagination, artifact-overwrite reduction,TASK_STATE_AUTH_REQUIREDhandling, and several gateway JSON-RPC route edge cases (malformed body, non-scalar request id,SendStreamingMessage, missing-id task methods, partial room-creation state).A2AGatewayAdapter._execute_a2a: a REST failure while posting to Band left the remote A2A caller stuck on a WORKING task forever, since no terminal FAILED event was ever published on that path. Now callspending.fail(...)before re-raising.A2AAdapter.cleanup_all: an exception from the A2A client'sclose()skipped closing the owned httpx transport, leaking it on shutdown. Now closes the transport in afinally.Part 2 — live E2E smokes:
tests/e2e/baseline/smoke/adapters/(A2A is a protocol bridge, not a matrix adapter, so these follow thetest_parlant.pypattern):test_a2a.py— a liveA2AAdapteragainst a new scripted, non-Band A2A counterparty fixture (a2aServer.py, built on a2a-sdk's own server primitives — no LLM key needed). Covers both a canned-reply happy path and a scripted remote task failure.test_a2a_gateway.py— the official a2a-sdk reference client (a2a.client.Client) driving a liveA2AGatewayAdapterthat exposes a real Anthropic-backed Band peer — validates the gateway's JSON-RPC server independent of our ownA2AAdapter.test_a2a_roundtrip.py— the full Band-to-Band shape: Band Agent A (A2AAdapter) → live gateway → Band Agent B (Anthropic) → reply relayed all the way back.GatewayServer.start()returned as soon asserve()was scheduled, before uvicorn was actually listening — a caller dialing in immediately after (a real A2A client, or these new smokes) could hit connection-refused.start()now waits for uvicorn's ready signal, bounded by a timeout, with a small regression test.ANTHROPIC_API_KEY(see Part 3 — two of the three initially failed live and led to two more real bug fixes); all four tests (test_a2a.pyhas two) now pass live with zero reruns.Part 3 — two more real bugs found running the smokes live:
A2AAdapter's owned httpx client had no read-timeout override, so httpx's default 5s read timeout fired on the ordinary multi-second gap between SSE events during a real remote turn (a live LLM call), not just a genuine hang — surfaced as a spuriousA2AClientTimeoutErroron any real turn slower than 5s. Fixed by leavingreadunbounded while keeping connect/write/pool bounded, so a genuinely dead peer still fails promptly. Same fix applied to the raw a2a-sdk client built intest_a2a_gateway.py.sse_starlette's process-global shutdown flag across instances.sse_starlette's background shutdown watcher polls whicheveruvicorn.Serverowns the process's SIGTERM slot and promotes itsshould_exitto the process-globalAppStatus.should_exit.GatewayServer.stop()setsshould_exitdirectly (never via a real signal) — so once any gateway in a process stops, every laterGatewayServer's SSE streams in that same process get cancelled immediately ("ASGI callable returned without completing response.", an incomplete chunked read on the client side). This is exactly whytest_a2a_roundtrip.pyfailed right aftertest_a2a_gateway.pypassed in the same pytest session — each test builds its ownGatewayServer.band.integrations.mcp.local_serverhad already hit and fixed the identical bug for itself (a real Windows CI hang); the A2A gateway now disables the same process-global switch directly, rather than depending on that other module happening to be imported first. Added a regression test with two realGatewayServerinstances in one process (confirmed it reproduces the exact same failure signature with the fix reverted, and passes with it restored).Closes INT-1357.
Test plan
uv run pytest tests/integrations/a2a/ tests/integrations/mcp/ -v --no-cov(89 passed)uv run pytest tests/ --ignore=tests/integration/ --ignore=tests/e2e/ -q(5288 passed, 123 skipped, 0 failed)uv run ruff check ./uv run ruff format .uv run pyrefly check(0 errors)E2E_TESTS_ENABLED=true uv run pytest tests/e2e/baseline/smoke/adapters/test_a2a.py tests/e2e/baseline/smoke/adapters/test_a2a_gateway.py tests/e2e/baseline/smoke/adapters/test_a2a_roundtrip.py -v -s --no-covagainst a live platform +ANTHROPIC_API_KEY— all 4 tests pass, zero reruns (117s total)RemoteProtocolError: incomplete chunked read/ASGI callable returned without completing response.); restored the fix and confirmed it passes again🤖 Generated with Claude Code
https://claude.ai/code/session_017iDgB8tuW6tPom4PFomMzr