This commit is contained in:
Kevin
2026-04-28 10:41:48 +08:00
parent 482b016872
commit 15884bd68e
60 changed files with 2092 additions and 1994 deletions

View File

@@ -6,7 +6,7 @@ import asyncio
import json
from datetime import datetime, timezone
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from fastapi import FastAPI
@@ -107,6 +107,43 @@ def test_assign_voice_terminal_helper_matches_start_surgery_behavior(
assert hub.get_assignment("TERM-X") == "123456"
async def test_voice_terminal_hub_notify_pending_head_payload() -> None:
payload = SurgeryPendingConfirmationResponse(
surgery_id="123456",
confirmation_id="cid-a",
pending_queue_length=1,
pending_queue_position=1,
pending_cumulative_ordinal=1,
prompt_text="请确认",
prompt_audio_mp3_base64="QQ==",
options=[],
model_top1_label="x",
model_top1_confidence=0.5,
created_at=datetime.now(timezone.utc),
)
fetcher = AsyncMock(return_value=payload)
hub = VoiceTerminalHub(Settings(), pending_head_fetcher=fetcher)
with patch.object(hub, "_broadcast", new_callable=AsyncMock) as bc:
await hub.notify_pending_head("TERM-1", "123456")
fetcher.assert_awaited_once_with("123456")
sent = bc.await_args[0][1]
assert sent["type"] == "voice_pending"
assert sent["confirmation_id"] == "cid-a"
assert sent["surgery_id"] == "123456"
async def test_voice_terminal_hub_notify_pending_head_empty() -> None:
fetcher = AsyncMock(return_value=None)
hub = VoiceTerminalHub(Settings(), pending_head_fetcher=fetcher)
with patch.object(hub, "_broadcast", new_callable=AsyncMock) as bc:
await hub.notify_pending_head("TERM-1", "123456")
fetcher.assert_awaited_once_with("123456")
assert bc.await_args[0][1] == {
"type": "voice_pending_empty",
"surgery_id": "123456",
}
def test_start_surgery_notifies_voice_terminal_when_binding_matches(
api_app: FastAPI, instant_sleep: None, tmp_path: Path
) -> None:
@@ -265,6 +302,9 @@ def test_pending_confirmation_200_and_404(api_app: FastAPI) -> None:
payload = SurgeryPendingConfirmationResponse(
surgery_id="123456",
confirmation_id="cid",
pending_queue_length=1,
pending_queue_position=1,
pending_cumulative_ordinal=1,
prompt_text="请确认",
prompt_audio_mp3_base64="//uQ",
options=[],
@@ -280,6 +320,9 @@ def test_pending_confirmation_200_and_404(api_app: FastAPI) -> None:
assert r.status_code == 200
body_ok = r.json()
assert body_ok["confirmation_id"] == "cid"
assert body_ok["pending_queue_length"] == 1
assert body_ok["pending_queue_position"] == 1
assert body_ok["pending_cumulative_ordinal"] == 1
assert body_ok["prompt_audio_mp3_base64"] == "//uQ"
pipeline_none = MagicMock()
@@ -333,11 +376,34 @@ def test_resolve_200(api_app: FastAPI) -> None:
)
assert r.status_code == 200
body = r.json()
assert body["status"] == "accepted"
assert body["error_code"] is None
assert body["resolved_label"] == "纱布"
assert body["rejected"] is False
assert body["asr_text"] == "第一个"
def test_resolve_voice_recoverable_error_returns_200_failed(api_app: FastAPI) -> None:
pipeline = MagicMock()
pipeline.resolve_pending_confirmation_from_audio = AsyncMock(
side_effect=SurgeryPipelineError(
"VOICE_ASR_FAILED",
"asr_err_3301: speech quality error.",
)
)
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["status"] == "failed"
assert body["error_code"] == "VOICE_ASR_FAILED"
assert "3301" in body["message"]
def test_resolve_maps_surgery_pipeline_error_to_http(api_app: FastAPI) -> None:
pipeline = MagicMock()
pipeline.resolve_pending_confirmation_from_audio = AsyncMock(