88 lines
3.0 KiB
Python
88 lines
3.0 KiB
Python
"""线索闭环 + 系统自审计集成测试(需 PostgreSQL)。
|
|
|
|
覆盖 R7/R17/R18/R19:线索生成与分级、状态流转、底稿、审计哈希链、线索不可删。
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
from sqlalchemy import text
|
|
from sqlalchemy.exc import InternalError, ProgrammingError
|
|
|
|
from app.audit import service as audit
|
|
from app.clues import service as clue_svc
|
|
from app.clues.models import ClueStatus, ConfidenceTier
|
|
|
|
|
|
def _new_clue(session, score=0.9):
|
|
return clue_svc.create_clue(
|
|
session,
|
|
title="疑似政企拆单",
|
|
risk_domain="收入",
|
|
scenario_code="R8",
|
|
score=score,
|
|
rationale="8 个客户金额集中在审批阈值边缘,且法人关联同一实控人",
|
|
evidence={"contracts": 8, "threshold": 1000000},
|
|
amount_involved=4800000,
|
|
actor="system",
|
|
)
|
|
|
|
|
|
def test_score_to_confidence_tier():
|
|
assert clue_svc.score_to_tier(0.9) == ConfidenceTier.HIGH
|
|
assert clue_svc.score_to_tier(0.6) == ConfidenceTier.MEDIUM
|
|
assert clue_svc.score_to_tier(0.2) == ConfidenceTier.LOW
|
|
|
|
|
|
def test_clue_full_lifecycle(session):
|
|
clue = _new_clue(session)
|
|
assert clue.confidence == ConfidenceTier.HIGH
|
|
assert clue.status == ClueStatus.NEW
|
|
|
|
clue_svc.assign(session, clue, assignee="auditor_zhang", actor="manager_li")
|
|
assert clue.status == ClueStatus.ASSIGNED
|
|
assert clue.assignee == "auditor_zhang"
|
|
|
|
paper = clue_svc.adjudicate(session, clue, confirmed=True, actor="auditor_zhang", note="属实,移交")
|
|
assert clue.status == ClueStatus.CONFIRMED
|
|
assert clue.feedback == "confirmed"
|
|
assert paper.conclusion == "confirmed"
|
|
assert paper.snapshot["score"] == 0.9
|
|
|
|
# 继续闭环:确认 -> 移交 -> 销项
|
|
clue_svc.transition(session, clue, ClueStatus.TRANSFERRED, actor="manager_li")
|
|
clue_svc.transition(session, clue, ClueStatus.CLOSED, actor="manager_li")
|
|
assert clue.status == ClueStatus.CLOSED
|
|
|
|
|
|
def test_illegal_transition_rejected(session):
|
|
clue = _new_clue(session)
|
|
with pytest.raises(clue_svc.IllegalTransitionError):
|
|
# NEW 不能直接到 CLOSED
|
|
clue_svc.transition(session, clue, ClueStatus.CLOSED, actor="x")
|
|
|
|
|
|
def test_audit_hash_chain_integrity(session):
|
|
_new_clue(session)
|
|
clue = _new_clue(session)
|
|
clue_svc.assign(session, clue, "auditor_zhang", "manager_li")
|
|
ok, broken = audit.verify_chain(session)
|
|
assert ok is True
|
|
assert broken is None
|
|
|
|
|
|
def test_clue_cannot_be_deleted(session):
|
|
"""R19:数据库触发器禁止物理删除线索。"""
|
|
clue = _new_clue(session)
|
|
session.flush()
|
|
with pytest.raises((InternalError, ProgrammingError)):
|
|
session.execute(text("DELETE FROM clue WHERE id = :i"), {"i": clue.id})
|
|
session.flush()
|
|
|
|
|
|
def test_list_clues_filters(session):
|
|
_new_clue(session, score=0.9)
|
|
_new_clue(session, score=0.3)
|
|
highs = clue_svc.list_clues(session, confidence=ConfidenceTier.HIGH)
|
|
assert all(c.confidence == ConfidenceTier.HIGH for c in highs)
|