"""Dual-model report and API contracts, exercised offscreen without network access.""" from __future__ import annotations import os import socket from copy import deepcopy from typing import Any from uuid import UUID os.environ.setdefault("QT_QPA_PLATFORM", "offscreen") import pytest from PySide6.QtCore import Qt from PySide6.QtWidgets import QApplication, QPushButton from doctor_workstation.services import DemoDoctorRepository from doctor_workstation.services.repository import RemoteDoctorRepository from doctor_workstation.ui.dialogs import issued_prescription_ai as ai from doctor_workstation.ui.dialogs import prescription as editor_module from doctor_workstation.ui.pages import prescriptions as page_module def batch(batch_id: int = 40, prescription_id: int = 801, status: str = "running") -> dict[str, Any]: return { "id": batch_id, "prescription_id": prescription_id, "prescription_revision": 2, "patient_id": 301, "diagnosis_id": 501, "status": status, "validity": "current", "comparison_type": "independent_baseline", "source_summary": {"diagnosis_count": 2, "attachment_count": 4}, "missing": [{"source": "tongue", "reason": "第4张图片不可读"}], "cutoff_at": "2026-09-09 10:00:00", "created_at": "2026-09-09 10:01:00", "models": { "qwen": { "status": "succeeded", "report": {"summary": "已保存千问分析 "}, "candidate": {"status": "available_for_review", "herbs": [{"name": "黄芪", "dosage": 12, "unit": "g", "dose_basis": "每剂", "formula_type": "主方"}]}, "comparison": {"status": "comparable", "score": 0, "herb_score": 0, "algorithm_version": "v1", "rows": [{"name": "黄芪", "doctor_dose": None, "candidate_dose": 12, "contribution": 0}], "usage_differences": ["疗程需复核"]}, "coverage": {"status": "incomplete", "manifest": [{"name": "舌象4", "status": "unreadable"}]}, "review": {"status": "needs_information", "comment": "需补充清晰舌象"}, }, "openai": {"status": "running", "report": None, "candidate": None, "comparison": None, "coverage": {"status": "pending"}}, }, } class Repository(DemoDoctorRepository): def __init__(self) -> None: super().__init__() self.calls: list[tuple[str, Any]] = [] self.batches = [batch()] self.enabled = True def list_prescription_ai_reports(self, **params: Any) -> dict[str, Any]: self.calls.append(("reports", params)) return {"enabled": self.enabled, "lists": deepcopy(self.batches), "count": len(self.batches), "page_no": 1, "page_size": 20} def get_prescription_ai_report(self, batch_id: int) -> dict[str, Any]: self.calls.append(("detail", batch_id)) return deepcopy(next(row for row in self.batches if row["id"] == batch_id)) def retry_prescription_ai(self, batch_id: int, model_key: str) -> dict[str, Any]: self.calls.append(("retry", (batch_id, model_key))) return {"batch_id": batch_id, "status": "queued"} def regenerate_prescription_ai(self, prescription_id: int, reason: str) -> dict[str, Any]: self.calls.append(("regenerate", (prescription_id, reason))) return {"batch_id": 41, "status": "queued"} def review_prescription_ai(self, batch_id: int, model_key: str, status: str, comment: str) -> dict[str, Any]: self.calls.append(("review", (batch_id, model_key, status, comment))) return {"saved": True} def list_prescription_ai_statuses(self, ids: list[int]) -> dict[str, Any]: self.calls.append(("statuses", list(ids))) return {"enabled": self.enabled, "items": [{"prescription_id": value, "batch_id": 40, "status": "running", "validity": "current", "comparison_type": "independent_baseline", "models": {"qwen": {"status": "succeeded", "score": 0, "comparison_status": "comparable"}, "openai": {"status": "running", "score": None}}} for value in ids] if self.enabled else []} @pytest.fixture(scope="module") def application() -> QApplication: return QApplication.instance() or QApplication([]) @pytest.fixture(autouse=True) def no_network(monkeypatch: pytest.MonkeyPatch) -> None: def denied(*_args: Any, **_kwargs: Any) -> None: pytest.fail("These tests must never contact external systems") monkeypatch.setattr(socket.socket, "connect", denied) monkeypatch.setattr(socket.socket, "connect_ex", denied) monkeypatch.setattr(socket, "create_connection", denied) @pytest.fixture def immediate(monkeypatch: pytest.MonkeyPatch) -> None: def run(function: Any, *, on_success: Any = None, on_error: Any = None, on_finished: Any = None) -> object: try: result = function() except Exception as error: if on_error: on_error(error) else: raise else: if on_success: on_success(result) finally: if on_finished: on_finished() return object() for module in (ai, page_module, editor_module): monkeypatch.setattr(module, "run_async", run) @pytest.mark.parametrize(("value", "expected"), [(None, "—"), (0, "0%"), (100, "100%"), (42.3, "42%"), (float("nan"), "—"), (float("inf"), "—"), (False, "—"), (-1, "—")]) def test_percentage_never_turns_missing_into_zero(value: Any, expected: str) -> None: assert ai.percentage(value) == expected def test_shared_report_reads_only_and_renders_each_model_independently(application: QApplication, immediate: None) -> None: repository = Repository() dialog = ai.IssuedPrescriptionAiDialog(repository, ["*"], prescription_id=801) dialog.show() application.processEvents() assert [call[0] for call in repository.calls] == ["reports", "detail"] assert "千问分析" in dialog.model_views["qwen"]["report"].toPlainText() assert dialog.model_views["qwen"]["score"].text() == "0.0%" assert dialog.model_views["openai"]["score"].text() == "—" assert "资料不全" in dialog.model_views["qwen"]["title"].text() assert "黄芪" in dialog.model_views["qwen"]["candidate"].toPlainText() assert "医生剂量" in dialog.model_views["qwen"]["comparison"].toPlainText() assert "第4张图片不可读" in dialog.model_views["qwen"]["sources"].toPlainText() assert "<script>" in dialog.model_views["qwen"]["report"].toHtml() assert dialog._timer.isActive() assert not dialog.model_views["qwen"]["retry"].isEnabled() assert not dialog.model_views["openai"]["retry"].isEnabled() dialog.hide() assert not dialog._timer.isActive() calls = list(repository.calls) dialog._poll() assert repository.calls == calls def test_retry_and_review_target_only_selected_model(application: QApplication, immediate: None) -> None: repository = Repository() repository.batches[0]["status"] = "partial" repository.batches[0]["models"]["openai"].update(status="failed", error_code="timeout", error_message="模型超时") dialog = ai.IssuedPrescriptionAiDialog(repository, ["*"], prescription_id=801) dialog.show() assert dialog.model_views["openai"]["retry"].isEnabled() assert not dialog._timer.isActive() dialog._retry("openai") assert ("retry", (40, "openai")) in repository.calls dialog.model_views["qwen"]["review_state"].setCurrentIndex(3) dialog.model_views["qwen"]["comment"].setPlainText("已核对逐味差异") dialog._review("qwen") assert ("review", (40, "qwen", "reviewed", "已核对逐味差异")) in repository.calls assert not any(call[0] == "regenerate" for call in repository.calls) dialog.close() def test_history_and_hidden_dialog_ignore_late_detail(application: QApplication, immediate: None, monkeypatch: pytest.MonkeyPatch) -> None: repository = Repository() repository.batches.append(batch(41)) repository.batches[1]["models"]["qwen"]["report"] = {"summary": "第二批报告"} dialog = ai.IssuedPrescriptionAiDialog(repository, ["*"], prescription_id=801) dialog.show() pending = [] monkeypatch.setattr(ai, "run_async", lambda function, **callbacks: pending.append((function, callbacks))) dialog._poll() assert len(pending) == 1 old_generation = dialog._generation dialog.history.setCurrentIndex(1) assert dialog._generation > old_generation old_function, old_callbacks = pending[0] old_callbacks["on_success"](old_function()) assert dialog._batch == {} new_function, new_callbacks = pending[1] new_callbacks["on_success"](new_function()) assert dialog._batch["id"] == 41 assert "第二批报告" in dialog.model_views["qwen"]["report"].toPlainText() dialog.hide() new_callbacks["on_success"](batch(41)) assert "第二批报告" in dialog.model_views["qwen"]["report"].toPlainText() def test_wrong_patient_batch_is_not_displayed(application: QApplication, immediate: None, monkeypatch: pytest.MonkeyPatch) -> None: repository = Repository() wrong_patient = batch() wrong_patient["patient_id"] = 999 monkeypatch.setattr(repository, "get_prescription_ai_report", lambda _batch_id: wrong_patient) dialog = ai.IssuedPrescriptionAiDialog(repository, ["*"], diagnosis_id=501) dialog.show() assert repository.calls[0][1]["diagnosis_id"] == 501 assert dialog._batch == {} assert "归属" in dialog.message.text() assert not dialog.model_views["qwen"]["report"].toPlainText() dialog.close() def test_patient_history_accepts_other_authorized_visits(application: QApplication, immediate: None) -> None: repository = Repository() repository.batches[0]["diagnosis_id"] = 499 dialog = ai.IssuedPrescriptionAiDialog(repository, ["*"], diagnosis_id=501) dialog.show() assert dialog._batch["diagnosis_id"] == 499 assert "千问分析" in dialog.model_views["qwen"]["report"].toPlainText() dialog.close() def test_disabled_generation_keeps_history_readable(application: QApplication, immediate: None) -> None: repository = Repository() repository.enabled = False dialog = ai.IssuedPrescriptionAiDialog(repository, ["*"], prescription_id=801) dialog.show() assert "千问分析" in dialog.model_views["qwen"]["report"].toPlainText() assert not dialog.regenerate_button.isEnabled() assert "未启用" in dialog.message.text() dialog.close() def test_view_permission_cannot_mutate(application: QApplication, immediate: None) -> None: repository = Repository() permissions = ["tcm.prescriptionAi/reports", "tcm.prescriptionAi/detail"] dialog = ai.IssuedPrescriptionAiDialog(repository, permissions, prescription_id=801) dialog.show() assert dialog.regenerate_button.isHidden() dialog._review("qwen") dialog._retry("openai") assert not any(call[0] in {"review", "retry", "regenerate"} for call in repository.calls) dialog.close() def test_list_batches_visible_ids_and_stops_on_hide(application: QApplication, immediate: None) -> None: repository = Repository() page = page_module.PrescriptionsPage(repository, ["*"]) page.resize(1366, 800) page.show() application.processEvents() statuses = [call for call in repository.calls if call[0] == "statuses"] assert statuses and set(statuses[-1][1]) <= {801, 802} assert len(statuses[-1][1]) <= 100 assert not page.table.isColumnHidden(11) assert not page.table.isColumnHidden(12) assert "千问 0%" in page.table.item(0, 12).text() assert "OpenAI —" in page.table.item(0, 12).text() assert any(button.accessibleName() == "AI 报告" for button in page.table.findChildren(QPushButton)) assert page._ai_timer.isActive() page.hide() assert not page._ai_timer.isActive() calls = list(repository.calls) page._load_ai_statuses() assert repository.calls == calls def test_blank_list_row_has_no_ai_display_or_entry_points(application: QApplication, immediate: None, monkeypatch: pytest.MonkeyPatch) -> None: repository = Repository() opened = [] monkeypatch.setattr(page_module, "present_issued_prescription_ai", lambda *args, **kwargs: opened.append(kwargs)) page = page_module.PrescriptionsPage(repository, ["*"]) page.resize(1366, 800) page.show() application.processEvents() # Demo row 801 contains herbs; row 802 is an empty manual prescription. assert all(call[1] == [801] for call in repository.calls if call[0] == "statuses") assert not page.table.isColumnHidden(11) and not page.table.isColumnHidden(12) page._ai_statuses[802] = batch(prescription_id=802) page._render_ai_statuses() for column in (11, 12): item = page.table.item(1, column) assert item.text() == item.toolTip() == item.data(Qt.ItemDataRole.AccessibleTextRole) == "" assert not any(button.accessibleName() == "AI 报告" for button in page.table.cellWidget(1, 2).findChildren(QPushButton)) page.table.selectRow(1) assert page.ai_report_button.isHidden() and not page.ai_report_button.isEnabled() page._open_ai_report() page.table.itemClicked.emit(page.table.item(1, 11)) page.table.itemClicked.emit(page.table.item(1, 12)) page._open_ai_context_menu(page.table.visualItemRect(page.table.item(1, 1)).center()) assert not hasattr(page, "_ai_context_menu") assert opened == [] page.table.selectRow(0) assert page.ai_report_button.isVisible() and page.ai_report_button.isEnabled() page.ai_report_button.click() assert opened == [{"prescription_id": 801}] page.close() @pytest.mark.parametrize("blank_fields", [ {"is_system_auto": "1", "herbs": []}, {"is_system_auto": True, "herbs": [{"name": "黄芪"}]}, {"is_system_auto": 0, "herbs": [{}, {"name": " "}]}, {"is_system_auto": 0, "herbs": None}, ]) def test_all_blank_list_hides_ai_until_prescription_is_saved(application: QApplication, immediate: None, monkeypatch: pytest.MonkeyPatch, blank_fields: dict[str, Any]) -> None: repository = Repository() row = {"id": 803, "sn": "RX-BLANK", "patient_name": "测试患者", **blank_fields} monkeypatch.setattr(repository, "list_prescriptions", lambda **_filters: {"lists": [row], "count": 1}) page = page_module.PrescriptionsPage(repository, ["*"]) page.resize(1366, 800) page.show() application.processEvents() assert page.table.isColumnHidden(11) and page.table.isColumnHidden(12) assert page.ai_report_button.isHidden() and page.ai_status_notice.isHidden() assert not page._ai_timer.isActive() assert not any(call[0] == "statuses" for call in repository.calls) assert not any(button.accessibleName() == "AI 报告" for button in page.table.cellWidget(0, 2).findChildren(QPushButton)) row.update(is_system_auto=0, herbs=[{"name": "黄芪", "dosage": 12}]) page.refresh() application.processEvents() assert ("statuses", [803]) in repository.calls assert not page.table.isColumnHidden(11) and not page.table.isColumnHidden(12) assert page.ai_report_button.isVisible() and page.ai_status_notice.isVisible() assert "千问 0%" in page.table.item(0, 12).text() assert any(button.accessibleName() == "AI 报告" for button in page.table.cellWidget(0, 2).findChildren(QPushButton)) page.close() def test_list_disabled_explains_availability_and_keeps_history(application: QApplication, immediate: None) -> None: repository = Repository() repository.enabled = False page = page_module.PrescriptionsPage(repository, ["*"]) page.resize(1366, 800) page.show() application.processEvents() assert page.table.isColumnHidden(11) and page.table.isColumnHidden(12) assert not page._ai_timer.isActive() assert page.ai_report_button.isVisible() assert page.ai_status_notice.isVisible() assert "未启用" in page.ai_status_notice.text() page.close() def test_list_service_error_is_visible_and_clears_after_recovery(application: QApplication, immediate: None, monkeypatch: pytest.MonkeyPatch) -> None: repository = Repository() def unavailable(_ids: list[int]) -> dict[str, Any]: raise RuntimeError("AI service unavailable") monkeypatch.setattr(repository, "list_prescription_ai_statuses", unavailable) page = page_module.PrescriptionsPage(repository, ["*"]) page.resize(1366, 800) page.show() application.processEvents() assert page.ai_status_notice.isVisible() assert "暂不可用" in page.ai_status_notice.text() assert page.table.isColumnHidden(12) assert not page._ai_timer.isActive() monkeypatch.setattr(repository, "list_prescription_ai_statuses", Repository.list_prescription_ai_statuses.__get__(repository)) page.refresh() application.processEvents() assert "已启用" in page.ai_status_notice.text() assert not page.ai_status_notice.toolTip() assert not page.table.isColumnHidden(12) page.close() def test_list_missing_permission_explains_without_loading_ai(application: QApplication, immediate: None) -> None: repository = Repository() page = page_module.PrescriptionsPage(repository, ["cf.prescription/read"]) page.resize(1366, 800) page.show() application.processEvents() assert page.ai_status_notice.isVisible() assert "权限" in page.ai_status_notice.text() assert page.table.isColumnHidden(11) and page.table.isColumnHidden(12) assert not page.ai_report_button.isVisible() assert not any(call[0] in {"statuses", "reports", "detail"} for call in repository.calls) page.close() def test_list_ignores_response_after_filter_generation_changes(application: QApplication, immediate: None, monkeypatch: pytest.MonkeyPatch) -> None: repository = Repository() page = page_module.PrescriptionsPage(repository, ["*"]) page.resize(1366, 800) page.show() application.processEvents() callbacks = [] monkeypatch.setattr(page_module, "run_async", lambda function, **kwargs: callbacks.append((function, kwargs))) page._load_ai_statuses() assert callbacks page._generation += 1 page._ai_statuses = {} function, call = callbacks[0] call["on_success"](function()) assert page._ai_statuses == {} page.close() def test_statistics_nulls_and_absent_review_samples(application: QApplication, immediate: None) -> None: class StatisticsRepository: def prescription_ai_statistics(self, *_args: Any) -> dict[str, Any]: return {"total_count": 3, "patient_count": 2, "doctors": [{"doctor_name": "张医生", "total_count": 3, "patient_count": 2, "models": {"qwen": {"eligible_count": 0, "coverage_rate": 0, "mean": None, "median": None, "excluded_reasons": {"non_independent": 3}}, "openai": {"eligible_count": 1, "coverage_rate": 33.3, "mean": 0, "median": 0}}, "review": {"evaluated_count": 0, "qualified_count": 0, "qualified_rate": None}}]} dialog = ai.PrescriptionAiStatisticsDialog(StatisticsRepository(), ["*"]) dialog.show() text = dialog.report.toPlainText() assert "均值:—" in text and "均值:0.0%" in text assert "未建立复核样本" in text assert "开方事件:3" in dialog.summary.text() dialog.close() def test_editor_keeps_request_key_for_identical_content_and_preserves_ai_assistance(application: QApplication, immediate: None) -> None: editor = editor_module.PrescriptionEditorDialog(DemoDoctorRepository(), {"ai_assisted": True}) first = editor.payload() assert first["ai_assisted"] is True assert str(UUID(first["request_key"])) == first["request_key"] assert first["request_key"] == editor.payload()["request_key"] editor.patient_name.setText("新患者") assert first["request_key"] != editor.payload()["request_key"] editor.close() unknown = editor_module.PrescriptionEditorDialog(DemoDoctorRepository()) assert "ai_assisted" not in unknown.payload() unknown.close() unconfirmed = editor_module.PrescriptionEditorDialog(DemoDoctorRepository(), {"ai_assisted": False}) assert "ai_assisted" not in unconfirmed.payload() unconfirmed.close() def test_repository_exact_read_and_mutation_contracts() -> None: class Client: def __init__(self) -> None: self.calls = [] def get(self, endpoint: str, params: Any) -> dict[str, Any]: self.calls.append(("GET", endpoint, params)) return {"enabled": False, "items": []} def post(self, endpoint: str, params: Any) -> dict[str, Any]: self.calls.append(("POST", endpoint, params)) return {"saved": True} client = Client() repository = RemoteDoctorRepository(client) assert repository.list_prescription_ai_statuses([801, 802, 801]) == {"enabled": False, "items": []} repository.list_prescription_ai_reports(diagnosis_id=501, page_no=2, page_size=20) repository.get_prescription_ai_report(40) assert all(call[0] == "GET" for call in client.calls) repository.regenerate_prescription_ai(801, "补充病历") repository.retry_prescription_ai(40, "openai") repository.review_prescription_ai(40, "qwen", "reviewed", "核对完成") repository.prescription_ai_statistics("2026-09-01", "2026-09-09", 7) assert client.calls == [ ("GET", "tcm.prescriptionAi/statuses", {"ids": "801,802"}), ("GET", "tcm.prescriptionAi/reports", {"diagnosis_id": 501, "page_no": 2, "page_size": 20}), ("GET", "tcm.prescriptionAi/detail", {"batch_id": 40}), ("POST", "tcm.prescriptionAi/regenerate", {"prescription_id": 801, "reason": "补充病历"}), ("POST", "tcm.prescriptionAi/retry", {"batch_id": 40, "model_key": "openai"}), ("POST", "tcm.prescriptionAi/review", {"batch_id": 40, "model_key": "qwen", "status": "reviewed", "comment": "核对完成"}), ("GET", "tcm.prescriptionAi/statistics", {"date_from": "2026-09-01", "date_to": "2026-09-09", "doctor_id": 7}), ] with pytest.raises(ValueError): repository.list_prescription_ai_statuses(list(range(1, 102))) with pytest.raises(ValueError): repository.list_prescription_ai_reports(prescription_id=801, diagnosis_id=501) with pytest.raises(ValueError): repository.retry_prescription_ai(40, "other") def test_stale_list_score_is_not_attached_to_current_prescription() -> None: value = {"validity": "superseded", "models": {"qwen": {"score": 100}, "openai": {"score": 90}}} assert ai.agreement_text(value) == "千问 —\nOpenAI —" assert ai.state_text(value) == "处方已变更" assert not ai.batch_running({**value, "status": "running"}) @pytest.mark.parametrize("status", ["preparing", "waiting_sources", "retry_wait", "queued", "running"]) def test_server_pending_states_continue_polling(status: str) -> None: assert ai.batch_running({"status": status, "validity": "current"}) assert not ai.batch_running({"status": status, "validity": "prescription_changed"}) def test_regeneration_selects_new_batch_and_preserves_old_history(application: QApplication, immediate: None, monkeypatch: pytest.MonkeyPatch) -> None: repository = Repository() repository.batches[0]["status"] = "success" repository.batches[0]["models"]["openai"]["status"] = "success" def regenerate(prescription_id: int, reason: str) -> dict[str, Any]: repository.calls.append(("regenerate", (prescription_id, reason))) repository.batches.insert(0, batch(41)) return {"batch_id": 41, "status": "queued"} monkeypatch.setattr(repository, "regenerate_prescription_ai", regenerate) monkeypatch.setattr(ai.QInputDialog, "getMultiLineText", lambda *_args: ("已补充资料", True)) dialog = ai.IssuedPrescriptionAiDialog(repository, ["*"], prescription_id=801) dialog.show() dialog._regenerate() assert dialog.history.count() == 2 assert dialog.history.currentData() == 41 assert dialog._batch["id"] == 41 assert dialog._timer.isActive() assert ("regenerate", (801, "已补充资料")) in repository.calls dialog.close() def test_list_append_retains_finished_cached_scores(application: QApplication, immediate: None) -> None: repository = Repository() page = page_module.PrescriptionsPage(repository, ["*"]) page.resize(1366, 800) page.show() application.processEvents() for cached in page._ai_statuses.values(): cached["status"] = "success" cached["models"]["openai"].update(status="success", score=62) calls = len([call for call in repository.calls if call[0] == "statuses"]) page._apply_result(repository.list_prescriptions(), page._generation, 1) assert "OpenAI 62%" in page.table.item(0, 12).text() assert len([call for call in repository.calls if call[0] == "statuses"]) == calls assert not page._ai_timer.isActive() page.close() def test_context_menu_targets_clicked_prescription(application: QApplication, immediate: None, monkeypatch: pytest.MonkeyPatch) -> None: repository = Repository() opened = [] monkeypatch.setattr(page_module, "present_issued_prescription_ai", lambda *args, **kwargs: opened.append(kwargs)) page = page_module.PrescriptionsPage(repository, ["*"]) page.resize(1366, 800) page.show() application.processEvents() page.table.selectRow(1) item = page.table.item(0, 1) expected_id = page.table.item(0, 0).data(Qt.ItemDataRole.UserRole).id page._open_ai_context_menu(page.table.visualItemRect(item).center()) page._ai_context_menu.actions()[0].trigger() assert opened == [{"prescription_id": expected_id}] page._ai_context_menu.close() page.close() def test_real_server_comparison_rows_keep_both_doses_and_basis() -> None: rows = ai._comparison_rows([{ "key": "1|raw|main", "medicine_id": 1, "name": "黄芪", "processing": "生品", "formula_type": "主方", "administration_route": "口服", "group": "", "doctor": {"dosage": 30, "unit": "g", "dose_basis": "per_dose"}, "candidate": {"dosage": 15, "unit": "g", "dose_basis": "per_dose"}, "doctor_dosage": 30, "candidate_dosage": 15, "unit": "g", "dose_basis": "per_dose", "match_type": "matched", "contribution": 0.5, }]) assert rows[0]["doctor_dosage"] == "30 克" assert rows[0]["candidate_dosage"] == "15 克" assert rows[0]["dose_basis"] == "per_dose" assert rows[0]["contribution"] == 0.5 assert "生品 / 主方 / 口服" in rows[0]["name"] def test_unavailable_candidate_has_reason_and_pending_review_is_disabled(application: QApplication, immediate: None) -> None: repository = Repository() repository.batches[0]["models"]["qwen"]["candidate"] = {"status": "withheld_for_risk", "reason": "过敏用药信息待核实", "herbs": []} dialog = ai.IssuedPrescriptionAiDialog(repository, ["*"], prescription_id=801) dialog.show() text = dialog.model_views["qwen"]["candidate"].toPlainText() assert "因风险暂缓候选用药" in text and "过敏用药信息待核实" in text assert not dialog.model_views["openai"]["save"].isEnabled() assert "分析尚未完成" in dialog.model_views["openai"]["candidate"].toPlainText() dialog.close() def test_preparation_failure_reason_is_visible_without_models(application: QApplication, immediate: None) -> None: repository = Repository() repository.batches[0].update(status="blocked", models={}, error_message="患者关联冲突,需要核对诊单", coverage_status="partial") dialog = ai.IssuedPrescriptionAiDialog(repository, ["*"], prescription_id=801) dialog.show() assert "患者关联冲突" in dialog.batch_summary.text() assert "资料不全" in dialog.batch_summary.text() assert "患者关联冲突" in dialog.model_views["qwen"]["candidate"].toPlainText() dialog.close() def test_actual_history_pagination_contract(application: QApplication, immediate: None, monkeypatch: pytest.MonkeyPatch) -> None: repository = Repository() repository.batches = [batch(value) for value in range(61, 40, -1)] def reports(**params: Any) -> dict[str, Any]: repository.calls.append(("reports", params)) page = params["page_no"] return {"lists": repository.batches[(page - 1) * 20:page * 20], "count": 21, "page_no": page, "page_size": 20} monkeypatch.setattr(repository, "list_prescription_ai_reports", reports) dialog = ai.IssuedPrescriptionAiDialog(repository, ["*"], prescription_id=801) dialog.show() assert dialog.history.count() == 20 and dialog.next.isEnabled() dialog._history_page(1) assert dialog.history.count() == 1 assert dialog._batch["id"] == 41 assert dialog.previous.isEnabled() and not dialog.next.isEnabled() assert repository.calls[-2] == ("reports", {"prescription_id": 801, "diagnosis_id": 0, "page_no": 2, "page_size": 20}) dialog.close() @pytest.mark.parametrize(("status", "expected"), [("blank", "尚未开方"), ("not_generated", "尚无分析记录")]) def test_server_empty_batch_statuses_are_not_fake_scores(status: str, expected: str) -> None: data = {"status": status, "models": {}, "coverage_status": "pending"} assert ai.state_text(data) == expected assert ai.agreement_text(data) == "千问 —\nOpenAI —" def test_statistics_displays_actual_version_strata_without_filling_null_mean(application: QApplication, immediate: None) -> None: class StatisticsRepository: def prescription_ai_statistics(self, *_args: Any) -> dict[str, Any]: strata = [{"versions": {"model_version": version, "algorithm_version": "v1", "prompt_version": "p1", "dictionary_version": "d1"}, "count": 1, "mean": mean, "median": mean, "distribution": {"[0,20)": 1 if mean == 0 else 0}, "sample_status": "insufficient_sample"} for version, mean in (("model-1", 0), ("model-2", 80))] return {"total_count": 2, "patient_count": 1, "doctors": [{"doctor_id": 7, "doctor_name": "张医生", "total_count": 2, "patient_count": 1, "paired_count": 0, "models": {"qwen": {"eligible_count": 2, "coverage_rate": 100, "mean": None, "median": None, "excluded_reasons": {}, "strata": strata}}, "review": {"evaluated_count": 0, "qualified_count": 0, "qualified_rate": None}}]} dialog = ai.PrescriptionAiStatisticsDialog(StatisticsRepository(), ["*"]) dialog.show() text = dialog.report.toPlainText() assert "双模型共同有效样本:0" in text assert "整体均值与中位数不合并" in text assert "model-1" in text and "model-2" in text assert "均值:—" in text and "均值:0.0%" in text and "均值:80.0%" in text assert "样本不足" in text and "未建立复核样本" in text dialog.close() @pytest.mark.parametrize(("code", "expected"), [ ("SOURCE_HISTORY_VERSIONS_UNAVAILABLE", "来源历史版本无法核验"), ("ARCHIVE_SYNC_WATERMARK_UNAVAILABLE: chat_records", "归档同步完整性尚未核验:聊天记录"), ("TRANSCRIPT_NOT_VERIFIED_COMPLETE:call_records:1820", "问诊转写完整性尚未核验:问诊通话(编号:1820)"), ("TRANSCRIPT_PARTIAL: video_calls:1821", "问诊转写仅部分完成:视频问诊(编号:1821)"), ("CRITICAL_CLINICAL_FACT_MISSING:clinical.allergy_history", "关键临床资料缺失:临床资料 · 过敏史"), ("MODEL_FILE_OUTPUT_INVALID", "模型未能正确解析这组附件"), ("missing_or_unknown_dose_basis", "每剂或每日剂量基准不明确"), ("review_not_independent", "复核不具独立性"), ]) def test_metadata_and_historical_composite_reasons_are_chinese(code: str, expected: str) -> None: assert ai.label(code) == expected assert expected in ai._html({"missing_information": [code]}) def test_source_labels_keep_exact_ids_and_distinguish_source_kinds() -> None: fingerprint = "0123456789abcdef" * 4 payload = {"evidence_references": ["diagnoses:501", "video_calls:1820", "call_records:1820", "file:" + fingerprint], "source_id": "clinical.pregnancy_history", "redaction_manifest": ["diagnoses:501:doctor_advice"]} original = deepcopy(payload) rendered = ai._html(payload) assert "诊单(编号:501)" in rendered assert "视频问诊(编号:1820)" in rendered and "问诊通话(编号:1820)" in rendered assert f"附件(编号:{fingerprint})" in rendered assert "临床资料 · 妊娠与哺乳情况" in rendered assert "诊单(编号:501) · 医嘱" in rendered assert all(raw not in rendered for raw in ("diagnoses:", "video_calls:", "file:", "clinical.", "doctor_advice")) assert payload == original def test_unknown_metadata_has_chinese_fallback_without_translating_clinical_prose() -> None: clinical_text = "CT、MRI、HbA1c 6.2%,HbA1c_result 待复查,Follow-up in 2 weeks;unknown" payload = {"summary": clinical_text, "treatment_advice": "ALT_AST 需结合 COPD 病史评估", "status": "newphase", "error_code": "FUTURE_FAILURE_CODE", "future_field": 3, "future_status": "newstate", "future_enum": "new_enum_value", "missing_information": ["FUTURE_GAP_CODE:future_records:1820"], "source_id": "future_records:1821"} original = deepcopy(payload) rendered = ai._html(payload) assert clinical_text in rendered and "ALT_AST 需结合 COPD 病史评估" in rendered assert "未识别的系统状态" in rendered and "其他来源(编号:1820)" in rendered assert "其他来源(编号:1821)" in rendered and "3" in rendered for raw in ("newphase", "newstate", "new_enum_value", "FUTURE_FAILURE_CODE", "FUTURE_GAP_CODE", "future_field", "future_records"): assert raw not in rendered assert payload == original def test_nested_metadata_translates_fields_values_and_retains_all_statistics() -> None: payload = { "coverage": {"status": "partial", "source_complete": True, "files": {"file:42": {"status": "unreadable", "version_verified": False}}}, "source_summary": {"diagnoses_count": 2, "video_calls_count": 3, "source_record_count": 5, "history_versioning": "unavailable"}, "normalization": {"doctor": {"defaults": [{"field": "unit", "value": "g"}, {"field": "dose_basis", "value": "per_dose"}]}, "issues": [{"side": "both", "code": "unit_mismatch", "row": 0}], "unit_policy": "spelling_aliases_only_no_quantity_conversion"}, "exclusion_reasons": {"SOURCE_HISTORY_VERSIONS_UNAVAILABLE": 12, "future_reason_a": 7, "future_reason_b": 8}, "review": {"sampling_groups": [{"sampling_method": "risk_directed", "outcomes": {"qualified": 2, "needs_revision": 1}}]}, } original = deepcopy(payload) rendered = ai._html(payload) for expected in ("资料不全", "历史版本无法核验", "诊单数", "视频问诊数", "克", "每剂", "剂量单位不同", "双方", "按风险抽样", "合格", "需修订"): assert expected in rendered for count in (12, 7, 8, 0): assert f"
{count}
" in rendered assert rendered.count("未识别的系统标识") == 2 for raw in ("partial", "version_verified", "diagnoses_count", "per_dose", "SOURCE_HISTORY", "future_reason", "risk_directed", "qualified"): assert raw not in rendered assert payload == original def test_metadata_and_prose_remain_html_escaped(application: QApplication) -> None: attack = '