Files
InternalAuditInterprise/backend/tests/integration/test_clue_lifecycle.py
T
2026-06-16 00:38:57 +08:00

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)