Files
group_fqcd_jr/tests/unit/service/test_risk_scan_robustness.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

74 lines
3.0 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.
"""扫描健壮性:脏数据不该让整批停摆,调度器不该"重启后再也不跑"。
这两条都属于"风控看起来在工作、其实没在跑"那类缺陷,现象上很难发现:
1. `_level_value` 原先直接 `int(level.replace(prefix, ""))` —— 一条等级字段脏的数据
(比如写了「中风险」)会抛 ValueError,冒到 `scan()` 的兜底 → **整批 rollback**,
本次扫描前面已经生成的预警全部作废。
2. 调度器的 `last_run_at` 只在内存,重启后为 None,而 `_is_due` 此时返回
`config.run_immediately`(默认 False)→ **重启后永不执行**,且没有任何告警。
"""
import datetime as dt
from types import SimpleNamespace
from typing import Any, cast
from app.service.risk_scan_service import _level_value
from app.worker.risk_scan_scheduler import RiskScanSchedulerWorker
def test_level_value_parses_valid_levels() -> None:
assert _level_value("R2", "R") == 2
assert _level_value("C3", "C") == 3
assert _level_value("c5", "C") == 5 # 大小写不敏感
assert _level_value(" 2 ", "R") == 2 # 历史数据可能不带前缀
def test_level_value_returns_none_instead_of_raising_on_dirty_data() -> None:
"""脏数据返回 None,不抛异常 —— 这是不让整批扫描 rollback 的前提。"""
assert _level_value("中风险", "R") is None
assert _level_value("", "R") is None
assert _level_value("R", "R") is None
assert _level_value("R6", "R") == 6 # 越界值交给上层比较,解析本身只负责取数字
def test_level_value_does_not_over_strip() -> None:
"""用 removeprefix 而非 replace:`R2R` 不该被错当成 2。"""
assert _level_value("R2R", "R") is None
def _scheduler(*, last_run_at: dt.datetime | None) -> RiskScanSchedulerWorker:
"""只构造被测方法用到的那两个属性。"""
scheduler = object.__new__(RiskScanSchedulerWorker)
scheduler.last_run_at = last_run_at
return scheduler
def _config(*, run_immediately: bool, interval_minutes: int = 30) -> Any:
return SimpleNamespace(run_immediately=run_immediately, interval_minutes=interval_minutes)
_NOW = dt.datetime(2026, 9, 11, 12, 0, tzinfo=dt.UTC)
def test_first_run_is_due_even_when_run_immediately_is_false() -> None:
"""进程刚起来(last_run_at 为 None)必须视为 due,哪怕是 run_immediately=False。
原先这里返回 run_immediately,默认 False ⇒ 重启后永不执行。
"""
scheduler = cast(Any, _scheduler(last_run_at=None))
assert scheduler._is_due(_config(run_immediately=False), _NOW) is True
def test_not_due_before_interval_elapses() -> None:
scheduler = cast(Any, _scheduler(last_run_at=_NOW - dt.timedelta(minutes=10)))
assert scheduler._is_due(_config(run_immediately=False), _NOW) is False
def test_due_after_interval_elapses() -> None:
scheduler = cast(Any, _scheduler(last_run_at=_NOW - dt.timedelta(minutes=31)))
assert scheduler._is_due(_config(run_immediately=False), _NOW) is True