feat: surgery pipeline API, video inference, voice confirm, and tests
- Add FastAPI routes for surgery start/end, results, pending confirmation (WAV upload), and health checks.
- Implement RTSP/Hikvision capture, consumable classification, session manager, MinIO/Baidu voice resolution, and DB persistence.
- Add documentation (client API, video backends, staging checklist) and sample camera/RTSP config.
- Add pytest suite (API contract, session manager, voice, repositories, pipeline persistence) and httpx dev dependency.
- Replace deprecated HTTP_422_UNPROCESSABLE_ENTITY with HTTP_422_UNPROCESSABLE_CONTENT.
- Fix SurgeryPipeline DB reads to use an explicit transaction with autobegin disabled.
Made-with: Cursor
2026-04-21 18:33:54 +08:00
|
|
|
"""HTTP contract tests for surgery client API (dependency overrides, no real DB on lifespan)."""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
from datetime import datetime, timezone
|
|
|
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
from fastapi import FastAPI
|
|
|
|
|
from fastapi.testclient import TestClient
|
|
|
|
|
|
|
|
|
|
from app.api import router as api_router
|
|
|
|
|
from app.dependencies import get_surgery_pipeline
|
|
|
|
|
from app.schemas import (
|
|
|
|
|
SurgeryConsumptionDetail,
|
|
|
|
|
SurgeryPendingConfirmationResponse,
|
|
|
|
|
)
|
|
|
|
|
from app.services.voice_resolution import VoiceResolveResult
|
|
|
|
|
from app.surgery_errors import SurgeryPipelineError
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def instant_sleep(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
|
|
|
async def _noop(_delay: float) -> None:
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr("app.api.asyncio.sleep", _noop)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def api_app(monkeypatch: pytest.MonkeyPatch) -> FastAPI:
|
|
|
|
|
async def _check_db_ok() -> None:
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr("app.api.check_database", _check_db_ok)
|
|
|
|
|
|
|
|
|
|
app = FastAPI()
|
|
|
|
|
app.include_router(api_router)
|
|
|
|
|
return app
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_health_ok(api_app: FastAPI) -> None:
|
|
|
|
|
client = TestClient(api_app)
|
|
|
|
|
r = client.get("/health")
|
|
|
|
|
assert r.status_code == 200
|
|
|
|
|
assert r.json()["status"] == "ok"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_start_surgery_accepted(api_app: FastAPI, instant_sleep: None) -> None:
|
|
|
|
|
pipeline = MagicMock()
|
|
|
|
|
pipeline.start_recording = AsyncMock(return_value=None)
|
|
|
|
|
api_app.dependency_overrides[get_surgery_pipeline] = lambda: pipeline
|
|
|
|
|
client = TestClient(api_app)
|
|
|
|
|
r = client.post(
|
|
|
|
|
"/client/surgeries/start",
|
|
|
|
|
json={
|
|
|
|
|
"surgery_id": "123456",
|
|
|
|
|
"camera_ids": ["cam1"],
|
|
|
|
|
"candidate_consumables": ["纱布"],
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
assert r.status_code == 200
|
|
|
|
|
body = r.json()
|
|
|
|
|
assert body["surgery_id"] == "123456"
|
|
|
|
|
assert body["status"] == "accepted"
|
|
|
|
|
pipeline.start_recording.assert_awaited_once()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_start_surgery_503_on_pipeline_error(
|
|
|
|
|
api_app: FastAPI, instant_sleep: None
|
|
|
|
|
) -> None:
|
|
|
|
|
pipeline = MagicMock()
|
|
|
|
|
pipeline.start_recording = AsyncMock(
|
|
|
|
|
side_effect=SurgeryPipelineError("RECORDING_CANNOT_START", "cannot")
|
|
|
|
|
)
|
|
|
|
|
api_app.dependency_overrides[get_surgery_pipeline] = lambda: pipeline
|
|
|
|
|
client = TestClient(api_app)
|
|
|
|
|
r = client.post(
|
|
|
|
|
"/client/surgeries/start",
|
|
|
|
|
json={"surgery_id": "123456", "camera_ids": ["c1"], "candidate_consumables": []},
|
|
|
|
|
)
|
|
|
|
|
assert r.status_code == 503
|
|
|
|
|
d = r.json()["detail"]
|
|
|
|
|
assert d["code"] == "RECORDING_CANNOT_START"
|
|
|
|
|
assert d["surgery_id"] == "123456"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_start_surgery_422_invalid_surgery_id(api_app: FastAPI) -> None:
|
|
|
|
|
pipeline = MagicMock()
|
|
|
|
|
pipeline.start_recording = AsyncMock()
|
|
|
|
|
api_app.dependency_overrides[get_surgery_pipeline] = lambda: pipeline
|
|
|
|
|
client = TestClient(api_app)
|
|
|
|
|
r = client.post(
|
|
|
|
|
"/client/surgeries/start",
|
|
|
|
|
json={"surgery_id": "12", "camera_ids": ["c1"], "candidate_consumables": []},
|
|
|
|
|
)
|
|
|
|
|
assert r.status_code == 422
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_end_surgery_accepted(api_app: FastAPI, instant_sleep: None) -> None:
|
|
|
|
|
pipeline = MagicMock()
|
|
|
|
|
pipeline.stop_recording = AsyncMock(return_value=None)
|
|
|
|
|
api_app.dependency_overrides[get_surgery_pipeline] = lambda: pipeline
|
|
|
|
|
client = TestClient(api_app)
|
|
|
|
|
r = client.post("/client/surgeries/end", json={"surgery_id": "123456"})
|
|
|
|
|
assert r.status_code == 200
|
|
|
|
|
pipeline.stop_recording.assert_awaited_once_with("123456")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_get_result_200(api_app: FastAPI) -> None:
|
|
|
|
|
ts = datetime(2026, 4, 21, 12, 0, tzinfo=timezone.utc)
|
|
|
|
|
pipeline = MagicMock()
|
|
|
|
|
pipeline.get_consumption_details_for_client = AsyncMock(
|
|
|
|
|
return_value=[
|
|
|
|
|
SurgeryConsumptionDetail(
|
|
|
|
|
item_id="纱布",
|
|
|
|
|
item_name="纱布",
|
2026-04-23 16:09:20 +08:00
|
|
|
qty=1,
|
feat: surgery pipeline API, video inference, voice confirm, and tests
- Add FastAPI routes for surgery start/end, results, pending confirmation (WAV upload), and health checks.
- Implement RTSP/Hikvision capture, consumable classification, session manager, MinIO/Baidu voice resolution, and DB persistence.
- Add documentation (client API, video backends, staging checklist) and sample camera/RTSP config.
- Add pytest suite (API contract, session manager, voice, repositories, pipeline persistence) and httpx dev dependency.
- Replace deprecated HTTP_422_UNPROCESSABLE_ENTITY with HTTP_422_UNPROCESSABLE_CONTENT.
- Fix SurgeryPipeline DB reads to use an explicit transaction with autobegin disabled.
Made-with: Cursor
2026-04-21 18:33:54 +08:00
|
|
|
doctor_id="vision",
|
|
|
|
|
timestamp=ts,
|
|
|
|
|
),
|
|
|
|
|
]
|
|
|
|
|
)
|
|
|
|
|
api_app.dependency_overrides[get_surgery_pipeline] = lambda: pipeline
|
|
|
|
|
client = TestClient(api_app)
|
|
|
|
|
r = client.get("/client/surgeries/123456/result")
|
|
|
|
|
assert r.status_code == 200
|
|
|
|
|
body = r.json()
|
|
|
|
|
assert body["surgery_id"] == "123456"
|
|
|
|
|
assert len(body["details"]) == 1
|
2026-04-23 16:09:20 +08:00
|
|
|
assert list(body["details"][0].keys()) == [
|
|
|
|
|
"item_id",
|
|
|
|
|
"item_name",
|
|
|
|
|
"qty",
|
|
|
|
|
"doctor_id",
|
|
|
|
|
"timestamp",
|
|
|
|
|
]
|
feat: surgery pipeline API, video inference, voice confirm, and tests
- Add FastAPI routes for surgery start/end, results, pending confirmation (WAV upload), and health checks.
- Implement RTSP/Hikvision capture, consumable classification, session manager, MinIO/Baidu voice resolution, and DB persistence.
- Add documentation (client API, video backends, staging checklist) and sample camera/RTSP config.
- Add pytest suite (API contract, session manager, voice, repositories, pipeline persistence) and httpx dev dependency.
- Replace deprecated HTTP_422_UNPROCESSABLE_ENTITY with HTTP_422_UNPROCESSABLE_CONTENT.
- Fix SurgeryPipeline DB reads to use an explicit transaction with autobegin disabled.
Made-with: Cursor
2026-04-21 18:33:54 +08:00
|
|
|
assert body["summary"][0]["total_quantity"] == 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_get_result_503_not_ready(api_app: FastAPI) -> None:
|
|
|
|
|
pipeline = MagicMock()
|
|
|
|
|
pipeline.get_consumption_details_for_client = AsyncMock(return_value=None)
|
|
|
|
|
api_app.dependency_overrides[get_surgery_pipeline] = lambda: pipeline
|
|
|
|
|
client = TestClient(api_app)
|
|
|
|
|
r = client.get("/client/surgeries/123456/result")
|
|
|
|
|
assert r.status_code == 503
|
|
|
|
|
assert r.json()["detail"]["code"] == "RESULT_NOT_READY"
|
|
|
|
|
|
|
|
|
|
|
2026-04-23 20:42:21 +08:00
|
|
|
def test_get_result_503_empty_details(api_app: FastAPI) -> None:
|
|
|
|
|
pipeline = MagicMock()
|
|
|
|
|
pipeline.get_consumption_details_for_client = AsyncMock(return_value=[])
|
|
|
|
|
api_app.dependency_overrides[get_surgery_pipeline] = lambda: pipeline
|
|
|
|
|
client = TestClient(api_app)
|
|
|
|
|
r = client.get("/client/surgeries/123456/result")
|
|
|
|
|
assert r.status_code == 503
|
|
|
|
|
assert r.json()["detail"]["code"] == "RESULT_NOT_READY"
|
|
|
|
|
|
|
|
|
|
|
feat: surgery pipeline API, video inference, voice confirm, and tests
- Add FastAPI routes for surgery start/end, results, pending confirmation (WAV upload), and health checks.
- Implement RTSP/Hikvision capture, consumable classification, session manager, MinIO/Baidu voice resolution, and DB persistence.
- Add documentation (client API, video backends, staging checklist) and sample camera/RTSP config.
- Add pytest suite (API contract, session manager, voice, repositories, pipeline persistence) and httpx dev dependency.
- Replace deprecated HTTP_422_UNPROCESSABLE_ENTITY with HTTP_422_UNPROCESSABLE_CONTENT.
- Fix SurgeryPipeline DB reads to use an explicit transaction with autobegin disabled.
Made-with: Cursor
2026-04-21 18:33:54 +08:00
|
|
|
def test_pending_confirmation_200_and_404(api_app: FastAPI) -> None:
|
|
|
|
|
ts = datetime(2026, 4, 21, 12, 0, tzinfo=timezone.utc)
|
|
|
|
|
payload = SurgeryPendingConfirmationResponse(
|
|
|
|
|
surgery_id="123456",
|
|
|
|
|
confirmation_id="cid",
|
|
|
|
|
prompt_text="请确认",
|
2026-04-23 16:09:20 +08:00
|
|
|
prompt_audio_mp3_base64="//uQ",
|
feat: surgery pipeline API, video inference, voice confirm, and tests
- Add FastAPI routes for surgery start/end, results, pending confirmation (WAV upload), and health checks.
- Implement RTSP/Hikvision capture, consumable classification, session manager, MinIO/Baidu voice resolution, and DB persistence.
- Add documentation (client API, video backends, staging checklist) and sample camera/RTSP config.
- Add pytest suite (API contract, session manager, voice, repositories, pipeline persistence) and httpx dev dependency.
- Replace deprecated HTTP_422_UNPROCESSABLE_ENTITY with HTTP_422_UNPROCESSABLE_CONTENT.
- Fix SurgeryPipeline DB reads to use an explicit transaction with autobegin disabled.
Made-with: Cursor
2026-04-21 18:33:54 +08:00
|
|
|
options=[],
|
|
|
|
|
model_top1_label="x",
|
|
|
|
|
model_top1_confidence=0.4,
|
|
|
|
|
created_at=ts,
|
|
|
|
|
)
|
|
|
|
|
pipeline_ok = MagicMock()
|
2026-04-23 16:09:20 +08:00
|
|
|
pipeline_ok.get_pending_confirmation_for_client = AsyncMock(return_value=payload)
|
feat: surgery pipeline API, video inference, voice confirm, and tests
- Add FastAPI routes for surgery start/end, results, pending confirmation (WAV upload), and health checks.
- Implement RTSP/Hikvision capture, consumable classification, session manager, MinIO/Baidu voice resolution, and DB persistence.
- Add documentation (client API, video backends, staging checklist) and sample camera/RTSP config.
- Add pytest suite (API contract, session manager, voice, repositories, pipeline persistence) and httpx dev dependency.
- Replace deprecated HTTP_422_UNPROCESSABLE_ENTITY with HTTP_422_UNPROCESSABLE_CONTENT.
- Fix SurgeryPipeline DB reads to use an explicit transaction with autobegin disabled.
Made-with: Cursor
2026-04-21 18:33:54 +08:00
|
|
|
api_app.dependency_overrides[get_surgery_pipeline] = lambda: pipeline_ok
|
|
|
|
|
client = TestClient(api_app)
|
|
|
|
|
r = client.get("/client/surgeries/123456/pending-confirmation")
|
|
|
|
|
assert r.status_code == 200
|
2026-04-23 16:09:20 +08:00
|
|
|
body_ok = r.json()
|
|
|
|
|
assert body_ok["confirmation_id"] == "cid"
|
|
|
|
|
assert body_ok["prompt_audio_mp3_base64"] == "//uQ"
|
feat: surgery pipeline API, video inference, voice confirm, and tests
- Add FastAPI routes for surgery start/end, results, pending confirmation (WAV upload), and health checks.
- Implement RTSP/Hikvision capture, consumable classification, session manager, MinIO/Baidu voice resolution, and DB persistence.
- Add documentation (client API, video backends, staging checklist) and sample camera/RTSP config.
- Add pytest suite (API contract, session manager, voice, repositories, pipeline persistence) and httpx dev dependency.
- Replace deprecated HTTP_422_UNPROCESSABLE_ENTITY with HTTP_422_UNPROCESSABLE_CONTENT.
- Fix SurgeryPipeline DB reads to use an explicit transaction with autobegin disabled.
Made-with: Cursor
2026-04-21 18:33:54 +08:00
|
|
|
|
|
|
|
|
pipeline_none = MagicMock()
|
2026-04-23 16:09:20 +08:00
|
|
|
pipeline_none.get_pending_confirmation_for_client = AsyncMock(return_value=None)
|
feat: surgery pipeline API, video inference, voice confirm, and tests
- Add FastAPI routes for surgery start/end, results, pending confirmation (WAV upload), and health checks.
- Implement RTSP/Hikvision capture, consumable classification, session manager, MinIO/Baidu voice resolution, and DB persistence.
- Add documentation (client API, video backends, staging checklist) and sample camera/RTSP config.
- Add pytest suite (API contract, session manager, voice, repositories, pipeline persistence) and httpx dev dependency.
- Replace deprecated HTTP_422_UNPROCESSABLE_ENTITY with HTTP_422_UNPROCESSABLE_CONTENT.
- Fix SurgeryPipeline DB reads to use an explicit transaction with autobegin disabled.
Made-with: Cursor
2026-04-21 18:33:54 +08:00
|
|
|
api_app.dependency_overrides[get_surgery_pipeline] = lambda: pipeline_none
|
|
|
|
|
client2 = TestClient(api_app)
|
|
|
|
|
r2 = client2.get("/client/surgeries/123456/pending-confirmation")
|
|
|
|
|
assert r2.status_code == 404
|
|
|
|
|
assert r2.json()["detail"]["code"] == "NO_PENDING_CONFIRMATION"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_resolve_empty_audio_422(api_app: FastAPI) -> None:
|
|
|
|
|
pipeline = MagicMock()
|
|
|
|
|
api_app.dependency_overrides[get_surgery_pipeline] = lambda: pipeline
|
|
|
|
|
client = TestClient(api_app)
|
|
|
|
|
r = client.post(
|
|
|
|
|
"/client/surgeries/123456/pending-confirmation/cid/resolve",
|
|
|
|
|
files={"audio": ("a.wav", b"", "audio/wav")},
|
|
|
|
|
)
|
|
|
|
|
assert r.status_code == 422
|
|
|
|
|
assert r.json()["detail"]["code"] == "VOICE_AUDIO_INVALID"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_resolve_non_wav_422(api_app: FastAPI) -> None:
|
|
|
|
|
pipeline = MagicMock()
|
|
|
|
|
api_app.dependency_overrides[get_surgery_pipeline] = lambda: pipeline
|
|
|
|
|
client = TestClient(api_app)
|
|
|
|
|
r = client.post(
|
|
|
|
|
"/client/surgeries/123456/pending-confirmation/cid/resolve",
|
|
|
|
|
files={"audio": ("a.mp3", b"abc", "audio/mpeg")},
|
|
|
|
|
)
|
|
|
|
|
assert r.status_code == 422
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_resolve_200(api_app: FastAPI) -> None:
|
|
|
|
|
pipeline = MagicMock()
|
|
|
|
|
pipeline.resolve_pending_confirmation_from_audio = AsyncMock(
|
|
|
|
|
return_value=VoiceResolveResult(
|
|
|
|
|
resolved_label="纱布",
|
|
|
|
|
rejected=False,
|
|
|
|
|
asr_text="第一个",
|
|
|
|
|
audio_object_key="k.wav",
|
|
|
|
|
message="ok",
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
api_app.dependency_overrides[get_surgery_pipeline] = lambda: pipeline
|
|
|
|
|
client = TestClient(api_app)
|
|
|
|
|
r = client.post(
|
|
|
|
|
"/client/surgeries/123456/pending-confirmation/cid/resolve",
|
|
|
|
|
files={"audio": ("a.wav", b"RIFF", "audio/wav")},
|
|
|
|
|
)
|
|
|
|
|
assert r.status_code == 200
|
|
|
|
|
body = r.json()
|
|
|
|
|
assert body["resolved_label"] == "纱布"
|
|
|
|
|
assert body["rejected"] is False
|
|
|
|
|
assert body["asr_text"] == "第一个"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_resolve_maps_surgery_pipeline_error_to_http(api_app: FastAPI) -> None:
|
|
|
|
|
pipeline = MagicMock()
|
|
|
|
|
pipeline.resolve_pending_confirmation_from_audio = AsyncMock(
|
|
|
|
|
side_effect=SurgeryPipelineError("CONFIRMATION_NOT_FOUND", "missing")
|
|
|
|
|
)
|
|
|
|
|
api_app.dependency_overrides[get_surgery_pipeline] = lambda: pipeline
|
|
|
|
|
client = TestClient(api_app)
|
|
|
|
|
r = client.post(
|
|
|
|
|
"/client/surgeries/123456/pending-confirmation/cid/resolve",
|
|
|
|
|
files={"audio": ("a.wav", b"x", "audio/wav")},
|
|
|
|
|
)
|
|
|
|
|
assert r.status_code == 404
|
|
|
|
|
assert r.json()["detail"]["code"] == "CONFIRMATION_NOT_FOUND"
|