Files
javadandCursor b93ecf1691 Initial commit of the Kamidso API.
Keep source, config, and deploy files. Leave runtime data, uploads, and logs out of git.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-10 13:43:09 +03:30

347 lines
11 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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")