更新
This commit is contained in:
@@ -12,6 +12,7 @@ from PySide6.QtWidgets import (
|
||||
QApplication,
|
||||
QDialog,
|
||||
QInputDialog,
|
||||
QLabel,
|
||||
QMessageBox,
|
||||
QToolButton,
|
||||
QWidget,
|
||||
@@ -20,6 +21,7 @@ from PySide6.QtWidgets import (
|
||||
from doctor_workstation.core import PermissionSet
|
||||
from doctor_workstation.ui.dialogs import diagnosis as diagnosis_module
|
||||
from doctor_workstation.ui.pages import consultations as consultations_module
|
||||
from doctor_workstation.ui.pages import patients as patients_module
|
||||
from doctor_workstation.ui.pages.consultations import (
|
||||
ConsultationsPage,
|
||||
_video_payload,
|
||||
@@ -117,6 +119,166 @@ def test_video_condition_never_uses_diagnosis_status_or_missed_status() -> None:
|
||||
assert payload["patient_id"] == 301
|
||||
|
||||
|
||||
def test_create_and_order_dialogs_use_production_diagnosis_contract(
|
||||
application: QApplication,
|
||||
) -> None:
|
||||
create_dialog = consultations_module._DiagnosisCreateDialog()
|
||||
assert create_dialog.gender.itemData(create_dialog.gender.findText("女")) == 0
|
||||
create_dialog.patient_name.setText("林晓岚")
|
||||
create_dialog.phone.setText("13800138000")
|
||||
create_dialog.gender.setCurrentIndex(create_dialog.gender.findData(0))
|
||||
create_dialog.diagnosis_type.setText("复诊")
|
||||
create_dialog.local_hospital_name.setText("杭州市第一人民医院")
|
||||
create_dialog.local_hospital_diagnosis.setText("2型糖尿病")
|
||||
payload = create_dialog.payload()
|
||||
assert payload["gender"] == 0
|
||||
assert payload["local_hospital_name"] == "杭州市第一人民医院"
|
||||
create_dialog.close()
|
||||
|
||||
order_dialog = consultations_module._DiagnosisOrderDialog(_row(), None)
|
||||
order_dialog.order_type.setCurrentIndex(order_dialog.order_type.findData(2))
|
||||
order_dialog.amount.setValue(88.5)
|
||||
assert order_dialog.payload()["patient_id"] == 501
|
||||
order_dialog.close()
|
||||
application.processEvents()
|
||||
|
||||
|
||||
def test_diagnosis_appointment_uses_doctor_route_capability_and_diagnosis_owner(
|
||||
application: QApplication,
|
||||
immediate_async: None,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
calls: list[dict[str, Any]] = []
|
||||
|
||||
class Repository:
|
||||
def create_diagnosis_appointment(
|
||||
self, payload: dict[str, Any] | None = None, **fields: Any
|
||||
) -> dict[str, Any]:
|
||||
calls.append(dict(payload or fields))
|
||||
return {"ok": True}
|
||||
|
||||
class AcceptedAppointmentDialog:
|
||||
def __init__(self, *_args: Any, **_kwargs: Any) -> None:
|
||||
pass
|
||||
|
||||
def exec(self) -> Any:
|
||||
return QDialog.DialogCode.Accepted
|
||||
|
||||
def payload(self) -> dict[str, Any]:
|
||||
return {
|
||||
"diagnosis_id": 501,
|
||||
"patient_id": 301,
|
||||
"doctor_id": 77,
|
||||
"appointment_date": "2026-08-12",
|
||||
"appointment_time": "09:00-09:30",
|
||||
}
|
||||
|
||||
page = ConsultationsPage(
|
||||
Repository(),
|
||||
permissions=PermissionSet(["tcm.diagnosis/guahao"]),
|
||||
)
|
||||
page.table.set_rows([_row()])
|
||||
page.table.selectRow(0)
|
||||
monkeypatch.setattr(patients_module, "_AppointmentDialog", AcceptedAppointmentDialog)
|
||||
monkeypatch.setattr(page, "refresh", lambda *_args, **_kwargs: None)
|
||||
|
||||
page._book_selected_appointment()
|
||||
|
||||
assert calls == [
|
||||
{
|
||||
"diagnosis_id": 501,
|
||||
"patient_id": 501,
|
||||
"doctor_id": 77,
|
||||
"appointment_date": "2026-08-12",
|
||||
"appointment_time": "09:00-09:30",
|
||||
}
|
||||
]
|
||||
page.close()
|
||||
application.processEvents()
|
||||
|
||||
|
||||
def test_assigned_assistant_can_enter_receive_only_live_watch(
|
||||
application: QApplication,
|
||||
) -> None:
|
||||
class Repository:
|
||||
def get_assistant_watch_ticket(self, diagnosis_id: int) -> dict[str, Any]:
|
||||
return {"diagnosis_id": diagnosis_id}
|
||||
|
||||
page = ConsultationsPage(
|
||||
Repository(),
|
||||
permissions=PermissionSet(["tcm.diagnosis/watchCall"]),
|
||||
current_user={"id": 2001, "name": "周医助"},
|
||||
)
|
||||
row = _row(
|
||||
assistant_id=2001,
|
||||
video_call_hint={"state": "live", "label": "通话中", "room_id": 9001},
|
||||
)
|
||||
page.table.set_rows([row])
|
||||
page.table.selectRow(0)
|
||||
requested: list[dict[str, Any]] = []
|
||||
page.watch_requested.connect(requested.append)
|
||||
|
||||
watch_cell = page.table_host.fixed.indexWidget(page.table_host.model.index(0, 10))
|
||||
assert watch_cell is not None
|
||||
watch_buttons = [
|
||||
button
|
||||
for button in watch_cell.findChildren(QToolButton)
|
||||
if button.text() == "进入旁观"
|
||||
]
|
||||
assert len(watch_buttons) == 1
|
||||
assert watch_buttons[0].isEnabled()
|
||||
assert "不会开启摄像头与麦克风" in watch_buttons[0].toolTip()
|
||||
watch_buttons[0].click()
|
||||
|
||||
assert requested == [{"diagnosis_id": 501, "patient_name": "林晓岚"}]
|
||||
page.close()
|
||||
application.processEvents()
|
||||
|
||||
|
||||
def test_watch_entry_is_disabled_until_room_is_live_and_hidden_from_other_assistants(
|
||||
application: QApplication,
|
||||
) -> None:
|
||||
class Repository:
|
||||
def get_assistant_watch_ticket(self, diagnosis_id: int) -> dict[str, Any]:
|
||||
return {"diagnosis_id": diagnosis_id}
|
||||
|
||||
pending = ConsultationsPage(
|
||||
Repository(),
|
||||
permissions=PermissionSet(["tcm.diagnosis/watchCall"]),
|
||||
current_user={"id": 2001},
|
||||
)
|
||||
pending.table.set_rows(
|
||||
[
|
||||
_row(
|
||||
assistant_id=2001,
|
||||
video_call_hint={"state": "pending_room", "label": "接通中"},
|
||||
)
|
||||
]
|
||||
)
|
||||
pending_cell = pending.table_host.fixed.indexWidget(pending.table_host.model.index(0, 10))
|
||||
pending_button = next(
|
||||
button
|
||||
for button in pending_cell.findChildren(QToolButton)
|
||||
if button.text() == "进入旁观"
|
||||
)
|
||||
assert not pending_button.isEnabled()
|
||||
pending.close()
|
||||
|
||||
other = ConsultationsPage(
|
||||
Repository(),
|
||||
permissions=PermissionSet(["tcm.diagnosis/watchCall"]),
|
||||
current_user={"id": 2002},
|
||||
)
|
||||
other.table.set_rows(
|
||||
[_row(assistant_id=2001, video_call_hint={"state": "live", "label": "通话中"})]
|
||||
)
|
||||
other_cell = other.table_host.fixed.indexWidget(other.table_host.model.index(0, 10))
|
||||
assert all(button.text() != "进入旁观" for button in other_cell.findChildren(QToolButton))
|
||||
assert any(label.text() == "通话中" for label in other_cell.findChildren(QLabel))
|
||||
other.close()
|
||||
application.processEvents()
|
||||
|
||||
|
||||
def test_nested_appointments_confirmation_and_prescription_labels() -> None:
|
||||
row = _row(
|
||||
appointment_id=None,
|
||||
@@ -914,7 +1076,7 @@ def test_menu_handlers_call_only_permission_gated_real_repository_methods(
|
||||
("logs", 501),
|
||||
(
|
||||
"order",
|
||||
{"patient_id": 301, "order_type": 2, "amount": 88.5, "remark": "复诊"},
|
||||
{"patient_id": 501, "order_type": 2, "amount": 88.5, "remark": "复诊"},
|
||||
),
|
||||
("order_qr", "O-1"),
|
||||
]
|
||||
@@ -1065,14 +1227,14 @@ def test_diagnosis_order_qr_failure_retries_without_creating_a_second_order(
|
||||
assert dialog.retry_button.isEnabled()
|
||||
assert "生成失败" in dialog.status_label.text()
|
||||
assert calls == [
|
||||
("create", (301, 2, 88.5, "复诊")),
|
||||
("create", (501, 2, 88.5, "复诊")),
|
||||
("qr", "PAY-RETRY-1"),
|
||||
]
|
||||
|
||||
dialog.retry_button.click()
|
||||
|
||||
assert calls == [
|
||||
("create", (301, 2, 88.5, "复诊")),
|
||||
("create", (501, 2, 88.5, "复诊")),
|
||||
("qr", "PAY-RETRY-1"),
|
||||
("qr", "PAY-RETRY-1"),
|
||||
]
|
||||
|
||||
@@ -36,6 +36,9 @@ class _CancellationRepository:
|
||||
def cancel_diagnosis_appointment(self, appointment_id: int) -> None:
|
||||
del appointment_id
|
||||
|
||||
def create_diagnosis_appointment(self, payload: Any = None, **fields: Any) -> None:
|
||||
del payload, fields
|
||||
|
||||
|
||||
class _FullMenuRepository(_CancellationRepository):
|
||||
def generate_video_qrcode(
|
||||
@@ -466,9 +469,11 @@ def test_full_more_menu_requires_each_real_repository_capability(
|
||||
"appointment_cancel": True,
|
||||
"video_qr": True,
|
||||
"confirm_qr": True,
|
||||
"appointment_logs": True,
|
||||
"create_order": True,
|
||||
}
|
||||
"appointment_logs": True,
|
||||
"create_order": True,
|
||||
"watch_call": False,
|
||||
"watch_user_id": 0,
|
||||
}
|
||||
page.close()
|
||||
application.processEvents()
|
||||
|
||||
|
||||
@@ -65,6 +65,13 @@ class RecordingClient:
|
||||
return {}
|
||||
if endpoint == "doctor.appointment/availableSlots":
|
||||
return {"slots": [{"time": "09:00", "available": True}]}
|
||||
if endpoint == "tcm.diagnosis/watchCall":
|
||||
return {
|
||||
"sdkAppId": 1400123456,
|
||||
"userId": "doctor_20",
|
||||
"userSig": "short-lived",
|
||||
"roomId": 9001,
|
||||
}
|
||||
if endpoint == "tcm.prescriptionOrder/paidPayOrders":
|
||||
return {"lists": [{"id": 9}], "deposit_min_amount": 50}
|
||||
return {"lists": [], "count": 0, "extend": {"scope": {"label": "server"}}}
|
||||
@@ -200,7 +207,26 @@ def test_remote_new_contracts_use_exact_admin_endpoints_and_dtos() -> None:
|
||||
repository.assign_patient(5, 20, is_inherit=1)
|
||||
repository.fill_patient_id_card(5, "410000199001010000")
|
||||
repository.book_patient_appointment({"diagnosis_id": 5, "appointment_date": "2026-08-10"})
|
||||
repository.create_diagnosis_appointment(
|
||||
{
|
||||
"diagnosis_id": 5,
|
||||
"patient_id": 5,
|
||||
"doctor_id": 9,
|
||||
"appointment_date": "2026-08-11",
|
||||
}
|
||||
)
|
||||
repository.cancel_patient_appointment(7)
|
||||
watch_ticket = repository.get_assistant_watch_ticket(5)
|
||||
repository.create_diagnosis(
|
||||
{
|
||||
"patient_name": " 林晓岚 ",
|
||||
"phone": "13800138000",
|
||||
"gender": 0,
|
||||
"age": 36,
|
||||
"diagnosis_type": "复诊",
|
||||
"local_hospital_name": "杭州市第一人民医院",
|
||||
}
|
||||
)
|
||||
repository.update_diagnosis(5, {"clinical_diagnosis": "气虚证"})
|
||||
repository.list_appointment_rosters(
|
||||
doctor_id=1,
|
||||
@@ -238,7 +264,28 @@ def test_remote_new_contracts_use_exact_admin_endpoints_and_dtos() -> None:
|
||||
},
|
||||
) in client.post_calls
|
||||
assert ("tcm.diagnosis/edit", {"id": 5, "clinical_diagnosis": "气虚证"}) in client.post_calls
|
||||
assert (
|
||||
"doctor.appointment/create",
|
||||
{
|
||||
"diagnosis_id": 5,
|
||||
"patient_id": 5,
|
||||
"doctor_id": 9,
|
||||
"appointment_date": "2026-08-11",
|
||||
},
|
||||
) in client.post_calls
|
||||
assert (
|
||||
"tcm.diagnosis/add",
|
||||
{
|
||||
"patient_name": "林晓岚",
|
||||
"phone": "13800138000",
|
||||
"gender": 0,
|
||||
"age": 36,
|
||||
"diagnosis_type": "复诊",
|
||||
"local_hospital_name": "杭州市第一人民医院",
|
||||
},
|
||||
) in client.post_calls
|
||||
assert slots == {"slots": [{"time": "09:00", "available": True}]}
|
||||
assert watch_ticket["roomId"] == 9001
|
||||
get_endpoints = {endpoint for endpoint, _ in client.get_calls}
|
||||
assert {
|
||||
"tcm.prescriptionOrder/paidPayOrders",
|
||||
@@ -250,6 +297,7 @@ def test_remote_new_contracts_use_exact_admin_endpoints_and_dtos() -> None:
|
||||
"tcm.diagnosis/assignLogList",
|
||||
"doctor.roster/lists",
|
||||
"doctor.appointment/availableSlots",
|
||||
"tcm.diagnosis/watchCall",
|
||||
} <= get_endpoints
|
||||
|
||||
|
||||
@@ -270,6 +318,34 @@ def test_remote_note_rejects_local_material_references(unsafe_reference: str) ->
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("changes", "message"),
|
||||
[
|
||||
({"gender": 2}, "gender"),
|
||||
({"local_hospital_name": ""}, "local_hospital_name"),
|
||||
],
|
||||
)
|
||||
def test_remote_diagnosis_create_rejects_invalid_production_dto_before_transport(
|
||||
changes: dict[str, Any], message: str
|
||||
) -> None:
|
||||
client = RecordingClient()
|
||||
repository = RemoteDoctorRepository(client) # type: ignore[arg-type]
|
||||
payload = {
|
||||
"patient_name": "林晓岚",
|
||||
"phone": "13800138000",
|
||||
"gender": 0,
|
||||
"age": 36,
|
||||
"diagnosis_type": "复诊",
|
||||
"local_hospital_name": "杭州市第一人民医院",
|
||||
**changes,
|
||||
}
|
||||
|
||||
with pytest.raises(ValueError, match=message):
|
||||
repository.create_diagnosis(payload)
|
||||
|
||||
assert not client.post_calls
|
||||
|
||||
|
||||
def test_remote_dynamic_menu_preserves_json_metadata_but_drops_runtime_objects() -> None:
|
||||
"""Future menu fields pass through safely without evaluating arbitrary values."""
|
||||
|
||||
|
||||
@@ -19,7 +19,9 @@ from doctor_workstation.video.launcher import ( # noqa: E402
|
||||
VideoCallLauncher,
|
||||
VideoCallRequest,
|
||||
VideoTicketError,
|
||||
VideoWatchRequest,
|
||||
normalize_backend_ticket,
|
||||
normalize_backend_watch_ticket,
|
||||
)
|
||||
from doctor_workstation.video.lifecycle import OrderedCallLifecycle # noqa: E402
|
||||
from doctor_workstation.video.security import ( # noqa: E402
|
||||
@@ -57,6 +59,58 @@ def test_normalizes_admin_ticket_aliases_to_companion_contract() -> None:
|
||||
}
|
||||
|
||||
|
||||
def test_normalizes_receive_only_assistant_watch_ticket() -> None:
|
||||
request = normalize_backend_watch_ticket(
|
||||
{
|
||||
"sdkAppId": 1400123456,
|
||||
"userId": "doctor_20",
|
||||
"userSig": "short-lived-watch-ticket",
|
||||
"strRoomId": " diagnosis-room-501 ",
|
||||
"patientName": "林晓岚",
|
||||
},
|
||||
diagnosis_id=501,
|
||||
)
|
||||
|
||||
assert request == VideoWatchRequest(
|
||||
sdk_app_id=1400123456,
|
||||
user_id="doctor_20",
|
||||
user_sig="short-lived-watch-ticket",
|
||||
diagnosis_id=501,
|
||||
str_room_id="diagnosis-room-501",
|
||||
patient_name="林晓岚",
|
||||
)
|
||||
assert request.to_web_config() == {
|
||||
"SDKAppID": 1400123456,
|
||||
"userID": "doctor_20",
|
||||
"userSig": "short-lived-watch-ticket",
|
||||
"diagnosisId": 501,
|
||||
"patientName": "林晓岚",
|
||||
"strRoomId": "diagnosis-room-501",
|
||||
}
|
||||
assert "short-lived-watch-ticket" not in repr(request)
|
||||
assert "short-lived-watch-ticket" not in str(request.safe_log_context())
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"room_fields",
|
||||
[
|
||||
{},
|
||||
{"roomId": 9001, "strRoomId": "room-9001"},
|
||||
],
|
||||
)
|
||||
def test_rejects_missing_or_conflicting_watch_room(room_fields: dict[str, object]) -> None:
|
||||
with pytest.raises(VideoTicketError, match="room"):
|
||||
normalize_backend_watch_ticket(
|
||||
{
|
||||
"sdkAppId": 1400123456,
|
||||
"userId": "doctor_20",
|
||||
"userSig": "short-lived-watch-ticket",
|
||||
**room_fields,
|
||||
},
|
||||
diagnosis_id=501,
|
||||
)
|
||||
|
||||
|
||||
def test_accepts_uppercase_aliases_and_nested_backend_envelope() -> None:
|
||||
request = VideoCallRequest.from_backend_ticket(
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user