127 lines
4.1 KiB
Python
127 lines
4.1 KiB
Python
import json
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
from finance.formula_columns import available_metrics, ensure_available_metrics
|
|
from finance.formula_views import design_column
|
|
|
|
pytestmark = pytest.mark.django_db
|
|
|
|
|
|
def model_response(design):
|
|
return type(
|
|
"Response",
|
|
(),
|
|
{
|
|
"raise_for_status": lambda self: None,
|
|
"json": lambda self: {
|
|
"status": "completed",
|
|
"output": [{"content": [{"type": "output_text", "text": json.dumps(design)}]}],
|
|
},
|
|
},
|
|
)()
|
|
|
|
|
|
def test_available_metrics_follow_dataset_mapping(workspace):
|
|
ds, *_ = workspace
|
|
assert set(available_metrics(ds)) == {
|
|
"revenue",
|
|
"cost",
|
|
"gross",
|
|
"roi",
|
|
"customers",
|
|
"basket",
|
|
"margin",
|
|
"discount_rate",
|
|
"self_revenue",
|
|
"upgrade_rate",
|
|
"list_basket",
|
|
}
|
|
|
|
ds.mapping = {
|
|
**ds.mapping,
|
|
"columns": [
|
|
column
|
|
for column in ds.mapping["columns"]
|
|
if column["role"] not in {"cost", "gross", "customer", "list_amount"}
|
|
],
|
|
"self_operated_products": [],
|
|
"margin_rates": {},
|
|
}
|
|
assert set(available_metrics(ds)) == {"revenue"}
|
|
with pytest.raises(ValueError, match="消费人数"):
|
|
ensure_available_metrics(ds, {"expression": {"metric": "customers"}})
|
|
|
|
|
|
def test_model_cannot_complete_definition_using_unavailable_metrics(workspace, settings):
|
|
ds, *_ = workspace
|
|
settings.AI_ENABLED = True
|
|
settings.OPENAI_API_KEY = "test-key"
|
|
settings.OPENAI_MODEL = "test-model"
|
|
definition = {
|
|
"name": "收入乘消费人数",
|
|
"meaning": "实际收入乘消费人数",
|
|
"scope": "按所选分组和期间",
|
|
"missing": "结果不可用(#N/A);零分母不可用",
|
|
"expression": {
|
|
"operator": "multiply",
|
|
"left": {"metric": "revenue"},
|
|
"right": {"metric": "customers"},
|
|
},
|
|
}
|
|
ds.mapping = {
|
|
**ds.mapping,
|
|
"columns": [column for column in ds.mapping["columns"] if column["role"] != "customer"],
|
|
}
|
|
with patch("finance.formula_views.httpx.Client") as client:
|
|
client.return_value.__enter__.return_value.post.return_value = model_response(
|
|
{"complete": True, "questions": [], "column": definition}
|
|
)
|
|
result = design_column(
|
|
[{"role": "user", "content": "增加收入乘消费人数列"}],
|
|
ds,
|
|
{"current": ["2026-08"], "baseline": ["2025-08"], "filters": {}, "group": "department"},
|
|
)
|
|
|
|
assert result["complete"] is False
|
|
assert result["column"] is None
|
|
assert "消费人数" in result["questions"][0]
|
|
payload = client.return_value.__enter__.return_value.post.call_args.kwargs["json"]
|
|
system = payload["input"][0]["content"]
|
|
assert '"customers": "消费人数"' in system.split('"unavailable_metrics":', 1)[1]
|
|
metric_schema = payload["text"]["format"]["schema"]["properties"]["column"]["anyOf"][1]["properties"][
|
|
"expression"
|
|
]["anyOf"][0]["properties"]["metric"]
|
|
assert "customers" not in metric_schema["enum"]
|
|
assert "revenue" in metric_schema["enum"]
|
|
|
|
|
|
def test_export_api_rejects_unavailable_metric_even_without_design_dialog(workspace):
|
|
ds, _, client, *_ = workspace
|
|
definition = {
|
|
"name": "人数列",
|
|
"meaning": "消费人数",
|
|
"scope": "按分组期间",
|
|
"missing": "结果不可用(#N/A);零分母不可用",
|
|
"expression": {"metric": "customers"},
|
|
}
|
|
ds.mapping = {
|
|
**ds.mapping,
|
|
"columns": [column for column in ds.mapping["columns"] if column["role"] != "customer"],
|
|
}
|
|
ds.save(update_fields=["mapping"])
|
|
response = client.post(
|
|
f"/api/datasets/{ds.pk}/formula-exports",
|
|
data={
|
|
"start": "2026-08",
|
|
"end": "2026-08",
|
|
"comparison": "yoy",
|
|
"group": "department",
|
|
"custom_columns": [definition],
|
|
},
|
|
content_type="application/json",
|
|
)
|
|
assert response.status_code == 400
|
|
assert "消费人数" in response.json()["error"]
|