Files
zyt/app/tests/test_local_video_bridge.py
2026-10-30 13:41:35 +08:00

136 lines
6.5 KiB
Python

"""Trusted native PCM ingestion and exact transcript/AI identity handoff."""
from __future__ import annotations
import json
from types import SimpleNamespace
from unittest.mock import Mock
import pytest
from doctor_workstation.video import window
from doctor_workstation.video.security import TrustedDocumentPolicy
pytestmark = pytest.mark.skipif(not window.WEBENGINE_AVAILABLE, reason="optional QtWebEngine")
TRUSTED = "https://video.example.test/call/index.html"
def host(**changes):
item = SimpleNamespace(
_closing=False, _released=False, _shutdown_requested=False, _injected=True,
_start_requested=True, _call_cycle_closed=False,
_policy=TrustedDocumentPolicy.from_url(TRUSTED, is_local=False),
_page=Mock(), _local_transcription=Mock(), _live_ai=Mock(),
_video_ai_timer=Mock(), _asr_session_id="tr-local", _asr_ready=True,
_asr_partial=False, _transcription_final_status="completed",
lifecycle=SimpleNamespace(call_record_id=71, transcription_session_id="tr-local"),
request=SimpleNamespace(user_id="doctor_1", target_user_id="patient_9"),
_save_transcript_segment=Mock(),
)
item._page.url.return_value.toString.return_value = TRUSTED
for key, value in changes.items():
setattr(item, key, value)
item._local_transcription_context_is_trusted = lambda: window._EmbeddedVideoWindow._local_transcription_context_is_trusted(item)
item._on_local_transcription = lambda event: window._EmbeddedVideoWindow._on_local_transcription(item, event)
return item
def chunk(**changes):
return {"event": "local-transcription-chunk", "sessionId": "tr-local", "role": "patient",
"sequence": 3, "timestamp": 1793332800000.0, "data": "AQACAA==", **changes}
def test_pcm_uses_bound_session_and_double_timestamp_is_converted_safely():
item = host()
window._EmbeddedVideoWindow._handle_video_assistance_message(item, chunk())
item._local_transcription.submit.assert_called_once_with("tr-local", "patient", 3, 1793332800000, "AQACAA==")
@pytest.mark.parametrize("changes", [{"sessionId": "other"}, {"timestamp": float("nan")},
{"timestamp": float("inf")}, {"timestamp": -1}, {"timestamp": True}])
def test_wrong_session_or_unbounded_pcm_metadata_rejected(changes):
item = host()
window._EmbeddedVideoWindow._handle_video_assistance_message(item, chunk(**changes))
item._local_transcription.submit.assert_not_called()
@pytest.mark.parametrize("changes", [{"_closing": True}, {"_released": True}, {"_injected": False}])
def test_untrusted_lifecycle_cannot_ingest_audio(changes):
item = host(**changes)
window._EmbeddedVideoWindow._handle_video_assistance_message(item, chunk())
item._local_transcription.submit.assert_not_called()
def test_navigation_blocks_pcm_and_analysis_commands():
item = host()
item._page.url.return_value.toString.return_value = "https://another.example.test/"
window._EmbeddedVideoWindow._handle_video_assistance_message(item, chunk())
window._EmbeddedVideoWindow._handle_video_assistance_message(item, {"event": "video-ai-action", "action": "refresh"})
item._local_transcription.submit.assert_not_called()
item._live_ai.handle.assert_not_called()
def test_pending_shutdown_drains_pcm_but_cannot_start_new_inference_session():
item = host(_shutdown_requested=True)
window._EmbeddedVideoWindow._handle_video_assistance_message(item, chunk())
window._EmbeddedVideoWindow._handle_video_assistance_message(item, {"event": "local-transcription-start", "sessionId": "tr-next"})
window._EmbeddedVideoWindow._handle_video_assistance_message(item, {"event": "video-ai-action", "action": "refresh"})
item._local_transcription.submit.assert_called_once()
item._local_transcription.start.assert_not_called()
item._live_ai.handle.assert_not_called()
def test_asr_segment_is_persisted_with_host_speaker_before_json_display():
item = host()
event = {"type": "segment", "sessionId": "tr-local", "role": "patient", "segmentId": "tr-local:patient:3",
"text": '没有;0.5 mg,不是 5 mg。</script>\u2028', "timestamp": 1793332800000, "isCompleted": True}
item._on_local_transcription(event)
item._save_transcript_segment.assert_called_once()
saved = item._save_transcript_segment.call_args.args[0]
assert saved["segment"]["text"] == event["text"]
assert saved["segment"]["speaker_user_id"] == "patient_9"
script = item._page.runJavaScript.call_args.args[0]
assert json.loads(script.split("?.(", 1)[1][:-2]) == event
item._live_ai.changed.assert_not_called() # waits for persistence ACK
def test_late_asr_segment_cannot_enter_new_call():
item = host(_asr_session_id="tr-next")
item._on_local_transcription({"type": "segment", "sessionId": "tr-local", "role": "patient"})
item._save_transcript_segment.assert_not_called()
item._page.runJavaScript.assert_not_called()
def test_persistence_ack_is_the_only_ai_dirty_trigger():
item = host()
window._EmbeddedVideoWindow._on_transcription_completed(item, "segment", "other", "s1", True, "")
item._live_ai.changed.assert_not_called()
window._EmbeddedVideoWindow._on_transcription_completed(item, "segment", "tr-local", "s1", True, "")
item._live_ai.changed.assert_called_once_with("tr-local")
window._EmbeddedVideoWindow._on_transcription_completed(item, "segment", "tr-local", "s2", False, "")
assert item._asr_partial
assert item._live_ai.changed.call_count == 1
def test_model_missing_is_failed_and_partial_chunk_is_not_false_ready():
item = host(_asr_ready=False)
item._on_local_transcription({"type": "error", "sessionId": "tr-local", "message": "本地模型未安装"})
item._live_ai.transcription_error.assert_called_once_with("本地模型未安装", status="failed")
def test_shutdown_keeps_asr_for_drain_then_closes_it():
item = host()
window._EmbeddedVideoWindow._close_video_assistance(item, close_asr=False)
item._live_ai.close.assert_called_once()
item._local_transcription.close.assert_not_called()
window._EmbeddedVideoWindow._close_video_assistance(item)
item._local_transcription.close.assert_called_once()
def test_special_pcm_bridge_bounds_data_and_preserves_epoch():
callback = Mock()
bridge = window._QtVideoBridge(callback, Mock())
bridge.appendLocalTranscriptionChunk("tr-local", "doctor", 0, 1793332800000.0, "x" * 512001)
callback.assert_not_called()
bridge.appendLocalTranscriptionChunk("tr-local", "doctor", 0, 1793332800000.0, "AQACAA==")
assert callback.call_args.args[0]["timestamp"] == 1793332800000.0