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("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)