Files
X-Financial/server/tests/test_hermes_risk_clue_collector.py

107 lines
4.3 KiB
Python
Raw Normal View History

from __future__ import annotations
from datetime import UTC, datetime
from decimal import Decimal
from sqlalchemy import create_engine
from sqlalchemy.orm import Session, sessionmaker
from sqlalchemy.pool import StaticPool
from app.db.base import Base
from app.models.financial_record import ExpenseClaim
from app.services.hermes_risk_clue_collector import HermesRiskClueCollectorService
from app.services.risk_observations import RiskObservationService
def build_session_factory() -> sessionmaker[Session]:
engine = create_engine(
"sqlite+pysqlite:///:memory:",
connect_args={"check_same_thread": False},
poolclass=StaticPool,
)
Base.metadata.create_all(bind=engine)
return sessionmaker(bind=engine, autoflush=False, autocommit=False)
def test_risk_clue_collector_outputs_review_packet_without_rule_writes() -> None:
forbidden_rule_execution = "执行" + "规则"
session_factory = build_session_factory()
with session_factory() as db:
claim = ExpenseClaim(
id="claim-risk-clue-1",
claim_no="RE-20260531090000-ABCDEFGH",
employee_name="张三",
department_name="销售部",
expense_type="travel",
reason="客户现场支持",
location="上海",
amount=Decimal("9800.00"),
currency="CNY",
invoice_count=2,
occurred_at=datetime(2026, 5, 30, 9, 0, tzinfo=UTC),
submitted_at=datetime(2026, 5, 31, 9, 0, tzinfo=UTC),
status="submitted",
approval_stage="财务审批",
risk_flags_json=[
{
"source": "rule_center",
"rule_code": "risk.travel.large_without_preapproval",
"label": "大额差旅缺少事前申请",
"message": "报销金额较高,未找到对应事前申请。",
"severity": "high",
}
],
)
db.add(claim)
db.flush()
RiskObservationService(db).upsert_observation(
{
"observation_key": "risk:claim-risk-clue-1:large_without_preapproval",
"subject_type": "expense_claim",
"subject_key": "claim:claim-risk-clue-1",
"subject_label": claim.claim_no,
"claim_id": claim.id,
"claim_no": claim.claim_no,
"risk_type": "preapproval_absent",
"risk_signal": "preapproval_absent",
"title": "大额差旅缺少事前申请",
"description": "报销金额较高,暂未匹配到事前申请,需要人工复核。",
"risk_score": 86,
"risk_level": "high",
"confidence_score": 0.82,
"source": "rule_center",
"contribution_scores": {"S_rule": 86},
"evidence": [
{
"source": "rule_center",
"title": "规则命中",
"detail": "金额 9800 元,缺少事前申请。",
}
],
"policy_refs": ["risk.travel.large_without_preapproval"],
}
)
db.commit()
packet = HermesRiskClueCollectorService(db).collect_risk_clues(run_id="run-risk-clue")
assert packet["task_type"] == "risk_clue_collect"
assert packet["writes_rules"] is False
assert packet["human_review_required"] is True
assert "主流程由外层智能体执行" in packet["role_boundary"]
assert forbidden_rule_execution not in packet["role_boundary"]
assert packet["fact_count"] == 1
assert packet["rule_hit_count"] >= 1
assert packet["risk_clue_count"] >= 1
assert packet["facts"][0]["claim_kind"] == "reimbursement"
assert packet["risk_clues"][0]["status"] == "human_review_required"
assert packet["risk_clues"][0]["observation_key"]
assert packet["risk_clues"][0]["feedback_status"] == "unreviewed"
assert packet["risk_clues"][0]["next_action"]
assert "recent" in packet["feedback_summary"]
assert packet["risk_clues"][0]["not_final_conclusion"] is True
serialized = str(packet)
assert "auto_publish" not in serialized
assert "candidate_risk_rules" not in serialized