From 37738b4d0b2ea7a9a2b6d76bb3260bd547346ca4 Mon Sep 17 00:00:00 2001 From: CarterPerez-dev Date: Sat, 28 Feb 2026 07:13:30 -0500 Subject: [PATCH] test: add end-to-end training integration test --- .../backend/tests/test_training_e2e.py | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 PROJECTS/advanced/ai-threat-detection/backend/tests/test_training_e2e.py diff --git a/PROJECTS/advanced/ai-threat-detection/backend/tests/test_training_e2e.py b/PROJECTS/advanced/ai-threat-detection/backend/tests/test_training_e2e.py new file mode 100644 index 00000000..fcdd467d --- /dev/null +++ b/PROJECTS/advanced/ai-threat-detection/backend/tests/test_training_e2e.py @@ -0,0 +1,97 @@ +""" +©AngelaMos | 2026 +test_training_e2e.py +""" + +from pathlib import Path + + +from app.core.detection.ensemble import ( + blend_scores, + fuse_scores, + normalize_ae_score, + normalize_if_score, +) +from app.core.detection.inference import InferenceEngine +from ml.orchestrator import TrainingOrchestrator +from ml.synthetic import generate_mixed_dataset + +N_NORMAL = 500 +N_ATTACK = 200 +N_FEATURES = 35 +ENSEMBLE_WEIGHTS = {"ae": 0.4, "rf": 0.4, "if": 0.2} + + +class TestTrainingE2E: + """ + End-to-end training integration test + """ + + def test_full_training_produces_loadable_models( + self, tmp_path: Path + ) -> None: + """ + Full pipeline produces models that load and predict + """ + X, y = generate_mixed_dataset( + N_NORMAL, N_ATTACK + ) + assert X.shape == ( + N_NORMAL + N_ATTACK, + N_FEATURES, + ) + + model_dir = tmp_path / "models" + orch = TrainingOrchestrator( + output_dir=model_dir, epochs=3 + ) + result = orch.run(X, y) + + expected_files = [ + "ae.onnx", + "rf.onnx", + "if.onnx", + "scaler.json", + "threshold.json", + ] + for filename in expected_files: + assert ( + model_dir / filename + ).exists(), f"Missing {filename}" + + engine = InferenceEngine(str(model_dir)) + assert engine.is_loaded + + sample = X[:5] + predictions = engine.predict(sample) + assert predictions is not None + assert "ae" in predictions + assert "rf" in predictions + assert "if" in predictions + assert len(predictions["ae"]) == 5 + + threshold = engine.threshold + for i in range(5): + ae_score = normalize_ae_score( + predictions["ae"][i], threshold + ) + if_score = normalize_if_score( + predictions["if"][i] + ) + rf_score = predictions["rf"][i] + + scores = { + "ae": ae_score, + "rf": rf_score, + "if": if_score, + } + fused = fuse_scores( + scores, ENSEMBLE_WEIGHTS + ) + assert 0.0 <= fused <= 1.0 + + blended = blend_scores(fused, 0.0) + assert 0.0 <= blended <= 1.0 + + assert result.passed_gates is not None + assert isinstance(result.passed_gates, bool)