Keep source, config, and deploy files. Leave runtime data, uploads, and logs out of git. Co-authored-by: Cursor <cursoragent@cursor.com>
347 lines
11 KiB
Python
347 lines
11 KiB
Python
import json
|
||
from pathlib import Path
|
||
|
||
import pytest
|
||
|
||
from kamidso.contracts.report import (
|
||
REPORT_JSON_CONTRACT,
|
||
ReportValidationError,
|
||
report_validation_fix_hint,
|
||
validate_report,
|
||
)
|
||
|
||
_FIXTURE_DIR = Path(__file__).resolve().parent
|
||
|
||
|
||
def test_validate_report_minimal():
|
||
payload = validate_report(
|
||
{
|
||
"version": 1,
|
||
"title": "گزارش تست",
|
||
"blocks": [{"type": "text", "content": "سلام"}],
|
||
}
|
||
)
|
||
assert payload["title"] == "گزارش تست"
|
||
assert payload["blocks"][0]["type"] == "text"
|
||
|
||
|
||
def test_validate_report_kpi_table_chart():
|
||
payload = validate_report(
|
||
{
|
||
"version": 1,
|
||
"title": "Sales",
|
||
"blocks": [
|
||
{"type": "kpi", "label": "Total", "value": 100, "trend": "up"},
|
||
{
|
||
"type": "table",
|
||
"columns": ["name", "value"],
|
||
"rows": [{"name": "A", "value": 1}],
|
||
},
|
||
{
|
||
"type": "chart",
|
||
"chartType": "bar",
|
||
"data": [{"x": "A", "y": 1}],
|
||
"x": "x",
|
||
"y": "y",
|
||
},
|
||
],
|
||
}
|
||
)
|
||
assert len(payload["blocks"]) == 3
|
||
|
||
|
||
def test_validate_report_table_headers_and_row_arrays():
|
||
payload = validate_report(
|
||
{
|
||
"version": 1,
|
||
"title": "Accounting",
|
||
"blocks": [
|
||
{
|
||
"type": "table",
|
||
"title": "Top categories",
|
||
"headers": ["category", "amount", "share"],
|
||
"rows": [["bourse", "100", "31%"]],
|
||
}
|
||
],
|
||
}
|
||
)
|
||
table = payload["blocks"][0]
|
||
assert table["columns"] == ["category", "amount", "share"]
|
||
assert table["rows"][0] == ["bourse", "100", "31%"]
|
||
|
||
|
||
def test_validate_report_agent_field_aliases():
|
||
payload = validate_report(
|
||
{
|
||
"version": 1,
|
||
"title": "Accounting",
|
||
"blocks": [
|
||
{"type": "text", "text": "Summary paragraph"},
|
||
{"type": "kpi", "label": "Net", "value": "100", "sub": "toman"},
|
||
{
|
||
"type": "chart",
|
||
"chart_type": "bar",
|
||
"data": [{"label": "Jan", "value": 10}],
|
||
},
|
||
],
|
||
}
|
||
)
|
||
assert payload["version"] == 2
|
||
assert payload["blocks"][0]["content"] == "Summary paragraph"
|
||
assert payload["blocks"][1]["unit"] == "toman"
|
||
assert payload["blocks"][2]["x"] == "label"
|
||
assert payload["blocks"][2]["series"][0]["field"] == "value"
|
||
assert payload["blocks"][2]["id"] == "blk_03"
|
||
|
||
|
||
def test_validate_report_salvages_truncated_json():
|
||
truncated = (
|
||
'{"report": {"version": 1, "title": "گزارش", "blocks": ['
|
||
'{"type": "kpi", "label": "A", "value": 1}, '
|
||
'{"type": "chart", "chartType": "bar", "data": [{"label": "x", "value": 1}], '
|
||
'"x": "label", "y": "value"}, '
|
||
'{"type": "chart", "chart_type": "bar", "data": [{"label": "دی", "value":'
|
||
)
|
||
payload = validate_report(truncated)
|
||
assert payload["title"] == "گزارش"
|
||
assert len(payload["blocks"]) >= 2
|
||
|
||
|
||
def test_validate_report_kpi_group_and_multi_series_chart():
|
||
payload = validate_report(
|
||
{
|
||
"version": 1,
|
||
"title": "Agent shape",
|
||
"blocks": [
|
||
{
|
||
"type": "kpi",
|
||
"items": [
|
||
{"label": "Total in", "value": "100"},
|
||
{"label": "Total out", "value": "90", "sub": "rial"},
|
||
],
|
||
},
|
||
{
|
||
"type": "chart",
|
||
"chart_type": "bar",
|
||
"title": "Monthly",
|
||
"labels": ["Jan", "Feb"],
|
||
"series": [
|
||
{"label": "In", "values": [10, 20]},
|
||
{"label": "Out", "values": [8, 15]},
|
||
],
|
||
},
|
||
{"type": "callout", "level": "warning", "text": "Note"},
|
||
],
|
||
}
|
||
)
|
||
assert payload["blocks"][0]["type"] == "kpi_group"
|
||
assert payload["blocks"][0]["items"][0]["label"] == "Total in"
|
||
assert payload["blocks"][0]["items"][1]["unit"] == "rial"
|
||
chart = payload["blocks"][1]
|
||
assert chart["type"] == "chart"
|
||
assert chart["data"][0] == {"label": "Jan", "in": 10, "out": 8}
|
||
assert [s["field"] for s in chart["series"]] == ["in", "out"]
|
||
assert payload["blocks"][2]["variant"] == "warning"
|
||
|
||
|
||
def test_validate_report_callout_kind_and_content_aliases():
|
||
payload = validate_report(
|
||
{
|
||
"version": 1,
|
||
"title": "Q1",
|
||
"blocks": [
|
||
{
|
||
"type": "callout",
|
||
"kind": "warning",
|
||
"content": "Key insight about cash flow.",
|
||
}
|
||
],
|
||
}
|
||
)
|
||
callout = payload["blocks"][0]
|
||
assert callout["variant"] == "warning"
|
||
assert callout["text"] == "Key insight about cash flow."
|
||
|
||
|
||
def test_validate_conv_85df636d3765_inline_report_shape():
|
||
payload = validate_report(
|
||
{
|
||
"version": 1,
|
||
"title": "گزارش واریز و برداشت — ۳ ماه ابتدای سال ۱۴۰۴",
|
||
"blocks": [
|
||
{"type": "text", "content": "مرور مالی ۳ ماه."},
|
||
{"type": "kpi", "label": "کل واریز", "value": "1", "unit": "تومان"},
|
||
{"type": "kpi", "label": "کل برداشت", "value": "2", "unit": "تومان"},
|
||
{"type": "kpi", "label": "خالص", "value": "-1", "unit": "تومان"},
|
||
{"type": "kpi", "label": "تعداد", "value": "222", "unit": "tx"},
|
||
{
|
||
"type": "table",
|
||
"headers": ["ماه", "واریز"],
|
||
"rows": [["فروردین", "1"]],
|
||
},
|
||
{
|
||
"type": "chart",
|
||
"chartType": "bar",
|
||
"data": [{"label": "فروردین", "value": 1}],
|
||
},
|
||
{
|
||
"type": "table",
|
||
"headers": ["دسته", "خالص"],
|
||
"rows": [["فروش", "+1"]],
|
||
},
|
||
{
|
||
"type": "callout",
|
||
"kind": "warning",
|
||
"content": "نکات کلیدی: فروردین مثبت بود.",
|
||
},
|
||
],
|
||
}
|
||
)
|
||
assert payload["blocks"][8]["text"].startswith("نکات کلیدی")
|
||
|
||
|
||
def test_validate_full_accounting_report_fixture():
|
||
data = json.loads(
|
||
(_FIXTURE_DIR / "_user_report_1404.json").read_text(encoding="utf-8")
|
||
)
|
||
payload = validate_report(data)
|
||
assert payload["title"].startswith("گزارش حسابداری")
|
||
assert len(payload["blocks"]) == 26
|
||
tables = [b for b in payload["blocks"] if b["type"] == "table"]
|
||
assert len(tables) == 3
|
||
assert tables[0]["columns"][0] == "دستهبندی"
|
||
|
||
|
||
def test_validate_report_rejects_too_many_rows():
|
||
rows = [{"a": i} for i in range(51)]
|
||
with pytest.raises(ReportValidationError):
|
||
validate_report(
|
||
{
|
||
"version": 1,
|
||
"title": "Big table",
|
||
"blocks": [{"type": "table", "columns": ["a"], "rows": rows}],
|
||
}
|
||
)
|
||
|
||
|
||
def test_validate_report_v2_datasets_and_multi_series():
|
||
payload = validate_report(
|
||
{
|
||
"version": 2,
|
||
"title": "Sales",
|
||
"subtitle": "Q3",
|
||
"datasets": {
|
||
"monthly": [
|
||
{"month": "تیر", "sales": 120, "profit": 31},
|
||
{"month": "مرداد", "sales": 132, "profit": 35},
|
||
]
|
||
},
|
||
"blocks": [
|
||
{
|
||
"type": "chart",
|
||
"chartType": "line",
|
||
"title": "روند",
|
||
"dataset": "monthly",
|
||
"x": "month",
|
||
"series": [
|
||
{"field": "sales", "label": "فروش"},
|
||
{"field": "profit", "label": "سود"},
|
||
],
|
||
"span": 12,
|
||
},
|
||
{
|
||
"type": "table",
|
||
"dataset": "monthly",
|
||
"columns": [
|
||
{"field": "month", "label": "ماه"},
|
||
{"field": "sales", "label": "فروش"},
|
||
],
|
||
},
|
||
{
|
||
"type": "image",
|
||
"src": "plot.png",
|
||
"caption": "شکل ۱",
|
||
},
|
||
{
|
||
"type": "quote",
|
||
"content": "Insight",
|
||
"source": "Kamidso",
|
||
},
|
||
],
|
||
}
|
||
)
|
||
assert payload["version"] == 2
|
||
assert payload["subtitle"] == "Q3"
|
||
chart = payload["blocks"][0]
|
||
assert len(chart["data"]) == 2
|
||
assert chart["series"][1]["field"] == "profit"
|
||
assert payload["blocks"][1]["rows"][0]["month"] == "تیر"
|
||
assert payload["blocks"][2]["type"] == "image"
|
||
assert payload["blocks"][3]["type"] == "quote"
|
||
|
||
|
||
def test_validate_report_rejects_nested_chart_data():
|
||
with pytest.raises(ReportValidationError, match="nested values"):
|
||
validate_report(
|
||
{
|
||
"version": 2,
|
||
"title": "Bad",
|
||
"blocks": [
|
||
{
|
||
"type": "chart",
|
||
"title": "روند فروش",
|
||
"x": "branch",
|
||
"series": [{"field": "sales"}],
|
||
"data": [
|
||
{"branch": "تهران", "metrics": {"sales": 1}},
|
||
],
|
||
}
|
||
],
|
||
}
|
||
)
|
||
|
||
|
||
def test_validate_report_missing_series_field_message():
|
||
with pytest.raises(ReportValidationError, match='field "profit"'):
|
||
validate_report(
|
||
{
|
||
"version": 2,
|
||
"title": "Bad",
|
||
"blocks": [
|
||
{
|
||
"type": "chart",
|
||
"title": "روند فروش",
|
||
"x": "month",
|
||
"series": [{"field": "profit"}],
|
||
"data": [{"month": "A", "sales": 1}],
|
||
}
|
||
],
|
||
}
|
||
)
|
||
|
||
|
||
def test_report_validation_fix_hint_includes_contract_and_block_example():
|
||
try:
|
||
validate_report(
|
||
{
|
||
"title": "x",
|
||
"blocks": [
|
||
{
|
||
"type": "chart",
|
||
"x": "m",
|
||
"series": [{"field": "n"}],
|
||
"data": [{"m": "a", "n": {"nested": 1}}],
|
||
}
|
||
],
|
||
}
|
||
)
|
||
except ReportValidationError as exc:
|
||
hint = report_validation_fix_hint(exc)
|
||
assert "Fix only the invalid" in hint
|
||
assert "chart" in hint.lower()
|
||
assert '{"type":"chart"' in hint
|
||
assert "version" in REPORT_JSON_CONTRACT
|
||
assert "Max 60 blocks" in REPORT_JSON_CONTRACT
|
||
return
|
||
raise AssertionError("expected ReportValidationError")
|