Files
wehub-resource-sync 91e75e620b
CI: cua-driver distro-compat matrix / debian:12 (glibc 2.36) (push) Has been cancelled
CI: SPDX Headers / Check SPDX headers (warn-only) (push) Has been cancelled
CD: Docs MCP Server / build (linux/amd64) (push) Has been cancelled
CD: Docs MCP Server / build (linux/arm64) (push) Has been cancelled
CD: Docs MCP Server / merge (push) Has been cancelled
CI: cua-driver distro-compat matrix / Resolve release version (push) Has been cancelled
CI: cua-driver distro-compat matrix / fedora:41 (glibc 2.40) (push) Has been cancelled
CI: cua-driver distro-compat matrix / rockylinux:9 (glibc 2.34) (push) Has been cancelled
CI: cua-driver distro-compat matrix / ubuntu:22.04 (glibc 2.35) (push) Has been cancelled
CI: cua-driver distro-compat matrix / ubuntu:24.04 (glibc 2.39) (push) Has been cancelled
CI: cua-driver distro-compat matrix / Distro compat summary (push) Has been cancelled
CI: Rust Linux unit / Rust Linux unit and compile (push) Has been cancelled
CI: Rust Windows unit / Rust Windows unit and compile (push) Has been cancelled
CI: Nix Linux Rust source / Nix / compositor build (push) Has been cancelled
CI: Nix Linux Rust source / Nix / driver package (push) Has been cancelled
CI: Nix Linux Rust source / Nix / Rust unit tests (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:03:19 +08:00

130 lines
3.9 KiB
Python

"""Unit tests for HTTPTransport._cmd retry-on-5xx behavior.
The computer-server can briefly return 5xx (Traefik dropping the pod
from its endpoint list, grpc fork hiccups). The transport retries
up to 3 times with exponential backoff for 5xx responses. 4xx
errors are raised immediately. httpx transport exceptions (read
timeout, connection reset) are not retried because the command may
have already started on the server.
"""
from __future__ import annotations
import json
import httpx
import pytest
from cua_sandbox.transport.http import HTTPTransport
pytestmark = pytest.mark.asyncio
def _sse_body(payload: dict) -> str:
return f"data: {json.dumps(payload)}\n\n"
async def _make_transport(handler) -> HTTPTransport:
t = HTTPTransport("http://server:8000", timeout=5.0)
# Patch the async client after connect so the mock transport is in use.
await t.connect()
assert t._client is not None
await t._client.aclose()
t._client = httpx.AsyncClient(
base_url="http://server:8000",
transport=httpx.MockTransport(handler),
timeout=5.0,
)
return t
async def test_cmd_success_no_retry():
calls = []
def handler(request: httpx.Request) -> httpx.Response:
calls.append(request.url.path)
return httpx.Response(200, text=_sse_body({"width": 1, "height": 2}))
t = await _make_transport(handler)
result = await t._cmd("get_screen_size")
assert result == {"width": 1, "height": 2}
assert len(calls) == 1, "success should not retry"
async def test_cmd_retries_5xx_then_succeeds(monkeypatch):
# Skip sleeps so the test runs instantly.
import cua_sandbox.transport.http as http_mod
async def _no_sleep(_seconds):
pass
monkeypatch.setattr(http_mod.asyncio, "sleep", _no_sleep)
responses = iter(
[
httpx.Response(503, text="service unavailable"),
httpx.Response(502, text="bad gateway"),
httpx.Response(200, text=_sse_body({"ok": True})),
]
)
calls = []
def handler(request: httpx.Request) -> httpx.Response:
calls.append(request.url.path)
return next(responses)
t = await _make_transport(handler)
result = await t._cmd("screenshot")
assert result == {"ok": True}
assert len(calls) == 3, "should retry twice before succeeding"
async def test_cmd_gives_up_after_max_retries(monkeypatch):
import cua_sandbox.transport.http as http_mod
async def _no_sleep(_seconds):
pass
monkeypatch.setattr(http_mod.asyncio, "sleep", _no_sleep)
calls = []
def handler(request: httpx.Request) -> httpx.Response:
calls.append(request.url.path)
return httpx.Response(503, text="still unavailable")
t = await _make_transport(handler)
with pytest.raises(httpx.HTTPStatusError) as exc_info:
await t._cmd("screenshot")
assert exc_info.value.response.status_code == 503
# _CMD_MAX_RETRIES (3) attempts total, then raise.
assert len(calls) == 3
async def test_cmd_does_not_retry_4xx():
calls = []
def handler(request: httpx.Request) -> httpx.Response:
calls.append(request.url.path)
return httpx.Response(401, text="unauthorized")
t = await _make_transport(handler)
with pytest.raises(httpx.HTTPStatusError) as exc_info:
await t._cmd("screenshot")
assert exc_info.value.response.status_code == 401
assert len(calls) == 1, "4xx should not retry"
async def test_cmd_does_not_retry_read_timeout():
calls = []
def handler(request: httpx.Request) -> httpx.Response:
calls.append(request.url.path)
raise httpx.ReadTimeout("server too slow", request=request)
t = await _make_transport(handler)
with pytest.raises(httpx.ReadTimeout):
await t._cmd("run_command", params={"command": "slow"})
# ReadTimeout means the request reached the server — don't retry
# or we risk double-executing a non-idempotent command.
assert len(calls) == 1