Files
group_fqcd_jr/tests/unit/service/test_financial_nl2sql_service.py
T
张胜宇 e239eb778b docs: 品牌全量口径统一为「南方基金」+ 作废文档清理
1) 客服 Agent 四份交付文档 + 构建脚手架:品牌由包装占位 XX科技 / 旧名 南方财富
   统一为南方基金(热线 400-889-8899 / 官网 nffund.com),系统名改为「智能服务系统」;
   同步追加 §0.4 修订记录行,工程记录行保留原占位字面以支撑硬编码扫描验收。
2) 开发文档:清理 28 份已作废/残留文档(14 份移出归档 + 14 份仓库副本),
   新增《文档规整方案与开发前待决事项-2026-09-17》。
3) 客服agent 四份交付文档首次纳入本分支。
2026-09-17 15:15:22 +08:00

131 lines
4.8 KiB
Python
Raw 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.
from datetime import date
import pytest
from app.core.contracts import RequestContext
from app.core.nl2sql_contracts import FinancialNL2SQLInput
from app.service.financial_nl2sql_service import FinancialNL2SQLService
def context(**updates):
base = RequestContext(
user_id="1",
trace_id="trace-nl2sql",
roles=("advisor",),
permissions=("financial:nl2sql:read",),
data_scope="all",
)
return base.model_copy(update=updates)
def test_query_rows_convert_date_values_to_json_strings() -> None:
row = FinancialNL2SQLService._jsonable({"nav_date": date(2026, 9, 10)})
assert row == {"nav_date": "2026-09-10"}
@pytest.mark.asyncio
async def test_generates_read_only_market_sql_with_product_filter() -> None:
result = await FinancialNL2SQLService().query(
FinancialNL2SQLInput(question="查询159511近30天行情收盘价", dry_run=True),
context(),
)
assert result["status"] == "ready"
assert result["sql"].startswith("SELECT ")
assert "fin_market_price" in result["sql"]
assert "DROP" not in result["sql"]
assert result["parameters"]["filter_0"] == "159511"
assert result["audit"]["permission_check"]["status"] == "passed"
@pytest.mark.asyncio
async def test_low_confidence_question_requires_confirmation() -> None:
result = await FinancialNL2SQLService().query(
FinancialNL2SQLInput(question="帮我看看这个情况", dry_run=True),
context(),
)
assert result["status"] == "need_confirmation"
assert result["query_plan"]["confidence"] < 0.85
@pytest.mark.asyncio
async def test_as_of_query_rejects_current_snapshot_tables() -> None:
result = await FinancialNL2SQLService().query(
FinancialNL2SQLInput(question="截至某日客户当前持仓市值", dry_run=True),
context(),
)
assert result["status"] == "rejected"
assert "历史版本" in result["message"]
@pytest.mark.asyncio
async def test_customer_scope_is_injected_for_non_all_scope() -> None:
result = await FinancialNL2SQLService().query(
FinancialNL2SQLInput(question="查询客户账户余额", dry_run=True),
context(data_scope="own_customers", customer_ids=("7", "8")),
)
assert "customer_id IN" in result["sql"]
assert result["parameters"]["scope_customer_0"] == 7
assert result["parameters"]["scope_customer_1"] == 8
@pytest.mark.asyncio
async def test_cash_ledger_is_in_nl2sql_scope() -> None:
result = await FinancialNL2SQLService().query(
FinancialNL2SQLInput(question="查询近7天资金流水变化", dry_run=True),
context(),
)
assert result["status"] == "ready"
assert "fin_cash_ledger" in result["sql"]
assert result["query_plan"]["intent"] == "cash_ledger_query"
# --- 产品代码提取:5 位与 6 位并存(2026-09-14 修) --------------------------------
#
# 原先只认 `\d{6}`,5 位代码(演示产品 `15911`)被静默丢弃 ⇒ 查询**不带产品过滤**,
# 却仍返回 `status=success`:实测问 15911 的净值拿到的是 511810 的数据。
# 「看起来成功、数据却是错的」比报错更危险,所以这几条要钉住。
@pytest.mark.asyncio
async def test_five_digit_product_code_is_used_as_filter() -> None:
result = await FinancialNL2SQLService().query(
FinancialNL2SQLInput(question="查询15911的净值", dry_run=True),
context(),
)
assert result["status"] == "ready"
assert result["parameters"]["filter_0"] == "15911"
# 关键:SQL 里必须**真的带上产品过滤**(列名走别名 `p.`),否则就是"成功但查错数据"。
assert "p.product_code = :filter_0" in result["sql"]
@pytest.mark.asyncio
async def test_six_digit_product_code_still_works() -> None:
result = await FinancialNL2SQLService().query(
FinancialNL2SQLInput(question="查询510300的净值", dry_run=True),
context(),
)
assert result["parameters"]["filter_0"] == "510300"
@pytest.mark.asyncio
async def test_amount_like_five_digit_number_is_not_treated_as_product_code() -> None:
"""「申购金额50000元」里的 50000 是金额,不是产品代码 —— 它后面紧跟单位。"""
result = await FinancialNL2SQLService().query(
FinancialNL2SQLInput(question="查询申购金额50000元的记录", dry_run=True),
context(),
)
assert "filter_0" not in result.get("parameters", {})
def test_product_codes_ignores_years_and_short_numbers() -> None:
from app.service.financial_nl2sql_service import _product_codes
assert _product_codes("查询2026年的净值") == [] # 4 位年份
assert _product_codes("查询近30天净值") == [] # 天数
assert _product_codes("查询15911的净值") == ["15911"]
assert _product_codes("对比15911和510300") == ["15911", "510300"]