Files
next-tjwater-server/tests/unit/test_report_ui.py
T
jiang 05183aff93
Generic Container CI/CD / test-build-publish (push) Successful in 3m53s
Server CI/CD v2 / build-test-publish-and-deploy (push) Successful in 3m52s
feat: extend operational investigation workflows
2026-10-30 17:30:19 +08:00

118 lines
6.0 KiB
Python

from copy import deepcopy
import json
import pytest
from pydantic import ValidationError
from app.domain.schemas.operational_task import InvestigationReportInput, InvestigationReportRevision
def rich_report():
return {
"title": "演练压力核验", "current_conclusion": "待核验", "conclusion_boundary": "演练记录",
"stage": "interim", "outcome": "concluded", "data_gaps": [], "evidence_refs": ["demo-1"],
"ui_blocks": [{
"evidence_refs": ["demo-1"],
"block": {
"version": "answer-ui@1", "block_id": "pressure", "fallback_text": "演练压力 0.27 MPa",
"spec": {"root": "chart", "elements": {
"chart": {"type": "Chart", "children": [], "props": {
"title": "演练压力", "chart_type": "line", "x_data": ["08:00", "09:00"],
"series": [{"name": "压力 MPa", "data": [0.3, 0.27]}], "source": "演练记录",
}},
}},
},
}],
}
def test_report_ui_round_trips_with_a_version_and_keeps_legacy_reports_readable():
report = InvestigationReportInput.model_validate(rich_report())
snapshot = InvestigationReportRevision(
revision=3, execution_generation=2, source_session_id="test",
published_at="2026-10-20T08:00:00Z", published_by="test", report=report,
)
restored = InvestigationReportRevision.model_validate_json(snapshot.model_dump_json())
assert restored.report.ui_blocks[0].block.spec.elements["chart"].props["series"][0]["data"] == [0.3, 0.27]
assert (restored.revision, restored.execution_generation) == (3, 2)
old = json.loads(snapshot.model_dump_json())
del old["report"]["ui_blocks"]
assert InvestigationReportRevision.model_validate(old).report.ui_blocks == []
@pytest.mark.parametrize("phase", [None, "final"])
def test_report_ui_accepts_final_and_legacy_phases(phase):
data = rich_report()
if phase is not None:
data["ui_blocks"][0]["block"]["phase"] = phase
report = InvestigationReportInput.model_validate(data)
assert report.ui_blocks[0].block.phase == phase
serialized = report.model_dump()["ui_blocks"][0]["block"]
assert ("phase" in serialized) == (phase is not None)
assert InvestigationReportInput.model_validate(report.model_dump()).ui_blocks[0].block.phase == phase
@pytest.mark.parametrize("phase", ["process", "unknown", None])
def test_report_ui_rejects_process_and_unknown_phases(phase):
data = rich_report()
data["ui_blocks"][0]["block"]["phase"] = phase
with pytest.raises(ValidationError):
InvestigationReportInput.model_validate(data)
@pytest.mark.parametrize("defect", ["unknown", "script", "unaligned", "nan", "cycle", "unreachable", "duplicate", "foreign", "many", "oversized"])
def test_report_ui_rejects_invalid_or_unbound_content(defect):
data = rich_report()
entry = data["ui_blocks"][0]
spec = entry["block"]["spec"]
chart = spec["elements"]["chart"]
if defect == "unknown":
chart["type"] = "iframe"
elif defect == "script":
chart["props"]["onClick"] = "execute()"
elif defect == "unaligned":
chart["props"]["series"][0]["data"] = [1]
elif defect == "nan":
chart["props"]["series"][0]["data"][0] = float("nan")
elif defect == "cycle":
spec["elements"]["chart"] = {"type": "Stack", "props": {"columns": "one", "gap": "normal"}, "children": ["chart"]}
elif defect == "unreachable":
spec["elements"]["unused"] = deepcopy(chart)
elif defect == "duplicate":
data["ui_blocks"].append(deepcopy(entry))
elif defect == "foreign":
entry["evidence_refs"] = ["other-task"]
elif defect == "many":
data["ui_blocks"] = [{**deepcopy(entry), "block": {**deepcopy(entry["block"]), "block_id": f"b{i}"}} for i in range(9)]
elif defect == "oversized":
# Individually valid blocks can still exceed the UTF-8 report budget.
elements = {"root": {"type": "Stack", "props": {"columns": "one", "gap": "normal"}, "children": [f"n{i}" for i in range(20)]}}
elements.update({f"n{i}": {"type": "Text", "props": {"text": "资" * 3900, "tone": "neutral"}, "children": []} for i in range(20)})
entry["block"]["spec"] = {"root": "root", "elements": elements}
data["ui_blocks"] = [entry, {**deepcopy(entry), "block": {**deepcopy(entry["block"]), "block_id": "second"}}]
with pytest.raises(ValidationError):
InvestigationReportInput.model_validate(data)
def test_report_ui_accepts_semantic_results_without_allowing_raw_presentation_code():
data = rich_report()
spec = data["ui_blocks"][0]["block"]["spec"]
spec["root"] = "layout"
spec["elements"] = {
"layout": {"type": "Stack", "props": {"columns": "one", "gap": "normal", "intent": "report", "density": "compact"}, "children": ["section"]},
"section": {"type": "Section", "props": {"title": "演练结果", "description": None, "role": "result"}, "children": ["result", "note"]},
"result": {"type": "Result", "props": {"title": "演练记录已保存", "summary": "现场处置仍待核验。", "status": "partial", "source": "演练回执"}, "children": []},
"note": {"type": "Text", "props": {"title": "核验说明", "text": "结果仅限演练。", "tone": "neutral"}, "children": []},
}
restored = InvestigationReportInput.model_validate(data)
assert restored.ui_blocks[0].block.spec.elements["result"].props["status"] == "partial"
for extra in ({"style": {"color": "red"}}, {"className": "text-red-500"}, {"css": "display:none"}, {"action": "dispatch"}):
invalid = deepcopy(data)
invalid["ui_blocks"][0]["block"]["spec"]["elements"]["result"]["props"].update(extra)
with pytest.raises(ValidationError):
InvestigationReportInput.model_validate(invalid)
invalid = deepcopy(data)
invalid["ui_blocks"][0]["block"]["spec"]["elements"]["result"]["children"] = ["note"]
with pytest.raises(ValidationError):
InvestigationReportInput.model_validate(invalid)