更新
This commit is contained in:
@@ -232,6 +232,69 @@ def test_demo_call_lifecycle_mutates_record(repository: DemoDoctorRepository) ->
|
||||
assert ended["room_id"] == "room-501"
|
||||
|
||||
|
||||
def test_demo_transcript_upsert_and_finish_round_trip_in_call_records(
|
||||
repository: DemoDoctorRepository,
|
||||
) -> None:
|
||||
"""Demo replay reads expose one finalized segment for a repeated segment ID."""
|
||||
|
||||
started = repository.start_call(501, 301)
|
||||
call_record_id = started["id"]
|
||||
repository.start_call_transcription(501, call_record_id, "session-1")
|
||||
repository.upsert_call_transcript_segments(
|
||||
501,
|
||||
call_record_id,
|
||||
"session-1",
|
||||
[
|
||||
{
|
||||
"segment_id": "seg-1",
|
||||
"speaker_user_id": "patient_301",
|
||||
"speaker_role": "patient",
|
||||
"timestamp": 1200,
|
||||
"text": "draft words",
|
||||
}
|
||||
],
|
||||
)
|
||||
repository.upsert_call_transcript_segments(
|
||||
501,
|
||||
call_record_id,
|
||||
"session-1",
|
||||
[
|
||||
{
|
||||
"segment_id": "seg-1",
|
||||
"speaker_user_id": "patient_301",
|
||||
"speaker_role": "patient",
|
||||
"timestamp": 1200,
|
||||
"text": "final words",
|
||||
}
|
||||
],
|
||||
)
|
||||
repository.finish_call_transcription(
|
||||
501,
|
||||
call_record_id,
|
||||
"session-1",
|
||||
expected_segment_count=1,
|
||||
status="completed",
|
||||
)
|
||||
repository.end_call(501)
|
||||
|
||||
record = next(
|
||||
row for row in repository.list_call_records(501) if row["id"] == call_record_id
|
||||
)
|
||||
assert record["status"] == 2
|
||||
assert record["transcription_status"] == "completed"
|
||||
assert record["transcription_segment_count"] == 1
|
||||
assert record["transcript_segments"] == [
|
||||
{
|
||||
"segment_id": "seg-1",
|
||||
"speaker_role": "patient",
|
||||
"speaker_user_id": "patient_301",
|
||||
"timestamp": 1200,
|
||||
"text": "final words",
|
||||
}
|
||||
]
|
||||
assert "final words" in record["transcript_text"]
|
||||
|
||||
|
||||
def test_tolerant_page_parsing_accepts_aliases_and_bad_rows() -> None:
|
||||
"""List parsing handles nullable fields, aliases and non-object rows safely."""
|
||||
|
||||
@@ -315,6 +378,8 @@ class _StubApiClient:
|
||||
"userSig": "short-lived",
|
||||
"patientUserId": "patient_2",
|
||||
}
|
||||
if endpoint == "tcm.diagnosis/startCall":
|
||||
return {"call_record_id": 901}
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from doctor_workstation.core.errors import ApiProtocolError
|
||||
from doctor_workstation.core.models import Appointment, Consultation, PageResult, Prescription
|
||||
from doctor_workstation.services.mock_repository import DemoDoctorRepository
|
||||
from doctor_workstation.services.repository import (
|
||||
@@ -76,6 +77,8 @@ class RecordingClient:
|
||||
self.post_calls.append((endpoint, body))
|
||||
if endpoint in {"tcm.prescription/add", "tcm.prescriptionOrder/create"}:
|
||||
return {"id": 88}
|
||||
if endpoint == "tcm.diagnosis/startCall":
|
||||
return {"call_record_id": 901}
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@@ -253,6 +256,106 @@ def test_remote_new_contracts_use_exact_admin_endpoints_and_dtos() -> None:
|
||||
} <= get_endpoints
|
||||
|
||||
|
||||
def test_remote_transcription_endpoints_use_exact_normalized_dtos() -> None:
|
||||
"""Realtime transcript persistence stays within the three audited POST DTOs."""
|
||||
|
||||
client = RecordingClient()
|
||||
repository = RemoteDoctorRepository(client) # type: ignore[arg-type]
|
||||
|
||||
repository.start_call_transcription(501, 901, " session-1 ", language=" zh-CN ")
|
||||
repository.upsert_call_transcript_segments(
|
||||
501,
|
||||
901,
|
||||
"session-1",
|
||||
[
|
||||
{
|
||||
"segment_id": "seg-1",
|
||||
"speaker_user_id": "patient_301",
|
||||
"speaker_role": "patient",
|
||||
"timestamp": "1200",
|
||||
"text": " patient words ",
|
||||
}
|
||||
],
|
||||
)
|
||||
repository.finish_call_transcription(
|
||||
501,
|
||||
901,
|
||||
"session-1",
|
||||
expected_segment_count=1,
|
||||
status="completed",
|
||||
)
|
||||
|
||||
assert client.post_calls == [
|
||||
(
|
||||
"tcm.diagnosis/startCallTranscription",
|
||||
{
|
||||
"diagnosis_id": 501,
|
||||
"call_record_id": 901,
|
||||
"transcription_session_id": "session-1",
|
||||
"language": "zh-CN",
|
||||
},
|
||||
),
|
||||
(
|
||||
"tcm.diagnosis/upsertCallTranscriptSegments",
|
||||
{
|
||||
"diagnosis_id": 501,
|
||||
"call_record_id": 901,
|
||||
"transcription_session_id": "session-1",
|
||||
"segments": [
|
||||
{
|
||||
"segment_id": "seg-1",
|
||||
"speaker_user_id": "patient_301",
|
||||
"speaker_role": "patient",
|
||||
"timestamp": 1200,
|
||||
"text": "patient words",
|
||||
}
|
||||
],
|
||||
},
|
||||
),
|
||||
(
|
||||
"tcm.diagnosis/finishCallTranscription",
|
||||
{
|
||||
"diagnosis_id": 501,
|
||||
"call_record_id": 901,
|
||||
"transcription_session_id": "session-1",
|
||||
"expected_segment_count": 1,
|
||||
"status": "completed",
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def test_remote_start_call_requires_and_normalizes_current_record_id() -> None:
|
||||
client = RecordingClient()
|
||||
repository = RemoteDoctorRepository(client) # type: ignore[arg-type]
|
||||
|
||||
assert repository.start_call(501, 301) == {"call_record_id": 901}
|
||||
assert client.post_calls == [
|
||||
(
|
||||
"tcm.diagnosis/startCall",
|
||||
{"diagnosis_id": 501, "patient_id": 301, "call_type": 2},
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"response",
|
||||
[None, {}, {"ok": True}, {"call_record_id": 0}, {"callRecordId": -1}, {"id": True}],
|
||||
)
|
||||
def test_remote_start_call_rejects_missing_or_invalid_record_id(response: Any) -> None:
|
||||
class StartCallClient(RecordingClient):
|
||||
def post(self, endpoint: str, payload: dict[str, Any] | None = None) -> Any:
|
||||
if endpoint == "tcm.diagnosis/startCall":
|
||||
self.post_calls.append((endpoint, dict(payload or {})))
|
||||
return response
|
||||
return super().post(endpoint, payload)
|
||||
|
||||
repository = RemoteDoctorRepository(StartCallClient()) # type: ignore[arg-type]
|
||||
|
||||
with pytest.raises(ApiProtocolError, match="call_record"):
|
||||
repository.start_call(501, 301)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"unsafe_reference",
|
||||
[r"C:\records\tongue.jpg", r"\\server\share\report.pdf", "file:///tmp/a.jpg"],
|
||||
|
||||
@@ -188,10 +188,13 @@ def test_call_lifecycle_is_fifo_daemon_and_never_blocks_caller() -> None:
|
||||
release_start = threading.Event()
|
||||
|
||||
class Repository:
|
||||
def start_call(self, diagnosis_id: int, patient_id: int, *, call_type: int) -> None:
|
||||
def start_call(
|
||||
self, diagnosis_id: int, patient_id: int, *, call_type: int
|
||||
) -> dict[str, int]:
|
||||
start_entered.set()
|
||||
assert release_start.wait(2)
|
||||
events.append(("start", diagnosis_id, patient_id, call_type))
|
||||
return {"call_record_id": 900}
|
||||
|
||||
def bind_call_room(self, diagnosis_id: int, room_id: str) -> None:
|
||||
events.append(("bind", diagnosis_id, room_id))
|
||||
@@ -237,6 +240,208 @@ def test_call_lifecycle_is_fifo_daemon_and_never_blocks_caller() -> None:
|
||||
]
|
||||
|
||||
|
||||
def test_transcription_lifecycle_uses_start_record_id_and_remains_fifo() -> None:
|
||||
events: list[tuple[object, ...]] = []
|
||||
|
||||
class Repository:
|
||||
def start_call(
|
||||
self, diagnosis_id: int, patient_id: int, *, call_type: int
|
||||
) -> dict[str, object]:
|
||||
events.append(("start", diagnosis_id, patient_id, call_type))
|
||||
return {"data": {"callRecordId": 901}}
|
||||
|
||||
def start_call_transcription(
|
||||
self,
|
||||
diagnosis_id: int,
|
||||
call_record_id: int,
|
||||
transcription_session_id: str,
|
||||
*,
|
||||
language: str,
|
||||
) -> None:
|
||||
events.append(
|
||||
(
|
||||
"transcription-start",
|
||||
diagnosis_id,
|
||||
call_record_id,
|
||||
transcription_session_id,
|
||||
language,
|
||||
)
|
||||
)
|
||||
|
||||
def upsert_call_transcript_segments(
|
||||
self,
|
||||
diagnosis_id: int,
|
||||
call_record_id: int,
|
||||
transcription_session_id: str,
|
||||
segments: list[dict[str, object]],
|
||||
) -> None:
|
||||
events.append(
|
||||
(
|
||||
"segment",
|
||||
diagnosis_id,
|
||||
call_record_id,
|
||||
transcription_session_id,
|
||||
segments,
|
||||
)
|
||||
)
|
||||
|
||||
def finish_call_transcription(
|
||||
self,
|
||||
diagnosis_id: int,
|
||||
call_record_id: int,
|
||||
transcription_session_id: str,
|
||||
expected_segment_count: int,
|
||||
*,
|
||||
status: str,
|
||||
) -> None:
|
||||
events.append(
|
||||
(
|
||||
"finish",
|
||||
diagnosis_id,
|
||||
call_record_id,
|
||||
transcription_session_id,
|
||||
expected_segment_count,
|
||||
status,
|
||||
)
|
||||
)
|
||||
|
||||
def end_call(self, diagnosis_id: int) -> None:
|
||||
events.append(("end", diagnosis_id))
|
||||
|
||||
request = VideoCallRequest(
|
||||
sdk_app_id=1400123456,
|
||||
user_id="doctor_42",
|
||||
user_sig="short-lived-ticket",
|
||||
target_user_id="patient_8",
|
||||
diagnosis_id=123,
|
||||
patient_id=8,
|
||||
)
|
||||
lifecycle = OrderedCallLifecycle(request, Repository(), logging.getLogger(__name__))
|
||||
|
||||
start = lifecycle.start()
|
||||
transcription_start = lifecycle.start_transcription("session-1")
|
||||
segment = lifecycle.save_transcript_segment(
|
||||
{
|
||||
"segment_id": "seg-1",
|
||||
"speaker_user_id": "patient_8",
|
||||
"speaker_role": "patient",
|
||||
"timestamp": 1200,
|
||||
"text": "patient words",
|
||||
}
|
||||
)
|
||||
duplicate = lifecycle.save_transcript_segment(
|
||||
{"segment_id": "seg-1", "text": "must not produce another write"}
|
||||
)
|
||||
finish = lifecycle.finish_transcription(status="completed")
|
||||
end = lifecycle.end("test")
|
||||
|
||||
assert duplicate is segment
|
||||
assert start.result(timeout=2) is True
|
||||
assert transcription_start.result(timeout=2) is True
|
||||
assert segment.result(timeout=2) is True
|
||||
assert finish.result(timeout=2) is True
|
||||
assert end.result(timeout=2) is True
|
||||
assert lifecycle.wait(1) is True
|
||||
assert lifecycle.call_record_id == 901
|
||||
assert events == [
|
||||
("start", 123, 8, 2),
|
||||
("transcription-start", 123, 901, "session-1", "zh-CN"),
|
||||
(
|
||||
"segment",
|
||||
123,
|
||||
901,
|
||||
"session-1",
|
||||
[
|
||||
{
|
||||
"segment_id": "seg-1",
|
||||
"transcription_session_id": "session-1",
|
||||
"speaker_user_id": "patient_8",
|
||||
"speaker_role": "patient",
|
||||
"timestamp": 1200,
|
||||
"text": "patient words",
|
||||
}
|
||||
],
|
||||
),
|
||||
("finish", 123, 901, "session-1", 1, "completed"),
|
||||
("end", 123),
|
||||
]
|
||||
|
||||
|
||||
def test_end_auto_finishes_active_transcription_as_partial() -> None:
|
||||
events: list[tuple[object, ...]] = []
|
||||
|
||||
class Repository:
|
||||
def start_call(
|
||||
self, diagnosis_id: int, patient_id: int, *, call_type: int
|
||||
) -> dict[str, int]:
|
||||
events.append(("start", diagnosis_id, patient_id, call_type))
|
||||
return {"call_record_id": 902}
|
||||
|
||||
def start_call_transcription(
|
||||
self,
|
||||
diagnosis_id: int,
|
||||
call_record_id: int,
|
||||
transcription_session_id: str,
|
||||
*,
|
||||
language: str,
|
||||
) -> None:
|
||||
events.append(
|
||||
(
|
||||
"transcription-start",
|
||||
diagnosis_id,
|
||||
call_record_id,
|
||||
transcription_session_id,
|
||||
language,
|
||||
)
|
||||
)
|
||||
|
||||
def finish_call_transcription(
|
||||
self,
|
||||
diagnosis_id: int,
|
||||
call_record_id: int,
|
||||
transcription_session_id: str,
|
||||
expected_segment_count: int,
|
||||
*,
|
||||
status: str,
|
||||
) -> None:
|
||||
events.append(
|
||||
(
|
||||
"finish",
|
||||
diagnosis_id,
|
||||
call_record_id,
|
||||
transcription_session_id,
|
||||
expected_segment_count,
|
||||
status,
|
||||
)
|
||||
)
|
||||
|
||||
def end_call(self, diagnosis_id: int) -> None:
|
||||
events.append(("end", diagnosis_id))
|
||||
|
||||
request = VideoCallRequest(
|
||||
sdk_app_id=1400123456,
|
||||
user_id="doctor_42",
|
||||
user_sig="short-lived-ticket",
|
||||
target_user_id="patient_8",
|
||||
diagnosis_id=123,
|
||||
patient_id=8,
|
||||
)
|
||||
lifecycle = OrderedCallLifecycle(request, Repository(), logging.getLogger(__name__))
|
||||
|
||||
lifecycle.start()
|
||||
lifecycle.start_transcription("session-auto-partial")
|
||||
ended = lifecycle.end("window-closed")
|
||||
|
||||
assert ended.result(timeout=2) is True
|
||||
assert lifecycle.wait(1) is True
|
||||
assert events == [
|
||||
("start", 123, 8, 2),
|
||||
("transcription-start", 123, 902, "session-auto-partial", "zh-CN"),
|
||||
("finish", 123, 902, "session-auto-partial", 0, "partial"),
|
||||
("end", 123),
|
||||
]
|
||||
|
||||
|
||||
def test_failed_start_prevents_bind_and_end_writes() -> None:
|
||||
events: list[str] = []
|
||||
|
||||
@@ -278,8 +483,9 @@ def test_video_screenshot_is_uploaded_and_appended_to_patient_tongue_images() ->
|
||||
events: list[tuple[object, ...]] = []
|
||||
|
||||
class Repository:
|
||||
def start_call(self, diagnosis_id: int, *, call_type: int) -> None:
|
||||
def start_call(self, diagnosis_id: int, *, call_type: int) -> dict[str, int]:
|
||||
events.append(("start", diagnosis_id, call_type))
|
||||
return {"call_record_id": 903}
|
||||
|
||||
def upload_material_bytes(
|
||||
self,
|
||||
@@ -325,6 +531,28 @@ def test_video_screenshot_is_uploaded_and_appended_to_patient_tongue_images() ->
|
||||
]
|
||||
|
||||
|
||||
def test_start_rejects_missing_record_id_without_using_ticket_fallback() -> None:
|
||||
class Repository:
|
||||
def start_call(self, diagnosis_id: int, *, call_type: int) -> dict[str, object]:
|
||||
del diagnosis_id, call_type
|
||||
return {}
|
||||
|
||||
request = VideoCallRequest(
|
||||
sdk_app_id=1400123456,
|
||||
user_id="doctor_42",
|
||||
user_sig="short-lived-ticket",
|
||||
target_user_id="patient_8",
|
||||
diagnosis_id=123,
|
||||
call_record_id=77,
|
||||
)
|
||||
lifecycle = OrderedCallLifecycle(request, Repository(), logging.getLogger(__name__))
|
||||
|
||||
with pytest.raises(ValueError, match="startCall response did not include"):
|
||||
lifecycle.start().result(timeout=2)
|
||||
|
||||
assert lifecycle.started is False
|
||||
|
||||
|
||||
def test_https_document_policy_is_exact_and_origin_scoped() -> None:
|
||||
policy = TrustedDocumentPolicy.from_url(
|
||||
"https://RTC.Example.com/doctor-call/index.html?tenant=a#boot",
|
||||
|
||||
Reference in New Issue
Block a user