test/acp/test_remote_transport.py
Rama main · réplica de lectura del repositorio original.
# Copyright (c) 2026, AG2ai, Inc., AG2ai open-source projects maintainers and core contributors
#
# SPDX-License-Identifier: Apache-2.0
"""The remote transports against a real ACP agent, served on loopback.
Everywhere else the fake agent replaces the connection hook, which is what keeps
those tests fast and transport-agnostic — but it also means no test there opens a
socket. This file is the other half: an ordinary ``Agent.ask`` reaches an agent
over real HTTP and a real WebSocket, so what a request actually carries (the
configured headers, in particular) is asserted from the receiving end.
"""
import pytest
pytest.importorskip("websockets")
pytest.importorskip("h2")
import asyncio
import contextlib
from collections.abc import AsyncGenerator, Callable
from typing import Any
import acp
import uvicorn
from acp import schema
from acp.http.asgi import create_asgi_app
from ag2 import Agent
from ag2.acp import ACPRemoteConfig
REPLY = "hi from the wire"
class _EchoAgent:
"""A minimal ACP Agent: enough of one to carry a single turn."""
def __init__(self, conn: acp.Client) -> None:
self.conn = conn
async def initialize(self, **kwargs: Any) -> schema.InitializeResponse:
return schema.InitializeResponse(
protocol_version=acp.PROTOCOL_VERSION,
agent_info=schema.Implementation(name="echo", version="test"),
)
async def new_session(self, **kwargs: Any) -> schema.NewSessionResponse:
return schema.NewSessionResponse(session_id="remote-session-1")
async def prompt(self, *, session_id: str, **kwargs: Any) -> schema.PromptResponse:
await self.conn.session_update(session_id=session_id, update=acp.update_agent_message_text(REPLY))
return schema.PromptResponse(stop_reason="end_turn")
@contextlib.asynccontextmanager
async def _served_agent() -> AsyncGenerator[tuple[int, list[dict[str, str]]], None]:
"""Serve ``_EchoAgent`` on a loopback port, recording every request's headers.
The ASGI app handles both the HTTP and the WebSocket transport, so one server
answers whichever URL scheme a test points at it.
"""
requests: list[dict[str, str]] = []
app = create_asgi_app(_EchoAgent) # type: ignore[arg-type]
async def recording(scope: dict[str, Any], receive: Callable, send: Callable) -> None:
if scope["type"] in ("http", "websocket"):
requests.append({k.decode("latin-1").lower(): v.decode("latin-1") for k, v in scope["headers"]})
await app(scope, receive, send)
config = uvicorn.Config(recording, host="127.0.0.1", port=0, log_level="warning")
# Bound here rather than inside `serve()`: the socket is already listening, so
# the port is known and a connection can be made without waiting for start-up.
sock = config.bind_socket()
server = uvicorn.Server(config)
serving = asyncio.create_task(server.serve(sockets=[sock]))
try:
yield sock.getsockname()[1], requests
finally:
server.should_exit = True
await serving
sock.close()
@pytest.mark.asyncio
@pytest.mark.parametrize("scheme", ["http", "ws"], ids=["http", "websocket"])
async def test_a_turn_crosses_a_real_connection(scheme: str) -> None:
async with _served_agent() as (port, _):
cfg = ACPRemoteConfig(url=f"{scheme}://127.0.0.1:{port}/acp", expose_tools=False)
try:
reply = await Agent("acp", config=cfg).ask("hello")
finally:
await cfg.aclose()
assert reply.body == REPLY
@pytest.mark.asyncio
@pytest.mark.parametrize("scheme", ["http", "ws"], ids=["http", "websocket"])
async def test_configured_headers_are_sent_with_every_request(scheme: str) -> None:
"""This is how a bearer-token gateway is reached, so no request may skip them."""
async with _served_agent() as (port, requests):
cfg = ACPRemoteConfig(
url=f"{scheme}://127.0.0.1:{port}/acp",
headers={"Authorization": "Bearer t0ken", "X-Tenant": "acme"},
expose_tools=False,
)
try:
await Agent("acp", config=cfg).ask("hello")
finally:
await cfg.aclose()
assert requests, "the agent was never reached, so nothing was carried"
assert all(r.get("authorization") == "Bearer t0ken" for r in requests)
assert all(r.get("x-tenant") == "acme" for r in requests)
@pytest.mark.asyncio
async def test_no_headers_configured_sends_no_authorization() -> None:
async with _served_agent() as (port, requests):
cfg = ACPRemoteConfig(url=f"http://127.0.0.1:{port}/acp", expose_tools=False)
try:
await Agent("acp", config=cfg).ask("hello")
finally:
await cfg.aclose()
assert requests
assert not any("authorization" in r for r in requests)