This commit is contained in:
Your Name
2026-08-11 10:02:40 +08:00
parent cfe4c82c90
commit de990a921b
25 changed files with 1412 additions and 174 deletions
+165 -3
View File
@@ -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"),
]
+8 -3
View File
@@ -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()
+76
View File
@@ -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."""
+54
View File
@@ -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(
{