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