This commit is contained in:
Your Name
2026-10-17 16:51:48 +08:00
parent 56f85f7d89
commit ba2e2e4087
41 changed files with 2851 additions and 96 deletions
+230 -4
View File
@@ -23,7 +23,7 @@ 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,
"id": batch_id, "prescription_id": prescription_id, "prescription_revision": 2, "agreement_visible": True,
"patient_id": 301, "diagnosis_id": 501, "status": status,
"validity": "current", "comparison_type": "independent_baseline",
"source_summary": {"diagnosis_count": 2, "attachment_count": 4},
@@ -48,10 +48,14 @@ class Repository(DemoDoctorRepository):
self.calls: list[tuple[str, Any]] = []
self.batches = [batch()]
self.enabled = True
self.agreement_visible = True
def get_prescription_ai_capabilities(self) -> dict[str, Any]:
return {"agreement_visible": self.agreement_visible, "can_manage_visibility": False}
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}
return {"agreement_visible": self.agreement_visible, "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))
@@ -71,7 +75,7 @@ class Repository(DemoDoctorRepository):
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 []}
return {"agreement_visible": self.agreement_visible, "enabled": self.enabled, "items": [{"agreement_visible": self.agreement_visible, "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")
@@ -595,6 +599,9 @@ def test_list_ignores_response_after_filter_generation_changes(application: QApp
def test_statistics_nulls_and_absent_review_samples(application: QApplication, immediate: None) -> None:
class StatisticsRepository:
def get_prescription_ai_capabilities(self) -> dict[str, Any]:
return {"agreement_visible": True}
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(), ["*"])
@@ -635,6 +642,7 @@ def test_repository_exact_read_and_mutation_contracts() -> None:
return {"saved": True}
client = Client()
repository = RemoteDoctorRepository(client)
repository.get_prescription_ai_capabilities()
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)
@@ -644,6 +652,7 @@ def test_repository_exact_read_and_mutation_contracts() -> None:
repository.review_prescription_ai(40, "qwen", "reviewed", "核对完成")
repository.prescription_ai_statistics("2026-09-01", "2026-09-09", 7)
assert client.calls == [
("GET", "tcm.prescriptionAi/capabilities", {}),
("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}),
@@ -660,6 +669,217 @@ def test_repository_exact_read_and_mutation_contracts() -> None:
repository.retry_prescription_ai(40, "other")
@pytest.mark.parametrize("flag", [False, None, 1, "true"])
def test_agreement_denied_or_missing_keeps_reports_review_and_retry(
application: QApplication, immediate: None, flag: Any,
) -> None:
repository = Repository()
repository.agreement_visible = flag
value = repository.batches[0]
value["agreement_visible"] = flag
value["models"]["openai"].update(status="failed", error_message="上游超时")
dialog = ai.IssuedPrescriptionAiDialog(repository, ["*"], diagnosis_id=501,
current_user={"root": 1, "role_ids": [1]})
dialog.show()
application.processEvents()
assert not dialog._agreement_visible
assert "千问分析" in dialog.model_views["qwen"]["report"].toPlainText()
assert dialog.model_views["qwen"]["comment"].isEnabled()
assert dialog.model_views["qwen"]["save"].isVisible()
assert dialog.model_views["openai"]["retry"].isVisible()
assert dialog.model_views["openai"]["retry"].isEnabled()
assert dialog.history.count() == 1
assert dialog.comparison_panel.checklist.isVisible()
assert dialog.comparison_panel.conclusions.isHidden()
assert not dialog.comparison_panel.rows
assert not dialog.comparison_panel.doses.rows
assert dialog.nav_buttons["history"].isHidden()
assert dialog.agreement_note.isHidden()
for key, views in dialog.model_views.items():
assert not views["score"].text() and not views["score"].toolTip()
assert not views["agreement_bar"].toolTip() and not views["agreement_bar"].accessibleName()
assert views["gauge"]._value is None and not views["gauge"].accessibleDescription()
assert not views["comparison"].toPlainText()
assert "comparison" not in dialog._batch["models"][key]
assert not dialog.agreement.delta.text() and not dialog.agreement.overlap.text()
for key in ("differences", "consensus"):
assert not dialog.rail.focus_values[key].text()
assert dialog.rail.focus_values[key].parentWidget().isHidden()
dialog._goto("per_herb")
assert dialog.tabs.currentWidget() is dialog.tab_pages["candidate"]
assert "黄芪" in dialog.model_views["qwen"]["candidate"].toPlainText()
dialog._goto("comparison_raw")
assert dialog.tabs.currentWidget() is dialog.tab_pages["report"]
dialog._goto("history")
assert dialog.tabs.currentWidget() is dialog.tab_pages["report"]
dialog.close()
@pytest.mark.parametrize("missing", ["reports", "history_batch", "detail"])
def test_each_report_visibility_flag_is_required(application: QApplication, immediate: None, monkeypatch: pytest.MonkeyPatch, missing: str) -> None:
repository = Repository()
reports, detail = repository.list_prescription_ai_reports, repository.get_prescription_ai_report
def read_reports(**params: Any) -> dict[str, Any]:
data = reports(**params)
if missing == "reports":
data.pop("agreement_visible")
if missing == "history_batch":
data["lists"][0].pop("agreement_visible")
return data
def read_detail(batch_id: int) -> dict[str, Any]:
data = detail(batch_id)
if missing == "detail":
data.pop("agreement_visible")
return data
monkeypatch.setattr(repository, "list_prescription_ai_reports", read_reports)
monkeypatch.setattr(repository, "get_prescription_ai_report", read_detail)
dialog = ai.IssuedPrescriptionAiDialog(repository, ["*"], prescription_id=801)
dialog.show()
assert not dialog._agreement_visible
assert not dialog.model_views["qwen"]["score"].text()
assert "千问分析" in dialog.model_views["qwen"]["report"].toPlainText()
dialog.close()
def test_agreement_revocation_and_failed_read_clear_cached_views_and_survive_theme(
application: QApplication, immediate: None,
) -> None:
repository = Repository()
dialog = ai.IssuedPrescriptionAiDialog(repository, ["*"], prescription_id=801)
dialog.show()
assert dialog.model_views["qwen"]["score"].text() == "0.0%"
old_batch, old_generation = deepcopy(dialog._batch), dialog._generation
dialog.model_views["qwen"]["comment"].setPlainText("保留复核草稿")
repository.agreement_visible = False
dialog.refresh()
dialog._detail_ready(old_batch, old_generation, 40)
assert not dialog._agreement_visible
assert not dialog.model_views["qwen"]["score"].text()
assert dialog.history_page.table.rowCount() == 0
assert not dialog.history_page.chart._points
assert dialog.model_views["qwen"]["comment"].toPlainText() == "保留复核草稿"
dialog._switch_theme()
application.processEvents()
assert not dialog._agreement_visible and not dialog.model_views["qwen"]["score"].text()
assert dialog.model_views["qwen"]["comment"].toPlainText() == "保留复核草稿"
dialog._switch_theme()
repository.agreement_visible = True
dialog.refresh()
assert dialog._agreement_visible
dialog._detail_error(RuntimeError("offline"), dialog._generation)
assert not dialog._agreement_visible
assert not dialog.model_views["qwen"]["score"].text()
assert "comparison" not in dialog._batch["models"]["qwen"]
assert "千问分析" in dialog.model_views["qwen"]["report"].toPlainText()
dialog._detail_ready(old_batch, dialog._generation, 40)
assert not dialog._agreement_visible
dialog.close()
def test_revoked_agreement_cache_discards_flat_comparison_metadata() -> None:
value = batch()
sentinel = "previous-private-comparison"
value["baseline_exclusions_json"] = sentinel
value["models"]["qwen"].update({
"reason": sentinel, "comparison_reason_code": sentinel,
"dictionary_version": sentinel, "error_code": "",
})
value["models"]["openai"].update({
"error_code": "UPSTREAM_TIMEOUT", "reason": "模型请求超时",
})
sanitized = ai.agreement_safe_batch(value, False)
assert sentinel not in str(sanitized)
assert "千问分析" in str(sanitized["models"]["qwen"]["report"])
assert sanitized["models"]["openai"]["reason"] == "模型请求超时"
assert value["models"]["qwen"]["reason"] == sentinel
def test_list_capability_failure_revocation_and_late_status_cannot_restore_scores(
application: QApplication, immediate: None, monkeypatch: pytest.MonkeyPatch,
) -> None:
repository = Repository()
page = page_module.PrescriptionsPage(repository, ["*"], {"root": 1})
page.resize(1366, 800)
page.show()
application.processEvents()
assert not page.table.isColumnHidden(12)
old_statuses = repository.list_prescription_ai_statuses([801, 802])
old_token = page._ai_request_token
old_capability_token = page._ai_capability_token
monkeypatch.setattr(repository, "get_prescription_ai_capabilities", lambda: (_ for _ in ()).throw(RuntimeError("offline")))
page._load_ai_capabilities()
page._ai_statuses_ready(old_statuses, [801, 802], old_token, page._generation)
page._ai_capabilities_ready({"agreement_visible": True}, old_capability_token, page._generation)
assert page.table.isColumnHidden(12) and not page.table.isColumnHidden(11)
assert page.ai_statistics_button.isHidden()
assert page.ai_report_button.isVisible()
assert page._ai_statuses
for index in range(page.table.rowCount()):
item = page.table.item(index, 12)
assert not item.text() and not item.toolTip() and not item.data(Qt.ItemDataRole.AccessibleTextRole)
assert all("score" not in model for row in page._ai_statuses.values() for model in row.get("models", {}).values())
page.close()
def test_list_rechecks_completed_scores_after_hide_and_show(application: QApplication, immediate: None, monkeypatch: pytest.MonkeyPatch) -> None:
repository = Repository()
original = repository.list_prescription_ai_statuses
def statuses(ids: list[int]) -> dict[str, Any]:
data = original(ids)
for row in data["items"]:
row["status"] = "success"
for model in row["models"].values():
model["status"] = "success"
return data
monkeypatch.setattr(repository, "list_prescription_ai_statuses", statuses)
page = page_module.PrescriptionsPage(repository, ["*"])
page.resize(1366, 800)
page.show()
application.processEvents()
assert "0%" in page.table.item(0, 12).text()
before = len([call for call in repository.calls if call[0] == "statuses"])
page.hide()
page.show()
application.processEvents()
assert len([call for call in repository.calls if call[0] == "statuses"]) > before
assert "0%" in page.table.item(0, 12).text()
page.close()
@pytest.mark.parametrize("flag", [False, None, 1, "true"])
def test_direct_statistics_requires_strict_server_capability(application: QApplication, immediate: None, flag: Any) -> None:
class StatisticsRepository:
calls = 0
def get_prescription_ai_capabilities(self) -> dict[str, Any]:
return {"agreement_visible": flag, "can_manage_visibility": True}
def prescription_ai_statistics(self, *_args: Any) -> dict[str, Any]:
self.calls += 1
return {}
repository = StatisticsRepository()
dialog = ai.PrescriptionAiStatisticsDialog(repository, ["*"])
dialog.show()
assert repository.calls == 0
assert dialog.panel.isHidden() and not dialog.report.toPlainText()
dialog.close()
def test_statistics_failed_capability_clears_old_charts_and_ignores_late_result(
application: QApplication, immediate: None, monkeypatch: pytest.MonkeyPatch,
) -> None:
repository = ChineseStatisticsRepository()
dialog = ai.PrescriptionAiStatisticsDialog(repository, ["*"])
dialog.show()
assert dialog.panel.isVisible() and dialog.report.toPlainText()
old_generation = dialog._generation
monkeypatch.setattr(repository, "get_prescription_ai_capabilities", lambda: (_ for _ in ()).throw(RuntimeError("offline")))
dialog.refresh()
dialog._ready(repository.prescription_ai_statistics(), old_generation)
assert dialog.panel.isHidden() and not dialog.report.toPlainText()
assert not dialog.panel.distribution.has_data()
assert dialog.panel.doctors.rowCount() == 0
dialog.close()
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 —"
@@ -773,7 +993,7 @@ def test_actual_history_pagination_contract(application: QApplication, immediate
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}
return {"agreement_visible": True, "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()
@@ -795,6 +1015,9 @@ def test_server_empty_batch_statuses_are_not_fake_scores(status: str, expected:
def test_statistics_displays_actual_version_strata_without_filling_null_mean(application: QApplication, immediate: None) -> None:
class StatisticsRepository:
def get_prescription_ai_capabilities(self) -> dict[str, Any]:
return {"agreement_visible": True}
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}}]}
@@ -965,6 +1188,9 @@ def test_internal_version_labels_keep_revision_numbers(version: str, expected: s
class ChineseStatisticsRepository:
def get_prescription_ai_capabilities(self) -> dict[str, Any]:
return {"agreement_visible": True}
def prescription_ai_statistics(self, *_args: Any) -> dict[str, Any]:
exclusions = {"SOURCE_HISTORY_VERSIONS_UNAVAILABLE": 3, "transcript_not_final": 1,
"ATTACHMENT_TARGET_PLAN_LEAKAGE_UNVERIFIED": 2, "future_reason_a": 4, "future_reason_b": 5}