yusufcalisir's picture
deploy: Hugging Face space upload
eb0499d
Raw History Blame Contribute Delete
8.39 kB
"""Robustness & Failure Injection Test Suite for Explainability (XAI) Subsystem.
Stress-tests every explainability algorithm by injecting hostile boundary conditions:
GEX1: NaN Risk Score in Alert
GEX2: NaN Feature Values in Transaction Dict
GEX3: +Inf / -Inf Feature Amounts & Risk Scores
GEX4: Empty Transaction Dictionary ({})
GEX5: Empty Alert Reason Codes & Risk Factors
GEX6: Missing Features (9 out of 10 features absent)
GEX7: Malformed Feature Data Types (strings, lists, None for numeric features)
GEX8: Extreme Floating-Point Values (1e308, -1e308, 1e-308)
GEX9: Empty Category & MCC Strings in Real-Time Explainer
GEX10: Empty Feature Vector List ([]) in Real-Time SHAP Engine
GEX11: Path Traversal & Special Characters in Transaction ID
GEX12: Non-Existent Node ID in GNN Embedding Explainer
GEX13: Target Counterfactual Score Higher than Original Score
GEX14: Extremely Low Target Counterfactual Score (0.0 or negative)
"""
from __future__ import annotations
from pathlib import Path
import sys
import math
import pytest
import numpy as np
PROJECT_ROOT = str(Path(__file__).resolve().parents[3] / "backend")
sys.path.insert(0, PROJECT_ROOT)
from app.application.services.explainability_service import ExplainabilityService
from app.domain.realtime_explainer import FastInferenceExplainer
from app.domain.investigation_entities import Alert, AlertSeverity
explainer_service = ExplainabilityService()
fast_explainer = FastInferenceExplainer()
def assert_finite(val: float, name: str) -> None:
"""Assert that a value is non-NaN and non-Inf."""
assert not math.isnan(val), f"{name} must not be NaN, got {val}"
assert not math.isinf(val), f"{name} must not be Inf, got {val}"
# =====================================================================
# GEX1 & GEX2: NaN Inputs
# =====================================================================
def test_gex1_nan_risk_score_in_alert():
"""GEX1: NaN alert risk_score - must not crash or return NaN breakdown scores."""
alert = Alert(
id="alt_nan_01",
risk_score=float("nan"),
severity=AlertSeverity.HIGH,
model_confidence=0.85,
bank_id="bank_a",
reason_codes=["ML-HIGH"],
)
try:
rpt = explainer_service.explain_alert(alert)
for s in rpt.risk_score_breakdown:
assert_finite(s.normalized_score, "normalized_score")
except (ValueError, FloatingPointError):
pass # Explicit domain exception is acceptable
def test_gex2_nan_feature_values_in_shap():
"""GEX2: NaN feature values in compute_shap_values - must fail-closed deterministically (INV-C/INV-08)."""
txn_dict = {
"transaction_amount": float("nan"),
"velocity": float("nan"),
"merchant_risk_score": 0.5,
}
with pytest.raises(ValueError, match="Feature 'transaction_amount' contains non-finite value: nan"):
explainer_service.compute_shap_values(txn_dict)
# =====================================================================
# GEX3: Infinite (+/-Inf) Values
# =====================================================================
def test_gex3_positive_infinity_amount():
"""GEX3a: +Inf transaction_amount - must fail-closed deterministically (INV-C/INV-08)."""
txn_dict = {"transaction_amount": float("inf"), "velocity": 5.0}
with pytest.raises(ValueError, match="Feature 'transaction_amount' contains non-finite value: inf"):
explainer_service.compute_shap_values(txn_dict)
def test_gex3_negative_infinity_risk_score():
"""GEX3b: -Inf alert risk_score - successfully handled post-remediation (BUG-EX-01 fixed)."""
alert = Alert(
id="alt_inf_01",
risk_score=float("-inf"),
severity=AlertSeverity.LOW,
model_confidence=0.5,
bank_id="bank_a",
reason_codes=[],
)
report = explainer_service.explain_alert(alert)
assert report is not None
assert report.alert_id == "alt_inf_01"
def test_gex7_malformed_feature_data_types():
"""GEX7: Malformed string amount - must fail-closed deterministically without coercion (INV-D/INV-09)."""
txn_dict = {
"transaction_amount": "invalid_string_1000",
"velocity": [1, 2, 3],
"merchant_risk_score": None,
}
with pytest.raises(ValueError, match="Invalid numeric value for feature 'transaction_amount'"):
explainer_service.compute_shap_values(txn_dict)
# =====================================================================
# GEX8: Extreme Floating-Point Values (1e308, -1e308, 1e-308)
# =====================================================================
def test_gex8_extreme_floating_point_values():
"""GEX8: Extreme values (1e308) in compute_shap_values - must not overflow."""
txn_dict = {
"transaction_amount": 1e308,
"velocity": 1e-308,
"account_age_days": -1e308,
}
contributions = explainer_service.compute_shap_values(txn_dict)
assert len(contributions) == 10
for f in contributions:
assert_finite(f["contribution"], f"contribution for {f['feature']}")
# =====================================================================
# GEX9 & GEX10: Real-Time Explainer Edge Cases
# =====================================================================
def test_gex9_empty_category_strings_realtime():
"""GEX9: Empty string merchant_category in explain_realtime_score - no exception."""
attributions = fast_explainer.explain_realtime_score(
amount=100.0, velocity_1h=1, merchant_category="", risk_score=0.1
)
assert isinstance(attributions, list)
def test_gex10_empty_feature_vector_list_realtime():
"""GEX10: Empty list [] passed as feature_vector to compute_shap - returns fallback attributions."""
res = fast_explainer.compute_shap("tx_empty_01", [])
assert res["status"] == "COMPLETED"
assert isinstance(res["shap_values"], list)
# =====================================================================
# GEX11: Path Traversal & Special Characters in Transaction ID
# =====================================================================
def test_gex11_path_traversal_transaction_id():
"""GEX11: Transaction ID with path traversal `../../etc/passwd` - sanitized key in Redis/cache."""
res = fast_explainer.compute_shap("../../etc/passwd", {"amount": 500.0})
assert res["transaction_id"] == "../../etc/passwd"
assert res["status"] == "COMPLETED"
# =====================================================================
# GEX12: Non-Existent Node ID in GNN Explainer
# =====================================================================
def test_gex12_non_existent_gnn_node():
"""GEX12: Non-existent node_id in explain_gnn_embedding - safely returns zero-edge isolated report."""
gnn_rpt = explainer_service.explain_gnn_embedding("non_existent_node_99999")
assert gnn_rpt.node_id == "non_existent_node_99999"
assert len(gnn_rpt.top_contributing_edges) == 0
assert gnn_rpt.subgraph_edges_count == 0
assert "Isolated entity" in gnn_rpt.primary_driver_text
# =====================================================================
# GEX13 & GEX14: Counterfactual Edge Cases
# =====================================================================
def test_gex13_target_score_higher_than_original():
"""GEX13: Target score higher than original alert score - is_cleared must be True."""
alert = Alert(
id="alt_cf_01",
risk_score=200.0,
severity=AlertSeverity.LOW,
model_confidence=0.9,
bank_id="bank_a",
reason_codes=[],
)
cf = explainer_service.generate_counterfactuals(alert, target_score=350.0)
assert cf.is_cleared is True
assert cf.remediated_score <= 350.0
def test_gex14_extreme_low_target_score():
"""GEX14: Target score = 0.0 - counterfactuals clamp remediated score to min 50.0."""
alert = Alert(
id="alt_cf_02",
risk_score=800.0,
severity=AlertSeverity.CRITICAL,
model_confidence=0.95,
bank_id="bank_a",
reason_codes=["HIGH-AMT", "GEO-RISK"],
)
cf = explainer_service.generate_counterfactuals(alert, target_score=0.0)
assert cf.remediated_score >= 50.0 # Clamped to minimum 50.0
assert isinstance(cf.changes, list)
if __name__ == "__main__":
pytest.main([__file__, "-v"])