更新bug

This commit is contained in:
Your Name
2026-08-20 17:47:14 +08:00
parent 35f91ee37a
commit 5794f60c5d
67 changed files with 9257 additions and 1287 deletions
+620
View File
@@ -3,6 +3,7 @@
from __future__ import annotations
import os
from concurrent.futures import ThreadPoolExecutor
from copy import deepcopy
from datetime import date
from typing import Any
@@ -40,8 +41,11 @@ def immediate_async(monkeypatch: pytest.MonkeyPatch) -> None:
on_success: Any = None,
on_error: Any = None,
on_finished: Any = None,
pool: Any = None,
priority: int = 0,
**kwargs: Any,
) -> object:
del pool, priority
try:
result = function(*args, **kwargs)
except Exception as error:
@@ -305,6 +309,64 @@ def test_openai_failure_keeps_new_qwen_snapshot(
page.close()
def test_finished_only_cached_regeneration_unlocks_retry_and_keeps_snapshot(
application: QApplication,
monkeypatch: pytest.MonkeyPatch,
) -> None:
detail = _detail(109, 301, 509)
saved = _snapshot("qwen", 1, "2026-08-14 10:45:00")
jobs: list[dict[str, Any]] = []
def queue(function: Any, *args: Any, **options: Any) -> object:
jobs.append({"function": function, "args": args, **options})
return object()
monkeypatch.setattr(reception_module, "run_async", queue)
class Repository:
def list_patient_ai_reports(self, patient_id: int) -> dict[str, Any]:
return {"patient_id": patient_id, "reports": [saved]}
def generate_patient_ai_report(
self,
patient_id: int,
*,
model: str,
) -> dict[str, Any]:
raise AssertionError(f"queued worker must not run inline: {patient_id}/{model}")
page = ReceptionPage(
Repository(),
PermissionSet(
[
"tcm.diagnosis/patientAiReports",
"tcm.diagnosis/generatePatientAiReport",
]
),
)
page._select_record(detail["appointment"])
jobs[0]["on_success"]({"detail": detail, "warnings": []})
jobs[1]["on_success"]({"patient_id": 301, "reports": [saved]})
jobs[1]["on_finished"]()
assert page._ai_analysis_model_states["qwen"] == "success"
assert page.ai_analysis_regenerate_button.isEnabled()
page.ai_analysis_regenerate_button.click()
assert len(jobs) == 3
assert page._ai_analysis_regenerating
assert not page.ai_analysis_regenerate_button.isEnabled()
jobs[2]["on_finished"]()
assert not page._ai_analysis_regenerating
assert page._ai_analysis_regeneration_model is None
assert page._ai_analysis_model_states["qwen"] == "success"
assert page.ai_summary_label.text() == "千问第 1 版诊断建议"
assert page.ai_analysis_regenerate_button.isEnabled()
assert "未返回有效结果" in page.ai_analysis_secondary_status.text()
page.close()
def test_late_patient_history_response_is_discarded_after_switch(
application: QApplication,
monkeypatch: pytest.MonkeyPatch,
@@ -353,6 +415,7 @@ def test_late_patient_history_response_is_discarded_after_switch(
first_history_job = jobs[1]
page._select_record(second["appointment"])
assert first_history_job["function"]() is reception_module._ASYNC_REQUEST_CANCELLED
finish(jobs[2])
second_history_job = jobs[3]
finish(second_history_job)
@@ -364,6 +427,563 @@ def test_late_patient_history_response_is_discarded_after_switch(
page.close()
def test_aba_switch_attaches_to_inflight_patient_generation_without_duplicate_post(
application: QApplication,
monkeypatch: pytest.MonkeyPatch,
) -> None:
first = _detail(110, 301, 510)
second = _detail(111, 302, 511)
jobs: list[dict[str, Any]] = []
def queue(function: Any, *args: Any, **options: Any) -> object:
jobs.append({"function": function, "args": args, **options})
return object()
monkeypatch.setattr(reception_module, "run_async", queue)
class Repository:
def __init__(self) -> None:
self.generate_calls: list[tuple[int, str]] = []
def list_patient_ai_reports(self, patient_id: int) -> dict[str, Any]:
return {"patient_id": patient_id, "reports": []}
def generate_patient_ai_report(
self,
patient_id: int,
*,
model: str,
) -> dict[str, Any]:
self.generate_calls.append((patient_id, model))
generated = _snapshot(model, 1, "2026-08-14 12:00:00")
generated["patient_id"] = patient_id
return {
"patient_id": patient_id,
"generated_report": generated,
"report": generated,
}
repository = Repository()
page = ReceptionPage(
repository,
PermissionSet(
[
"tcm.diagnosis/patientAiReports",
"tcm.diagnosis/generatePatientAiReport",
]
),
)
page._select_record(first["appointment"])
jobs[0]["on_success"]({"detail": first, "warnings": []})
jobs[1]["on_success"]({"patient_id": 301, "reports": []})
first_qwen_job = jobs[2]
qwen_result = first_qwen_job["function"]()
page._select_record(second["appointment"])
page._select_record(first["appointment"])
jobs[4]["on_success"]({"detail": first, "warnings": []})
assert len(jobs) == 5
first_qwen_job["on_success"](qwen_result)
first_qwen_job["on_finished"]()
assert repository.generate_calls == [(301, "qwen")]
assert len(jobs) == 6
assert page._ai_analysis_model_states["qwen"] == "success"
assert page.ai_summary_label.text() == "千问第 1 版诊断建议"
page.close()
application.processEvents()
def test_patient_history_get_is_singleflight_across_aba_switch(
application: QApplication,
monkeypatch: pytest.MonkeyPatch,
) -> None:
first = _detail(112, 301, 512)
second = _detail(113, 302, 513)
saved = _snapshot("qwen", 1, "2026-08-14 12:10:00")
jobs: list[dict[str, Any]] = []
def queue(function: Any, *args: Any, **options: Any) -> object:
jobs.append({"function": function, "args": args, **options})
return object()
monkeypatch.setattr(reception_module, "run_async", queue)
class Repository:
def __init__(self) -> None:
self.list_calls: list[int] = []
def list_patient_ai_reports(self, patient_id: int) -> dict[str, Any]:
self.list_calls.append(patient_id)
return {"patient_id": patient_id, "reports": [saved]}
def generate_patient_ai_report(self, patient_id: int, *, model: str) -> Any:
raise AssertionError(f"history exists: {patient_id}/{model}")
repository = Repository()
page = ReceptionPage(
repository,
PermissionSet(
[
"tcm.diagnosis/patientAiReports",
"tcm.diagnosis/generatePatientAiReport",
]
),
)
page._select_record(first["appointment"])
jobs[0]["on_success"]({"detail": first, "warnings": []})
first_list_job = jobs[1]
list_result = first_list_job["function"]()
page._select_record(second["appointment"])
page._select_record(first["appointment"])
jobs[3]["on_success"]({"detail": first, "warnings": []})
assert len(jobs) == 4
assert repository.list_calls == [301]
first_list_job["on_success"](list_result)
first_list_job["on_finished"]()
assert page._ai_analysis_model_states["qwen"] == "success"
assert page.ai_summary_label.text() == "千问第 1 版诊断建议"
page.close()
application.processEvents()
def test_patient_ai_finished_only_tracks_qwen_workers_not_unrelated_openai(
application: QApplication,
monkeypatch: pytest.MonkeyPatch,
) -> None:
detail = _detail(114, 301, 514)
jobs: list[dict[str, Any]] = []
def queue(function: Any, *args: Any, **options: Any) -> object:
jobs.append({"function": function, "args": args, **options})
return object()
monkeypatch.setattr(reception_module, "run_async", queue)
class Repository:
def list_patient_ai_reports(self, patient_id: int) -> dict[str, Any]:
return {"patient_id": patient_id, "reports": []}
def generate_patient_ai_report(self, patient_id: int, *, model: str) -> Any:
raise AssertionError(f"worker must remain queued: {patient_id}/{model}")
page = ReceptionPage(
Repository(),
PermissionSet(
[
"tcm.diagnosis/patientAiReports",
"tcm.diagnosis/generatePatientAiReport",
]
),
)
page._select_record(detail["appointment"])
jobs[0]["on_success"]({"detail": detail, "warnings": []})
current_list_job = jobs[1]
stale_qwen = (page._ai_analysis_generation - 2, 114, 301, "qwen")
stale_openai = (page._ai_analysis_generation - 1, 114, 301, "openai")
page._patient_ai_generation_requests.add(stale_qwen)
page._patient_ai_generation_finished(*stale_qwen)
assert page._ai_analysis_model_states["qwen"] == "loading"
page._patient_ai_generation_requests.add(stale_openai)
current_list_job["on_finished"]()
page._patient_ai_generation_finished(*stale_openai)
assert not page._patient_ai_list_requests
assert not page._patient_ai_generation_requests
assert page._ai_analysis_model_states["qwen"] == "error"
assert not page._ai_analysis_loading
assert "未返回有效结果" in page.ai_analysis_state_label.text()
page.close()
application.processEvents()
def test_aba_cancelled_generation_is_replaced_instead_of_becoming_false_error(
application: QApplication,
monkeypatch: pytest.MonkeyPatch,
) -> None:
first = _detail(115, 301, 515)
second = _detail(116, 302, 516)
jobs: list[dict[str, Any]] = []
def queue(function: Any, *args: Any, **options: Any) -> object:
jobs.append({"function": function, "args": args, **options})
return object()
monkeypatch.setattr(reception_module, "run_async", queue)
monkeypatch.setattr(
reception_module,
"_AI_AUTOMATIC_GENERATION_SETTLE_SECONDS",
5.0,
)
class Repository:
def __init__(self) -> None:
self.generate_calls: list[tuple[int, str]] = []
def list_patient_ai_reports(self, patient_id: int) -> dict[str, Any]:
return {"patient_id": patient_id, "reports": []}
def generate_patient_ai_report(
self,
patient_id: int,
*,
model: str,
) -> dict[str, Any]:
self.generate_calls.append((patient_id, model))
generated = _snapshot(model, 1, "2026-08-14 12:20:00")
generated["patient_id"] = patient_id
return {
"patient_id": patient_id,
"generated_report": generated,
"report": generated,
}
repository = Repository()
page = ReceptionPage(
repository,
PermissionSet(
[
"tcm.diagnosis/patientAiReports",
"tcm.diagnosis/generatePatientAiReport",
]
),
)
page._select_record(first["appointment"])
jobs[0]["on_success"]({"detail": first, "warnings": []})
jobs[1]["on_success"]({"patient_id": 301, "reports": []})
old_qwen_job = jobs[2]
with ThreadPoolExecutor(max_workers=1) as executor:
cancelled_future = executor.submit(old_qwen_job["function"])
page._select_record(second["appointment"])
cancelled = cancelled_future.result(timeout=1.0)
monkeypatch.setattr(
reception_module,
"_AI_AUTOMATIC_GENERATION_SETTLE_SECONDS",
0.0,
)
assert cancelled is reception_module._ASYNC_REQUEST_CANCELLED
assert repository.generate_calls == []
page._select_record(first["appointment"])
jobs[4]["on_success"]({"detail": first, "warnings": []})
assert len(jobs) == 6
current_list_job = jobs[5]
current_list_job["on_success"]({"patient_id": 301, "reports": []})
assert len(jobs) == 7
replacement_qwen_job = jobs[6]
current_list_job["on_finished"]()
assert len(jobs) == 7
qwen_result = replacement_qwen_job["function"]()
replacement_qwen_job["on_success"](qwen_result)
replacement_qwen_job["on_finished"]()
jobs_after_replacement = len(jobs)
old_qwen_job["on_success"](cancelled)
old_qwen_job["on_finished"]()
assert repository.generate_calls == [(301, "qwen")]
assert len(jobs) == jobs_after_replacement
assert page._ai_analysis_model_states["qwen"] == "success"
assert page.ai_summary_label.text() == "千问第 1 版诊断建议"
page.close()
application.processEvents()
@pytest.mark.parametrize("late_completion", ["cancelled", "error"])
def test_late_history_completion_cannot_clear_newer_generated_snapshot(
application: QApplication,
monkeypatch: pytest.MonkeyPatch,
late_completion: str,
) -> None:
detail = _detail(117, 301, 517)
jobs: list[dict[str, Any]] = []
def queue(function: Any, *args: Any, **options: Any) -> object:
jobs.append({"function": function, "args": args, **options})
return object()
monkeypatch.setattr(reception_module, "run_async", queue)
class Repository:
def __init__(self) -> None:
self.generate_calls: list[tuple[int, str]] = []
def list_patient_ai_reports(self, patient_id: int) -> dict[str, Any]:
raise AssertionError(f"history worker remains pending: {patient_id}")
def generate_patient_ai_report(
self,
patient_id: int,
*,
model: str,
) -> dict[str, Any]:
self.generate_calls.append((patient_id, model))
generated = _snapshot(model, 9, "2026-08-14 12:30:00")
generated["id"] = 901 if model == "qwen" else 902
generated["patient_id"] = patient_id
return {
"patient_id": patient_id,
"generated_report": generated,
"report": generated,
}
repository = Repository()
page = ReceptionPage(
repository,
PermissionSet(
[
"tcm.diagnosis/patientAiReports",
"tcm.diagnosis/generatePatientAiReport",
]
),
)
page._select_record(detail["appointment"])
jobs[0]["on_success"]({"detail": detail, "warnings": []})
history_job = jobs[1]
current_context = page._current_patient_ai_context(301)
assert current_context is not None
generation, appointment_id, _patient_id = current_context
page._request_patient_ai_generation("qwen", generation, appointment_id, 301)
qwen_job = jobs[2]
qwen_result = qwen_job["function"]()
qwen_job["on_success"](qwen_result)
qwen_job["on_finished"]()
jobs_after_generation = len(jobs)
if late_completion == "cancelled":
history_job["on_success"](reception_module._ASYNC_REQUEST_CANCELLED)
else:
history_job["on_error"](RuntimeError("late history failure"))
history_job["on_finished"]()
assert repository.generate_calls == [(301, "qwen")]
assert len(jobs) == jobs_after_generation
assert page._ai_analysis_model_states["qwen"] == "success"
assert page._ai_analysis_payloads["qwen"]["id"] == 901
assert page.ai_summary_label.text() == "千问第 9 版诊断建议"
page.close()
application.processEvents()
def test_cancelled_queued_generation_does_not_invalidate_valid_history_get(
application: QApplication,
monkeypatch: pytest.MonkeyPatch,
) -> None:
first = _detail(118, 301, 518)
second = _detail(119, 302, 519)
saved = _snapshot("qwen", 10, "2026-08-14 12:40:00")
jobs: list[dict[str, Any]] = []
def queue(function: Any, *args: Any, **options: Any) -> object:
jobs.append({"function": function, "args": args, **options})
return object()
monkeypatch.setattr(reception_module, "run_async", queue)
class Repository:
def __init__(self) -> None:
self.list_calls: list[int] = []
self.generate_calls: list[tuple[int, str]] = []
def list_patient_ai_reports(self, patient_id: int) -> dict[str, Any]:
self.list_calls.append(patient_id)
return {"patient_id": patient_id, "reports": [saved]}
def generate_patient_ai_report(self, patient_id: int, *, model: str) -> Any:
self.generate_calls.append((patient_id, model))
raise AssertionError("cancelled queued POST must not enter repository")
repository = Repository()
page = ReceptionPage(
repository,
PermissionSet(
[
"tcm.diagnosis/patientAiReports",
"tcm.diagnosis/generatePatientAiReport",
]
),
)
page._select_record(first["appointment"])
jobs[0]["on_success"]({"detail": first, "warnings": []})
history_job = jobs[1]
history_result = history_job["function"]()
current_context = page._current_patient_ai_context(301)
assert current_context is not None
generation, appointment_id, _patient_id = current_context
page._request_patient_ai_generation("qwen", generation, appointment_id, 301)
queued_qwen_job = jobs[2]
page._select_record(second["appointment"])
cancelled = queued_qwen_job["function"]()
page._select_record(first["appointment"])
jobs[4]["on_success"]({"detail": first, "warnings": []})
queued_qwen_job["on_success"](cancelled)
queued_qwen_job["on_finished"]()
history_job["on_success"](history_result)
history_job["on_finished"]()
assert repository.list_calls == [301]
assert repository.generate_calls == []
assert len(jobs) == 5
assert page._ai_analysis_model_states["qwen"] == "success"
assert page.ai_summary_label.text() == "千问第 10 版诊断建议"
page.close()
application.processEvents()
def test_saturated_automatic_ai_slots_end_in_retryable_state_without_post(
application: QApplication,
monkeypatch: pytest.MonkeyPatch,
) -> None:
detail = _detail(120, 301, 520)
jobs: list[dict[str, Any]] = []
def queue(function: Any, *args: Any, **options: Any) -> object:
jobs.append({"function": function, "args": args, **options})
return object()
class BusyAutomaticSlots:
@staticmethod
def acquire(*, blocking: bool) -> bool:
assert not blocking
return False
@staticmethod
def release() -> None:
raise AssertionError("an unacquired slot must not be released")
monkeypatch.setattr(reception_module, "run_async", queue)
monkeypatch.setattr(
reception_module,
"_AI_AUTOMATIC_GENERATION_SETTLE_SECONDS",
0.0,
)
monkeypatch.setattr(
reception_module,
"_AI_AUTOMATIC_REQUEST_SLOTS",
BusyAutomaticSlots(),
)
class Repository:
def __init__(self) -> None:
self.generate_calls: list[tuple[int, str]] = []
def list_patient_ai_reports(self, patient_id: int) -> dict[str, Any]:
return {"patient_id": patient_id, "reports": []}
def generate_patient_ai_report(self, patient_id: int, *, model: str) -> Any:
self.generate_calls.append((patient_id, model))
raise AssertionError("busy automatic work must not submit a POST")
repository = Repository()
page = ReceptionPage(
repository,
PermissionSet(
[
"tcm.diagnosis/patientAiReports",
"tcm.diagnosis/generatePatientAiReport",
]
),
)
page._select_record(detail["appointment"])
jobs[0]["on_success"]({"detail": detail, "warnings": []})
jobs[1]["on_success"]({"patient_id": 301, "reports": []})
automatic_qwen_job = jobs[2]
deferred = automatic_qwen_job["function"]()
automatic_qwen_job["on_success"](deferred)
automatic_qwen_job["on_finished"]()
assert deferred is reception_module._ASYNC_REQUEST_DEFERRED
assert repository.generate_calls == []
assert page._ai_analysis_model_states["qwen"] == "error"
assert not page._ai_analysis_loading
assert not page._ai_analysis_regenerating
assert page.ai_analysis_regenerate_button.isEnabled()
assert "后台分析任务较多" in page.ai_analysis_state_label.text()
page.close()
application.processEvents()
def test_saturated_history_slots_end_in_retryable_state_without_get(
application: QApplication,
monkeypatch: pytest.MonkeyPatch,
) -> None:
detail = _detail(121, 301, 521)
jobs: list[dict[str, Any]] = []
def queue(function: Any, *args: Any, **options: Any) -> object:
jobs.append({"function": function, "args": args, **options})
return object()
class BusyAutomaticSlots:
@staticmethod
def acquire(*, blocking: bool) -> bool:
assert not blocking
return False
@staticmethod
def release() -> None:
raise AssertionError("an unacquired slot must not be released")
monkeypatch.setattr(reception_module, "run_async", queue)
monkeypatch.setattr(
reception_module,
"_AI_AUTOMATIC_GENERATION_SETTLE_SECONDS",
0.0,
)
monkeypatch.setattr(
reception_module,
"_AI_AUTOMATIC_REQUEST_SLOTS",
BusyAutomaticSlots(),
)
class Repository:
def __init__(self) -> None:
self.list_calls: list[int] = []
def list_patient_ai_reports(self, patient_id: int) -> dict[str, Any]:
self.list_calls.append(patient_id)
raise AssertionError("busy automatic read must not enter repository")
def generate_patient_ai_report(self, patient_id: int, *, model: str) -> Any:
raise AssertionError(f"history did not complete: {patient_id}/{model}")
repository = Repository()
page = ReceptionPage(
repository,
PermissionSet(
[
"tcm.diagnosis/patientAiReports",
"tcm.diagnosis/generatePatientAiReport",
]
),
)
page._select_record(detail["appointment"])
jobs[0]["on_success"]({"detail": detail, "warnings": []})
history_job = jobs[1]
deferred = history_job["function"]()
history_job["on_success"](deferred)
history_job["on_finished"]()
assert deferred is reception_module._ASYNC_REQUEST_DEFERRED
assert repository.list_calls == []
assert page._ai_analysis_model_states["qwen"] == "error"
assert not page._ai_analysis_loading
assert page.ai_analysis_retry_button.isEnabled()
assert "查询任务较多" in page.ai_analysis_state_label.text()
page.close()
application.processEvents()
def test_get_history_requires_exact_top_level_and_row_patient_ids() -> None:
row = _snapshot("qwen", 1, "2026-08-14 11:00:00")
valid = {"patient_id": 301, "reports": [row]}