"""Voice input endpoint (#116): transcribes only, validates the audio, never touches the dialog.""" from collections.abc import AsyncIterator import pytest from httpx import ASGITransport, AsyncClient from sqlalchemy import select from app.api import patient_web from app.db import session from app.db.bootstrap import init_db from app.db.clinical import Message from app.llm.stt import MockSTT, SttError, SttNoSpeech, load_texts from app.main import app from tests.test_llm_stt import make_wav MEM = "sqlite+aiosqlite:///:memory:" WAV = {"Content-Type": "audio/wav"} @pytest.fixture async def client(monkeypatch: pytest.MonkeyPatch) -> AsyncIterator[AsyncClient]: monkeypatch.setattr(patient_web, "get_stt", lambda _settings: MockSTT()) # never the network in tests await session.configure(identity_url=MEM, clinical_url=MEM) await init_db() async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as c: yield c await session.configure() async def _token(client: AsyncClient) -> str: return (await client.post("/api/v1/auth/anonymous")).json()["token"] async def test_transcribes_without_writing_to_dialog(client: AsyncClient) -> None: token = await _token(client) resp = await client.post(f"/api/v1/me/{token}/stt", content=make_wav(2), headers=WAV) assert resp.status_code == 200 assert resp.json() == {"text": load_texts().mock_transcript, "provider": "mock"} async with session.get_clinical_session() as s: assert (await s.execute(select(Message))).scalars().all() == [] async def test_capability_and_off(client: AsyncClient, monkeypatch: pytest.MonkeyPatch) -> None: assert (await client.get("/api/v1/patient/capabilities")).json()["voice_input"] is True monkeypatch.setattr(patient_web, "get_stt", lambda _settings: None) assert (await client.get("/api/v1/patient/capabilities")).json()["voice_input"] is False token = await _token(client) assert (await client.post(f"/api/v1/me/{token}/stt", content=make_wav(1), headers=WAV)).status_code == 404 async def test_rejects_unknown_patient_bad_format_and_long_audio(client: AsyncClient) -> None: assert (await client.post("/api/v1/me/nobody/stt", content=make_wav(1), headers=WAV)).status_code == 404 token = await _token(client) url = f"/api/v1/me/{token}/stt" assert (await client.post(url, content=b"OggS" + b"\0" * 200)).status_code == 415 assert (await client.post(url, content=make_wav(70), headers=WAV)).status_code == 413 assert (await client.post(url, content=make_wav(10, rate=48_000), headers=WAV)).status_code == 200 @pytest.mark.parametrize(("error", "status"), [(SttNoSpeech("x"), 422), (SttError("x"), 503)]) async def test_provider_errors(client: AsyncClient, monkeypatch: pytest.MonkeyPatch, error, status) -> None: class Failing: name = "failing" async def transcribe(self, audio: bytes, fmt: str = "wav") -> str: raise error monkeypatch.setattr(patient_web, "get_stt", lambda _settings: Failing()) token = await _token(client) assert (await client.post(f"/api/v1/me/{token}/stt", content=make_wav(1), headers=WAV)).status_code == status