Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license
This commit is contained in:
@@ -0,0 +1,325 @@
|
||||
import errno
|
||||
import selectors
|
||||
import socket
|
||||
|
||||
import httpcore
|
||||
import pytest
|
||||
|
||||
from agent import process_bootstrap
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def no_proxy_env(monkeypatch):
|
||||
for name in (
|
||||
"HTTPS_PROXY",
|
||||
"HTTP_PROXY",
|
||||
"ALL_PROXY",
|
||||
"https_proxy",
|
||||
"http_proxy",
|
||||
"all_proxy",
|
||||
"NO_PROXY",
|
||||
"no_proxy",
|
||||
):
|
||||
monkeypatch.delenv(name, raising=False)
|
||||
|
||||
|
||||
def _client_backends(client):
|
||||
transports = [client._transport, *client._mounts.values()]
|
||||
return [
|
||||
transport._pool._network_backend
|
||||
for transport in transports
|
||||
if transport is not None and hasattr(transport, "_pool")
|
||||
]
|
||||
|
||||
|
||||
def test_codex_sync_client_uses_happy_eyeballs_backend(no_proxy_env):
|
||||
from run_agent import AIAgent
|
||||
|
||||
client = AIAgent._build_keepalive_http_client(
|
||||
"https://chatgpt.com/backend-api/codex"
|
||||
)
|
||||
try:
|
||||
assert any(
|
||||
isinstance(backend, process_bootstrap._HappyEyeballsSyncBackend)
|
||||
for backend in _client_backends(client)
|
||||
)
|
||||
finally:
|
||||
client.close()
|
||||
|
||||
|
||||
def test_other_sync_clients_keep_httpcore_default_backend(no_proxy_env):
|
||||
client = process_bootstrap.build_keepalive_http_client(
|
||||
"https://api.openai.com/v1"
|
||||
)
|
||||
try:
|
||||
assert all(
|
||||
isinstance(backend, httpcore.SyncBackend)
|
||||
for backend in _client_backends(client)
|
||||
)
|
||||
finally:
|
||||
client.close()
|
||||
|
||||
|
||||
def test_connection_staggers_past_blackholed_ipv6(monkeypatch):
|
||||
clock = [0.0]
|
||||
sockets = []
|
||||
|
||||
class FakeSocket:
|
||||
def __init__(self, family, socktype, proto):
|
||||
self.family = family
|
||||
self.closed = False
|
||||
self.timeout = None
|
||||
sockets.append(self)
|
||||
|
||||
def setsockopt(self, *_args):
|
||||
pass
|
||||
|
||||
def setblocking(self, _blocking):
|
||||
pass
|
||||
|
||||
def settimeout(self, timeout):
|
||||
self.timeout = timeout
|
||||
|
||||
def bind(self, _address):
|
||||
pass
|
||||
|
||||
def connect_ex(self, _address):
|
||||
if self.family == socket.AF_INET6:
|
||||
return errno.EINPROGRESS
|
||||
return 0
|
||||
|
||||
def close(self):
|
||||
self.closed = True
|
||||
|
||||
class FakeSelector:
|
||||
def __init__(self):
|
||||
self.registered = set()
|
||||
|
||||
def register(self, fileobj, _events):
|
||||
self.registered.add(fileobj)
|
||||
|
||||
def unregister(self, fileobj):
|
||||
self.registered.discard(fileobj)
|
||||
|
||||
def select(self, timeout):
|
||||
clock[0] += timeout or 0.0
|
||||
return []
|
||||
|
||||
def close(self):
|
||||
pass
|
||||
|
||||
monkeypatch.setattr(
|
||||
process_bootstrap.socket,
|
||||
"getaddrinfo",
|
||||
lambda *_args, **_kwargs: [
|
||||
(
|
||||
socket.AF_INET6,
|
||||
socket.SOCK_STREAM,
|
||||
socket.IPPROTO_TCP,
|
||||
"",
|
||||
("2001:db8::1", 443, 0, 0),
|
||||
),
|
||||
(
|
||||
socket.AF_INET,
|
||||
socket.SOCK_STREAM,
|
||||
socket.IPPROTO_TCP,
|
||||
"",
|
||||
("192.0.2.1", 443),
|
||||
),
|
||||
],
|
||||
)
|
||||
monkeypatch.setattr(process_bootstrap.socket, "socket", FakeSocket)
|
||||
monkeypatch.setattr(
|
||||
process_bootstrap.selectors, "DefaultSelector", FakeSelector
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
process_bootstrap.time, "monotonic", lambda: clock[0]
|
||||
)
|
||||
|
||||
winner = process_bootstrap._happy_eyeballs_create_connection(
|
||||
("chatgpt.com", 443),
|
||||
timeout=10.0,
|
||||
)
|
||||
|
||||
assert winner.family == socket.AF_INET
|
||||
assert winner.timeout == 10.0
|
||||
assert clock[0] == process_bootstrap._HAPPY_EYEBALLS_DELAY_SECONDS
|
||||
assert sockets[0].closed is True
|
||||
assert sockets[1].closed is False
|
||||
|
||||
|
||||
def test_async_codex_client_relies_on_native_anyio_racing(no_proxy_env):
|
||||
"""The async transport needs no custom backend — anyio races natively.
|
||||
|
||||
httpcore's ``AnyIOBackend.connect_tcp`` delegates to
|
||||
``anyio.connect_tcp``, whose ``happy_eyeballs_delay`` default (0.25s)
|
||||
implements RFC 8305 staggered family racing. This pins the contract the
|
||||
``async_mode`` branch of ``build_keepalive_http_client`` documents: if
|
||||
anyio ever drops the parameter (or the default stops racing), this fails
|
||||
and the async path needs an explicit backend like the sync one.
|
||||
"""
|
||||
import inspect
|
||||
|
||||
import anyio
|
||||
|
||||
params = inspect.signature(anyio.connect_tcp).parameters
|
||||
assert "happy_eyeballs_delay" in params
|
||||
assert params["happy_eyeballs_delay"].default == pytest.approx(0.25)
|
||||
|
||||
client = process_bootstrap.build_keepalive_http_client(
|
||||
"https://chatgpt.com/backend-api/codex", async_mode=True
|
||||
)
|
||||
try:
|
||||
assert all(
|
||||
not isinstance(backend, process_bootstrap._HappyEyeballsSyncBackend)
|
||||
for backend in _client_backends(client)
|
||||
)
|
||||
finally:
|
||||
import asyncio
|
||||
|
||||
asyncio.get_event_loop_policy().new_event_loop().run_until_complete(
|
||||
client.aclose()
|
||||
)
|
||||
|
||||
|
||||
def test_async_connect_races_past_blackholed_ipv6(monkeypatch):
|
||||
"""IPv4 completes ~250ms after a hanging IPv6 attempt on the async path.
|
||||
|
||||
Mirrors ``test_connection_staggers_past_blackholed_ipv6`` for the async
|
||||
transport: resolve a fake host to a blackholed IPv6 address plus a live
|
||||
local IPv4 listener and assert httpcore's async backend connects fast
|
||||
instead of serially waiting out the IPv6 connect timeout.
|
||||
"""
|
||||
import asyncio
|
||||
import threading
|
||||
import time as _time
|
||||
|
||||
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
server.bind(("127.0.0.1", 0))
|
||||
server.listen(5)
|
||||
port = server.getsockname()[1]
|
||||
|
||||
def _accept_loop():
|
||||
while True:
|
||||
try:
|
||||
conn, _ = server.accept()
|
||||
conn.close()
|
||||
except OSError:
|
||||
return
|
||||
|
||||
thread = threading.Thread(target=_accept_loop, daemon=True)
|
||||
thread.start()
|
||||
|
||||
real_getaddrinfo = socket.getaddrinfo
|
||||
|
||||
def fake_getaddrinfo(host, *args, **kwargs):
|
||||
name = host.decode() if isinstance(host, (bytes, bytearray)) else str(host)
|
||||
if name == "codex-he-async.test":
|
||||
return [
|
||||
(
|
||||
socket.AF_INET6,
|
||||
socket.SOCK_STREAM,
|
||||
socket.IPPROTO_TCP,
|
||||
"",
|
||||
("100::1", port, 0, 0), # RFC 6666 discard prefix: blackhole
|
||||
),
|
||||
(
|
||||
socket.AF_INET,
|
||||
socket.SOCK_STREAM,
|
||||
socket.IPPROTO_TCP,
|
||||
"",
|
||||
("127.0.0.1", port),
|
||||
),
|
||||
]
|
||||
return real_getaddrinfo(host, *args, **kwargs)
|
||||
|
||||
monkeypatch.setattr(socket, "getaddrinfo", fake_getaddrinfo)
|
||||
|
||||
async def _connect():
|
||||
from httpcore._backends.auto import AutoBackend
|
||||
|
||||
backend = AutoBackend()
|
||||
start = _time.monotonic()
|
||||
stream = await backend.connect_tcp(
|
||||
"codex-he-async.test", port, timeout=30.0
|
||||
)
|
||||
elapsed = _time.monotonic() - start
|
||||
await stream.aclose()
|
||||
return elapsed
|
||||
|
||||
try:
|
||||
elapsed = asyncio.run(_connect())
|
||||
finally:
|
||||
server.close()
|
||||
|
||||
# Native anyio racing: IPv6 is attempted first, IPv4 starts 0.25s later
|
||||
# and wins immediately. Serial behavior would block until the IPv6
|
||||
# connect timeout (tens of seconds). Generous bound for slow CI hosts.
|
||||
assert elapsed < 5.0
|
||||
|
||||
|
||||
class _RecordingPool:
|
||||
def __init__(self):
|
||||
self._network_backend = "default"
|
||||
|
||||
|
||||
class _RecordingTransport:
|
||||
def __init__(self):
|
||||
self._pool = _RecordingPool()
|
||||
|
||||
|
||||
def test_enable_happy_eyeballs_on_client_covers_transport_and_mounts():
|
||||
class _Client:
|
||||
pass
|
||||
|
||||
client = _Client()
|
||||
client._transport = _RecordingTransport()
|
||||
client._mounts = {"https://": _RecordingTransport(), "http://": None}
|
||||
|
||||
process_bootstrap.enable_happy_eyeballs_on_client(client)
|
||||
|
||||
assert isinstance(
|
||||
client._transport._pool._network_backend,
|
||||
process_bootstrap._HappyEyeballsSyncBackend,
|
||||
)
|
||||
assert isinstance(
|
||||
client._mounts["https://"]._pool._network_backend,
|
||||
process_bootstrap._HappyEyeballsSyncBackend,
|
||||
)
|
||||
|
||||
|
||||
def test_enable_happy_eyeballs_on_client_skips_proxy_pools(no_proxy_env):
|
||||
import httpcore
|
||||
import httpx
|
||||
|
||||
client = httpx.Client(proxy="http://127.0.0.1:3128")
|
||||
try:
|
||||
process_bootstrap.enable_happy_eyeballs_on_client(client)
|
||||
proxy_pools = [
|
||||
transport._pool
|
||||
for transport in client._mounts.values()
|
||||
if transport is not None
|
||||
and isinstance(getattr(transport, "_pool", None), httpcore.HTTPProxy)
|
||||
]
|
||||
assert proxy_pools # the all:// mount is proxy-backed
|
||||
assert all(
|
||||
not isinstance(
|
||||
pool._network_backend, process_bootstrap._HappyEyeballsSyncBackend
|
||||
)
|
||||
for pool in proxy_pools
|
||||
)
|
||||
finally:
|
||||
client.close()
|
||||
|
||||
|
||||
def test_codex_auth_http_client_uses_happy_eyeballs_backend(no_proxy_env):
|
||||
from hermes_cli.auth import _codex_http_client
|
||||
|
||||
client = _codex_http_client(timeout=5.0)
|
||||
try:
|
||||
assert any(
|
||||
isinstance(backend, process_bootstrap._HappyEyeballsSyncBackend)
|
||||
for backend in _client_backends(client)
|
||||
)
|
||||
finally:
|
||||
client.close()
|
||||
Reference in New Issue
Block a user