Files
OpenJarvis/tests/server/test_speech_routes.py
T
d865b4bed4 Fix blocking async server handlers (#618)
Closes #219. Replace synchronous httpx calls in async SendBlue and model-management handlers with awaited httpx.AsyncClient (context-managed close); run Whisper transcription and engine.list_models via asyncio.to_thread so they don't block the event loop; and harden TelemetryStore/aggregator SQLite for concurrency (WAL, synchronous=NORMAL, busy_timeout=5000, plus a write-serializing lock on the shared connection). Adds async-usage assertions and a real 8-thread concurrent-write test. Related: #570 (async httpx, different issue #559).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 17:37:20 -07:00

140 lines
3.9 KiB
Python

"""Tests for speech API endpoints."""
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
fastapi = pytest.importorskip("fastapi")
from fastapi.testclient import TestClient # noqa: E402
from openjarvis.speech._stubs import TranscriptionResult # noqa: E402
@pytest.fixture
def mock_speech_backend():
backend = MagicMock()
backend.backend_id = "mock"
backend.health.return_value = True
backend.transcribe.return_value = TranscriptionResult(
text="Hello world",
language="en",
confidence=0.95,
duration_seconds=1.5,
segments=[],
)
return backend
@pytest.fixture
def app_with_speech(mock_speech_backend):
from fastapi import FastAPI
from openjarvis.server.api_routes import speech_router
app = FastAPI()
app.state.speech_backend = mock_speech_backend
app.include_router(speech_router)
return app
@pytest.fixture
def client(app_with_speech):
return TestClient(app_with_speech)
def test_transcribe_endpoint(client, mock_speech_backend):
response = client.post(
"/v1/speech/transcribe",
files={"file": ("test.wav", b"fake audio data", "audio/wav")},
)
assert response.status_code == 200
data = response.json()
assert data["text"] == "Hello world"
assert data["language"] == "en"
assert data["confidence"] == 0.95
assert data["duration_seconds"] == 1.5
def test_transcribe_endpoint_offloads_backend_work(client, mock_speech_backend):
expected = TranscriptionResult(
text="Offloaded",
language="en",
confidence=0.9,
duration_seconds=1.0,
segments=[],
)
with patch(
"openjarvis.server.api_routes.asyncio.to_thread",
new_callable=AsyncMock,
) as mock_to_thread:
mock_to_thread.return_value = expected
response = client.post(
"/v1/speech/transcribe",
files={"file": ("test.wav", b"fake audio data", "audio/wav")},
)
assert response.status_code == 200
mock_to_thread.assert_awaited_once()
args, kwargs = mock_to_thread.await_args
assert args == (mock_speech_backend.transcribe, b"fake audio data")
assert kwargs == {"format": "wav", "language": None}
assert response.json()["text"] == "Offloaded"
def test_transcribe_endpoint_surfaces_backend_error(client, mock_speech_backend):
mock_speech_backend.transcribe.side_effect = RuntimeError("missing cublas64_12.dll")
response = client.post(
"/v1/speech/transcribe",
files={"file": ("test.wav", b"fake audio data", "audio/wav")},
)
assert response.status_code == 500
assert "missing cublas64_12.dll" in response.json()["detail"]
def test_transcribe_no_file(client):
response = client.post("/v1/speech/transcribe")
assert response.status_code == 400 or response.status_code == 422
def test_health_endpoint(client):
response = client.get("/v1/speech/health")
assert response.status_code == 200
data = response.json()
assert data["available"] is True
assert data["backend"] == "mock"
def test_health_endpoint_includes_unavailable_reason(client, mock_speech_backend):
mock_speech_backend.health.return_value = False
mock_speech_backend.last_error.return_value = (
"Install with: uv sync --extra desktop"
)
response = client.get("/v1/speech/health")
assert response.status_code == 200
data = response.json()
assert data["available"] is False
assert data["reason"] == "Install with: uv sync --extra desktop"
def test_health_no_backend():
from fastapi import FastAPI
from fastapi.testclient import TestClient
from openjarvis.server.api_routes import speech_router
app = FastAPI()
app.state.speech_backend = None
app.include_router(speech_router)
client = TestClient(app)
response = client.get("/v1/speech/health")
assert response.status_code == 200
data = response.json()
assert data["available"] is False