Skip to content

feat(testing): in-process A2A test client (sibling to SellerTestClient) #678

@bokelley

Description

@bokelley

Parent: #662
MCP sibling shipped in #666 (SellerTestClient.invoke()).

Why

SellerTestClient eliminates JSON-RPC envelope + adcp_error extraction boilerplate for MCP adopters. A2A adopters need the same harness shape — but A2A's semantics are different enough that a shared invoke() would force-fit both.

What's different about A2A

A2A is task-state-machine, not call/return:

  • task.create(method, payload) → returns a task handle in submitted / working state
  • State transitions through TaskState.TASK_STATE_* (see src/adcp/server/a2a_server.py:467,482,547)
  • Terminal states: COMPLETED / FAILED / CANCELED
  • Async completion is delivered via push-notification webhooks the seller signs

A SellerTestClient.invoke() shape collapses all of that into a single call, hiding exactly the lifecycle adopters need to test.

Proposed shape (not final — open to triage)

from adcp.testing import SellerA2AClient   # name TBD

async def test_create_buy_completes(seller_a2a):
    handle = await seller_a2a.create_task(
        "create_media_buy", {"products": [...], ...}
    )
    result = await handle.wait_for(state="completed", timeout=5.0)
    assert result.passed
    assert result.data[\"media_buy_id\"]

async def test_push_notification_signed_on_completion(seller_a2a):
    handle = await seller_a2a.create_task("create_media_buy", {...})
    notif = await seller_a2a.expect_push_notification(timeout=5.0)
    assert notif.task_id == handle.task_id
    assert notif.signed_with_kid == \"kid_test\"
    assert notif.body[\"final\"] is True

Surfaces the harness likely needs:

  • create_task(method, payload) → TaskHandle
  • TaskHandle.wait_for(state=..., timeout=...) → ToolInvokeResult
  • TaskHandle.cancel()
  • An in-process push-notification sink (captures outbound webhooks instead of HTTP-POSTing them) so adopters can assert on signing, body shape, and ordering

Why this matters for the broader A2A adoption story

A2A adoption is currently deferred behind "pluggable TaskStore + push-notification + middleware hooks." An in-process test harness gives adopters something concrete to write against once those hooks land — and exercises the hooks themselves, which makes the harness a useful design forcing-function.

Scope notes

  • Not transport=\"a2a\" on SellerTestClient. The lifecycle is different enough that one method surface forces a bad shape onto both.
  • Reuse ToolInvokeResult / AdcpErrorPayload for terminal-state results so the assertion grammar stays consistent.
  • Defer the design until feat(testing): add SellerTestClient for in-process handler testing #666 lands (it'll be the concrete MCP reference point).

References

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions