mirror of
https://github.com/open-jarvis/OpenJarvis.git
synced 2026-07-30 19:02:16 +00:00
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>
140 lines
3.9 KiB
Python
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
|