|
1 | 1 | from __future__ import annotations |
2 | 2 |
|
3 | 3 | import os |
4 | | -from typing import Any, cast |
| 4 | +import asyncio |
| 5 | +from typing import Any, AsyncIterator, cast |
| 6 | +from typing_extensions import override |
5 | 7 |
|
6 | 8 | import httpx |
7 | 9 | import respx |
@@ -119,6 +121,56 @@ def test_telemetry_stream_routes_directly_to_vm(monkeypatch: pytest.MonkeyPatch) |
119 | 121 | assert request.headers.get("Authorization") is None |
120 | 122 |
|
121 | 123 |
|
| 124 | +@pytest.mark.asyncio |
| 125 | +async def test_async_telemetry_stream_cancellation_survives_direct_routing( |
| 126 | + monkeypatch: pytest.MonkeyPatch, |
| 127 | +) -> None: |
| 128 | + monkeypatch.setenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", "telemetry/stream") |
| 129 | + read_started = asyncio.Event() |
| 130 | + read_stopped = asyncio.Event() |
| 131 | + |
| 132 | + class BlockingSSEStream(httpx.AsyncByteStream): |
| 133 | + @override |
| 134 | + async def __aiter__(self) -> AsyncIterator[bytes]: |
| 135 | + read_started.set() |
| 136 | + try: |
| 137 | + await asyncio.Event().wait() |
| 138 | + finally: |
| 139 | + read_stopped.set() |
| 140 | + yield b"" |
| 141 | + |
| 142 | + @override |
| 143 | + async def aclose(self) -> None: |
| 144 | + read_stopped.set() |
| 145 | + |
| 146 | + async def handle_request(request: httpx.Request) -> httpx.Response: |
| 147 | + assert request.url.path == "/browser/kernel/telemetry/stream" |
| 148 | + return httpx.Response( |
| 149 | + 200, |
| 150 | + headers={"content-type": "text/event-stream"}, |
| 151 | + stream=BlockingSSEStream(), |
| 152 | + ) |
| 153 | + |
| 154 | + http_client = httpx.AsyncClient(transport=httpx.MockTransport(handle_request)) |
| 155 | + async with AsyncKernel( |
| 156 | + base_url=base_url, |
| 157 | + api_key=api_key, |
| 158 | + http_client=http_client, |
| 159 | + _strict_response_validation=True, |
| 160 | + ) as client: |
| 161 | + route = browser_route_from_browser(_fake_browser()) |
| 162 | + assert route is not None |
| 163 | + client.browser_route_cache.set(route) |
| 164 | + stream = await client.browsers.telemetry.stream("sess-1") |
| 165 | + consumer = asyncio.create_task(stream.__anext__()) |
| 166 | + await asyncio.wait_for(read_started.wait(), timeout=1) |
| 167 | + |
| 168 | + consumer.cancel() |
| 169 | + with pytest.raises(asyncio.CancelledError): |
| 170 | + await asyncio.wait_for(consumer, timeout=1) |
| 171 | + await asyncio.wait_for(read_stopped.wait(), timeout=1) |
| 172 | + |
| 173 | + |
122 | 174 | @respx.mock |
123 | 175 | def test_browser_request_params_cannot_override_target_url_or_jwt() -> None: |
124 | 176 | route = respx.get("http://browser-session.test/browser/kernel/curl/raw").mock( |
|
0 commit comments