feat: add project-scoped operations
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user