@@ -1468,6 +1468,106 @@ async def handle_async_request(self, request: httpx.Request) -> httpx.Response:
14681468 assert requests [1 ] == (httpx .URL (f"{ base_url } /browsers/sess-1/fs/write_file?path=%2Ftmp%2Fx" ), b"next" )
14691469
14701470
1471+ @pytest .mark .parametrize (
1472+ ("failure_type" , "expected_error" ),
1473+ [
1474+ (httpx .ReadError , APIConnectionError ),
1475+ (httpx .ReadTimeout , APITimeoutError ),
1476+ (None , InternalServerError ),
1477+ ],
1478+ )
1479+ def test_direct_vm_failure_does_not_retry_unreplayable_write (
1480+ monkeypatch : pytest .MonkeyPatch ,
1481+ failure_type : type [httpx .TransportError ] | None ,
1482+ expected_error : type [Exception ],
1483+ ) -> None :
1484+ monkeypatch .delenv ("KERNEL_BROWSER_ROUTING_SUBRESOURCES" , raising = False )
1485+ monkeypatch .setattr ("kernel._base_client.SyncAPIClient._sleep_for_retry" , _skip_retry_sleep )
1486+ requests : list [tuple [httpx .URL , bytes ]] = []
1487+
1488+ class Transport (httpx .BaseTransport ):
1489+ @override
1490+ def handle_request (self , request : httpx .Request ) -> httpx .Response :
1491+ body = b"" .join (cast (Iterator [bytes ], request .stream ))
1492+ requests .append ((request .url , body ))
1493+ if failure_type is not None :
1494+ raise failure_type ("connection failed after sending the request" , request = request )
1495+ return httpx .Response (500 , json = {"error" : "boom" })
1496+
1497+ http_client = httpx .Client (transport = Transport ())
1498+ with Kernel (
1499+ base_url = base_url ,
1500+ api_key = api_key ,
1501+ http_client = http_client ,
1502+ _strict_response_validation = True ,
1503+ ) as client :
1504+ _cache_browser (client )
1505+ with pytest .raises (expected_error ):
1506+ client .browsers .fs .write_file ("sess-1" , _UnseekableFile (b"payload" ), path = "/tmp/x" )
1507+
1508+ assert client .browser_route_cache .get ("sess-1" ) is not None
1509+
1510+ assert requests == [
1511+ (
1512+ httpx .URL ("http://browser-session.test/browser/kernel/fs/write_file?path=%2Ftmp%2Fx&jwt=token-abc" ),
1513+ b"payload" ,
1514+ )
1515+ ]
1516+
1517+
1518+ @pytest .mark .asyncio
1519+ @pytest .mark .parametrize (
1520+ ("failure_type" , "expected_error" ),
1521+ [
1522+ (httpx .ReadError , APIConnectionError ),
1523+ (httpx .ReadTimeout , APITimeoutError ),
1524+ (None , InternalServerError ),
1525+ ],
1526+ )
1527+ async def test_async_direct_vm_failure_does_not_retry_unreplayable_write (
1528+ monkeypatch : pytest .MonkeyPatch ,
1529+ failure_type : type [httpx .TransportError ] | None ,
1530+ expected_error : type [Exception ],
1531+ ) -> None :
1532+ monkeypatch .delenv ("KERNEL_BROWSER_ROUTING_SUBRESOURCES" , raising = False )
1533+ monkeypatch .setattr ("kernel._base_client.AsyncAPIClient._sleep_for_retry" , _skip_async_retry_sleep )
1534+ requests : list [tuple [httpx .URL , bytes ]] = []
1535+
1536+ async def payload () -> AsyncIterator [bytes ]:
1537+ yield b"payload"
1538+
1539+ class Transport (httpx .AsyncBaseTransport ):
1540+ @override
1541+ async def handle_async_request (self , request : httpx .Request ) -> httpx .Response :
1542+ body = b"" .join ([chunk async for chunk in cast (AsyncIterator [bytes ], request .stream )])
1543+ requests .append ((request .url , body ))
1544+ if failure_type is not None :
1545+ raise failure_type ("connection failed after sending the request" , request = request )
1546+ return httpx .Response (500 , json = {"error" : "boom" })
1547+
1548+ http_client = httpx .AsyncClient (transport = Transport ())
1549+ async with AsyncKernel (
1550+ base_url = base_url ,
1551+ api_key = api_key ,
1552+ http_client = http_client ,
1553+ _strict_response_validation = True ,
1554+ ) as client :
1555+ route = browser_route_from_browser (_fake_browser ())
1556+ assert route is not None
1557+ client .browser_route_cache .set (route )
1558+ with pytest .raises (expected_error ):
1559+ await client .browsers .fs .write_file ("sess-1" , cast (Any , payload ()), path = "/tmp/x" )
1560+
1561+ assert client .browser_route_cache .get ("sess-1" ) is not None
1562+
1563+ assert requests == [
1564+ (
1565+ httpx .URL ("http://browser-session.test/browser/kernel/fs/write_file?path=%2Ftmp%2Fx&jwt=token-abc" ),
1566+ b"payload" ,
1567+ )
1568+ ]
1569+
1570+
14711571def test_copied_client_registers_one_route_eviction_hook () -> None :
14721572 with Kernel (base_url = base_url , api_key = api_key , _strict_response_validation = True ) as client :
14731573 copied = client .copy (api_key = "sk-456" )
0 commit comments