|
9 | 9 | import httpx |
10 | 10 | import pytest |
11 | 11 |
|
12 | | -from codebeep.opencode_client import Message, OpenCodeClient |
| 12 | +from codebeep.opencode_client import Message, OpenCodeClient, OpenCodeInvalidResponseError |
13 | 13 |
|
14 | 14 |
|
15 | 15 | @pytest.fixture |
@@ -64,6 +64,21 @@ def stream(self, method: str, path: str) -> _FakeStreamResponse: |
64 | 64 | return self._responses.pop(0) |
65 | 65 |
|
66 | 66 |
|
| 67 | +class _RetryingHttpClient: |
| 68 | + def __init__(self, responses: list[httpx.Response | Exception]) -> None: |
| 69 | + self._responses = responses |
| 70 | + self.calls = 0 |
| 71 | + self.requests: list[dict[str, object]] = [] |
| 72 | + |
| 73 | + async def request(self, method: str, path: str, **kwargs) -> httpx.Response: |
| 74 | + self.calls += 1 |
| 75 | + self.requests.append({"method": method, "path": path, **kwargs}) |
| 76 | + response = self._responses.pop(0) |
| 77 | + if isinstance(response, Exception): |
| 78 | + raise response |
| 79 | + return response |
| 80 | + |
| 81 | + |
67 | 82 | class TestOpenCodeClient: |
68 | 83 | """Tests for OpenCodeClient.""" |
69 | 84 |
|
@@ -141,3 +156,36 @@ async def test_subscribe_events_falls_back_to_legacy_path( |
141 | 156 | assert fake_client.requested_paths == ["/global/event", "/event"] |
142 | 157 | assert events[0].type == "session.message" |
143 | 158 | assert client.extract_session_id_from_event(events[0]) == "sess-1" |
| 159 | + |
| 160 | + @pytest.mark.asyncio |
| 161 | + async def test_request_retries_transport_error(self, client: OpenCodeClient, monkeypatch) -> None: |
| 162 | + request = httpx.Request("GET", "http://127.0.0.1:4096/session") |
| 163 | + fake_client = _RetryingHttpClient( |
| 164 | + [ |
| 165 | + httpx.ConnectError("boom", request=request), |
| 166 | + httpx.Response(200, request=request, json=[]), |
| 167 | + ] |
| 168 | + ) |
| 169 | + client._get_client = AsyncMock(return_value=fake_client) # type: ignore[attr-defined] |
| 170 | + sleep = AsyncMock() |
| 171 | + monkeypatch.setattr("codebeep.opencode_client.asyncio.sleep", sleep) |
| 172 | + monkeypatch.setattr("codebeep.opencode_client.random.uniform", lambda _a, _b: 0.0) |
| 173 | + |
| 174 | + sessions = await client.list_sessions() |
| 175 | + |
| 176 | + assert sessions == [] |
| 177 | + assert fake_client.calls == 2 |
| 178 | + sleep.assert_awaited_once() |
| 179 | + |
| 180 | + @pytest.mark.asyncio |
| 181 | + async def test_list_sessions_rejects_non_json_payload(self, client: OpenCodeClient) -> None: |
| 182 | + request = httpx.Request("GET", "http://127.0.0.1:4096/session") |
| 183 | + fake_client = _RetryingHttpClient( |
| 184 | + [ |
| 185 | + httpx.Response(200, request=request, text="not-json"), |
| 186 | + ] |
| 187 | + ) |
| 188 | + client._get_client = AsyncMock(return_value=fake_client) # type: ignore[attr-defined] |
| 189 | + |
| 190 | + with pytest.raises(OpenCodeInvalidResponseError): |
| 191 | + await client.list_sessions() |
0 commit comments