更新
This commit is contained in:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user