Collaborative-Fraud-Intelligence-Simulator / verification /explainability /tests /test_explainability_robustness.py
Download verification/explainability/tests/test_explainability_robustness.py from yusufcalisir/Collaborative-Fraud-Intelligence-Simulator: direct link, hf CLI and curl.
- Browser
- Download file 8.39 kB
-
https://huggingface.co/spaces/yusufcalisir/Collaborative-Fraud-Intelligence-Simulator/resolve/main/verification/explainability/tests/test_explainability_robustness.py
- Command line
-
hf download hf://spaces/yusufcalisir/Collaborative-Fraud-Intelligence-Simulator/verification/explainability/tests/test_explainability_robustness.py
-
curl -L -o test_explainability_robustness.py https://huggingface.co/spaces/yusufcalisir/Collaborative-Fraud-Intelligence-Simulator/resolve/main/verification/explainability/tests/test_explainability_robustness.py
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"]) | |