Collaborative-Fraud-Intelligence-Simulator / backend /tests /unit /test_advanced_explainability.py
yusufcalisir's picture
deploy: Hugging Face space upload
eb0499d
Raw History Blame Contribute Delete
5.75 kB
"""Unit tests for Advanced AI Explainability Portal (Counterfactuals, Decision Replay, GNNExplainer)."""
from __future__ import annotations
from app.application.services.explainability_service import ExplainabilityService
from app.domain.enums import AlertSeverity, AlertStatus
from app.domain.investigation_entities import Alert
def _make_sample_alert(alert_id: str = "alt_1001", risk_score: float = 750.0) -> Alert:
return Alert(
id=alert_id,
bank_id="bank_a",
transaction_id="tx_9988",
risk_score=risk_score,
severity=AlertSeverity.HIGH,
status=AlertStatus.NEW,
reason_codes=["HIGH-AMT", "GEO-RISK", "VEL-001"],
confidence=0.92,
involved_entity_ids=["cust_a1"],
top_features=[{"feature": "transaction_amount", "contribution": 0.42}],
risk_factors=["Transaction amount exceeds threshold"],
historical_evidence=["Prior alert 2 days ago"],
model_confidence=0.92,
)
class TestCounterfactualEngine:
"""Verify counterfactual remediation path generation."""
def test_generate_counterfactuals_clears_alert(self):
svc = ExplainabilityService()
alert = _make_sample_alert(risk_score=750.0)
cf = svc.generate_counterfactuals(alert, target_score=350.0)
assert cf.alert_id == alert.id
assert cf.original_score == 750.0
assert cf.remediated_score <= 350.0
assert cf.is_cleared is True
assert len(cf.changes) >= 1
assert "CLEARED" in cf.summary_text
def test_counterfactual_remediates_high_amount_and_geo(self):
svc = ExplainabilityService()
alert = _make_sample_alert(risk_score=850.0)
cf = svc.generate_counterfactuals(alert, target_score=300.0)
features_changed = [c.feature for c in cf.changes]
assert "transaction_amount" in features_changed or "country_code" in features_changed
def test_counterfactual_executes_real_risk_engine_search(self):
"""Verifies that counterfactual remediated_score is computed by real RiskScoringEngine and strictly clears target threshold."""
from app.application.services.risk_engine import RiskScoringEngine
svc = ExplainabilityService()
engine = RiskScoringEngine()
alert = _make_sample_alert(risk_score=850.0)
target_threshold = 350.0
result = svc.generate_counterfactuals(alert, target_score=target_threshold, risk_engine=engine)
assert result.original_score == 850.0
assert result.remediated_score <= target_threshold
assert result.is_cleared is True
assert len(result.changes) >= 1
assert isinstance(result.remediated_score, float)
def test_counterfactual_honest_failure_when_unreachable(self):
"""Verifies that when target threshold is deliberately unreachable within the search budget (e.g. 10.0),
the search reports failure honestly rather than forcing a fake cleared status.
"""
from app.application.services.risk_engine import RiskScoringEngine
svc = ExplainabilityService()
engine = RiskScoringEngine()
alert = _make_sample_alert(risk_score=850.0)
unreachable_target = 10.0
result = svc.generate_counterfactuals(alert, target_score=unreachable_target, risk_engine=engine)
# Confirm search reports failure honestly
assert result.original_score == 850.0
assert result.is_cleared is False
assert result.remediated_score <= result.original_score
assert result.remediated_score > unreachable_target
assert "did not reach" in result.summary_text
class TestDecisionReplayAudit:
"""Verify deterministic decision replay inference audit."""
def test_replay_inference_audit_reproduces_score(self):
svc = ExplainabilityService()
alert = _make_sample_alert(risk_score=680.0)
audit = svc.replay_inference_audit(alert)
assert audit.alert_id == alert.id
assert audit.model_version == "v1.4.2-champion"
assert audit.model_auc > 0.90
assert len(audit.policy_rules_evaluated) == 9
assert audit.reconstructed_risk_score > 0
assert audit.audit_matched is not None
class TestGNNExplainer:
"""Verify GNNExplainer graph attribution over entity neighborhood."""
def test_explain_gnn_embedding_calculates_edge_contributions(self):
from app.application.services.graph_engine import GraphEngine
from app.domain.enums import EntityType, RelationshipType
from app.domain.investigation_entities import Entity, Relationship
ge = GraphEngine()
ge.register_entity(
Entity(id="cust_a1", entity_type=EntityType.CUSTOMER, privacy_id="priv_a1", bank_id="bank_a")
)
ge.register_entity(
Entity(id="dev_x99", entity_type=EntityType.DEVICE, privacy_id="priv_x99", bank_id="bank_a")
)
ge.add_relationship(
Relationship(
source_entity_id="cust_a1",
target_entity_id="dev_x99",
relationship_type=RelationshipType.SHARES_DEVICE,
)
)
svc = ExplainabilityService()
gnn_exp = svc.explain_gnn_embedding("cust_a1")
assert gnn_exp.node_id == "cust_a1"
assert gnn_exp.subgraph_nodes_count >= 1
assert len(gnn_exp.top_contributing_edges) >= 1
assert "Primary GNN Driver" in gnn_exp.primary_driver_text
assert gnn_exp.top_contributing_edges[0].target == "dev_x99"
# Verify sum of contribution percentages equals 100%
total_pct = sum(e.contribution_percentage for e in gnn_exp.top_contributing_edges)
assert abs(total_pct - 100.0) < 1.0