feat(ai): services partages A2 + SSE streaming B4 + confirmations UI B5 (#79)
CI / lint (push) Successful in 58s
CI / security (push) Successful in 40s
CI / test (push) Successful in 1m15s
CI / build (push) Successful in 37s
CI / e2e (push) Successful in 10m15s
Desktop Build / build-windows (push) Canceled after 0s
Desktop Build / build-linux (push) Canceled after 0s
CI / lint (push) Successful in 58s
CI / security (push) Successful in 40s
CI / test (push) Successful in 1m15s
CI / build (push) Successful in 37s
CI / e2e (push) Successful in 10m15s
Desktop Build / build-windows (push) Canceled after 0s
Desktop Build / build-linux (push) Canceled after 0s
This commit is contained in:
@@ -199,3 +199,95 @@ class TestGeminiToolsConversion:
|
||||
converted = _gemini_tools(TOOLS)
|
||||
assert converted[0]["functionDeclarations"][0]["name"] == "read_file"
|
||||
assert converted[0]["functionDeclarations"][0]["description"] == "Read a file"
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
# Streaming (B4)
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
class _StreamResponse:
|
||||
"""Fake httpx streaming response yielding pre-scripted SSE lines."""
|
||||
|
||||
def __init__(self, lines, status=200):
|
||||
self.lines = lines
|
||||
self.status_code = status
|
||||
|
||||
def raise_for_status(self):
|
||||
if self.status_code >= 400:
|
||||
raise _http_error(self.status_code)
|
||||
|
||||
async def aiter_lines(self):
|
||||
for line in self.lines:
|
||||
yield line
|
||||
|
||||
|
||||
class _FakeAsyncClient:
|
||||
"""Minimal stand-in for ``httpx.AsyncClient`` used in stream mode."""
|
||||
|
||||
captured: dict = {}
|
||||
response: _StreamResponse | None = None
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *exc):
|
||||
return False
|
||||
|
||||
def stream(self, method, url, headers=None, json=None):
|
||||
_FakeAsyncClient.captured = {"method": method, "url": url, "headers": headers, "json": json}
|
||||
resp = _FakeAsyncClient.response
|
||||
|
||||
class _Ctx:
|
||||
async def __aenter__(self_inner):
|
||||
return resp
|
||||
|
||||
async def __aexit__(self_inner, *exc):
|
||||
return False
|
||||
|
||||
return _Ctx()
|
||||
|
||||
|
||||
class TestStreamCompletion:
|
||||
@pytest.mark.asyncio
|
||||
async def test_openai_stream_yields_deltas(self, monkeypatch):
|
||||
monkeypatch.setattr(ai_chat, "_get_provider_config", lambda provider=None: OPENAI_CFG)
|
||||
monkeypatch.setattr(ai_chat.httpx, "AsyncClient", _FakeAsyncClient)
|
||||
_FakeAsyncClient.response = _StreamResponse([
|
||||
'data: {"choices":[{"delta":{"content":"Bon"}}]}',
|
||||
"",
|
||||
'data: {"choices":[{"delta":{"content":"jour"}}]}',
|
||||
"data: not-json",
|
||||
"data: [DONE]",
|
||||
])
|
||||
|
||||
chunks = [c async for c in ai_chat.stream_completion([{"role": "user", "content": "hi"}])]
|
||||
assert "".join(chunks) == "Bonjour"
|
||||
assert _FakeAsyncClient.captured["json"]["stream"] is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_gemini_stream_yields_text(self, monkeypatch):
|
||||
monkeypatch.setitem(ai_chat.PROVIDERS, "gemini", GEMINI_CFG)
|
||||
monkeypatch.setattr(ai_chat, "_get_provider_config", lambda provider=None: GEMINI_CFG)
|
||||
monkeypatch.setattr(ai_chat.httpx, "AsyncClient", _FakeAsyncClient)
|
||||
_FakeAsyncClient.response = _StreamResponse([
|
||||
'data: {"candidates":[{"content":{"parts":[{"text":"sa"}]}}]}',
|
||||
'data: {"candidates":[{"content":{"parts":[{"text":"lut"}]}}]}',
|
||||
])
|
||||
|
||||
chunks = [c async for c in ai_chat.stream_completion([{"role": "user", "content": "hi"}])]
|
||||
assert "".join(chunks) == "salut"
|
||||
assert "streamGenerateContent" in _FakeAsyncClient.captured["url"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stream_raises_on_http_error(self, monkeypatch):
|
||||
monkeypatch.setattr(ai_chat, "_get_provider_config", lambda provider=None: OPENAI_CFG)
|
||||
monkeypatch.setattr(ai_chat.httpx, "AsyncClient", _FakeAsyncClient)
|
||||
_FakeAsyncClient.response = _StreamResponse([], status=500)
|
||||
|
||||
with pytest.raises(httpx.HTTPStatusError):
|
||||
async for _ in ai_chat.stream_completion([{"role": "user", "content": "hi"}]):
|
||||
pass
|
||||
|
||||
Reference in New Issue
Block a user