Files
ai-exchange/tests/unit/test_alert_quality_summary_service.py
T
2026-05-12 08:30:47 +08:00

179 lines
9.6 KiB
Python

import unittest
from src.services.alerts import AlertQualitySummaryService
class _DecisionRepository:
def fetch_recent_decision_alerts(self, *, instrument_id: int, limit: int = 50):
return [
{"id": 1, "alert_type": "candidate_actionable", "payload": {"session_code": "LONDON"}},
{"id": 2, "alert_type": "runtime_degraded", "payload": {"session_code": "NY_AM"}},
][:limit]
class _DeliveryRepository:
def fetch_recent_deliveries_for_alerts(self, alert_ids, limit: int = 200):
return [
{"alert_id": 1, "status": "delivered", "channel": "webhook", "attempted_ts": "2026-05-11T10:00:00+00:00", "payload": {"dispatch_mode": "failed_retry", "result": {"attempt_count": 2}, "media_evidence": {"status": "media_linked"}}},
{"alert_id": 2, "status": "failed", "channel": "webhook", "attempted_ts": "2026-05-11T09:59:00+00:00", "payload": {"media_evidence": {"status": "media_unavailable"}}},
{"alert_id": 1, "status": "skipped", "channel": "workbench", "attempted_ts": "2026-05-11T09:58:00+00:00", "payload": {"reason": "retry_exhausted", "media_evidence": {"status": "media_attached"}}},
{"alert_id": 2, "status": "skipped", "channel": "webhook", "attempted_ts": "2026-05-11T09:57:00+00:00", "payload": {"reason": "suppressed_noise_policy", "delivery_policy": {"action": "suppressed_noise_policy"}, "media_evidence": {"status": "media_linked"}}},
{"alert_id": 1, "status": "skipped", "channel": "workbench", "attempted_ts": "2026-05-11T09:56:00+00:00", "payload": {"reason": "workbench_only", "delivery_policy": {"action": "workbench_only"}, "media_evidence": {"status": "media_attached"}}},
][:limit]
class _FeedbackRepository:
def fetch_feedback_for_alert_ids(self, alert_ids, limit: int = 200):
return [
{"alert_id": 1, "label": "useful", "created_ts": "2026-05-11T10:05:00+00:00"},
{"alert_id": 2, "label": "false_positive", "created_ts": "2026-05-11T10:04:00+00:00"},
][:limit]
def fetch_feedback_for_visual_review_ids(self, visual_review_ids, limit: int = 200):
return [
{"visual_review_id": 31, "label": "visual_helpful", "created_ts": "2026-05-11T10:03:00+00:00"},
{"visual_review_id": 32, "label": "visual_misleading", "created_ts": "2026-05-11T10:02:00+00:00"},
{"visual_review_id": 32, "label": "actionable", "created_ts": "2026-05-11T10:01:00+00:00"},
][:limit]
class _VisualRepository:
def fetch_recent_reviews_for_instrument(self, instrument_id: int, limit: int = 50):
return [
{"id": 31, "visual_state": "confirm", "trigger_type": "candidate_actionable_watch", "reviewed_ts": "2026-05-11T10:00:00+00:00"},
{"id": 32, "visual_state": "mismatch", "trigger_type": "market_anomaly_review", "reviewed_ts": "2026-05-11T09:59:00+00:00"},
][:limit]
class _SilentWindowService:
def __init__(self, active: bool = False) -> None:
self.active = active
def build_status(self):
return {
"active": self.active,
"enabled": True,
"window_label": "00:00-06:00",
"timezone": "UTC",
"next_allowed_ts": "2026-05-12T06:00:00+00:00" if self.active else None,
}
class AlertQualitySummaryServiceTests(unittest.TestCase):
def test_build_summary_aggregates_delivery_and_feedback_rates(self) -> None:
service = AlertQualitySummaryService(
decision_repository=_DecisionRepository(),
delivery_repository=_DeliveryRepository(),
feedback_repository=_FeedbackRepository(),
visual_repository=_VisualRepository(),
silent_window_service=_SilentWindowService(active=True),
)
summary = service.build_summary(instrument_id=1, alert_limit=10, visual_review_limit=10)
self.assertEqual(summary["recent_alert_count"], 2)
self.assertEqual(summary["delivery_summary"]["delivered_count"], 1)
self.assertEqual(summary["delivery_summary"]["failed_count"], 1)
self.assertEqual(summary["delivery_summary"]["skipped_count"], 3)
self.assertEqual(summary["delivery_summary"]["retried_delivery_count"], 1)
self.assertEqual(summary["delivery_summary"]["retry_exhausted_skip_count"], 1)
self.assertEqual(summary["delivery_summary"]["noise_policy_skip_count"], 1)
self.assertEqual(summary["delivery_summary"]["workbench_only_skip_count"], 1)
self.assertEqual(summary["delivery_summary"]["transport_retried_delivery_count"], 1)
self.assertEqual(summary["delivery_summary"]["media_attached_count"], 2)
self.assertEqual(summary["delivery_summary"]["media_linked_count"], 2)
self.assertEqual(summary["delivery_summary"]["media_unavailable_count"], 1)
self.assertEqual(summary["delivery_summary"]["channel_status_counts"]["webhook"]["delivered"], 1)
self.assertEqual(summary["delivery_summary"]["channel_status_counts"]["webhook"]["failed"], 1)
self.assertEqual(summary["delivery_summary"]["channel_status_counts"]["workbench"]["skipped"], 2)
self.assertEqual(summary["delivery_summary"]["channel_disabled_count"], 0)
self.assertTrue(summary["silent_window"]["active"])
self.assertEqual(summary["delivery_summary"]["success_rate"], 0.5)
self.assertEqual(summary["alert_feedback_summary"]["positive_count"], 1)
self.assertEqual(summary["alert_feedback_summary"]["negative_count"], 1)
self.assertEqual(summary["alert_feedback_summary"]["positive_rate"], 0.5)
self.assertEqual(summary["visual_feedback_summary"]["positive_count"], 2)
self.assertEqual(summary["visual_feedback_summary"]["negative_count"], 1)
self.assertAlmostEqual(summary["visual_feedback_summary"]["positive_rate"], 2 / 3)
self.assertEqual(summary["visual_review_summary"]["state_counts"]["confirm"], 1)
self.assertEqual(summary["visual_review_summary"]["state_counts"]["mismatch"], 1)
self.assertEqual(summary["visual_review_summary"]["trigger_type_counts"]["candidate_actionable_watch"], 1)
self.assertEqual(summary["visual_review_summary"]["trigger_type_counts"]["market_anomaly_review"], 1)
self.assertIn("delivery_failures_present", summary["quality_flags"])
self.assertIn("alert_silent_window_active", summary["quality_flags"])
self.assertIn("delivery_retry_exhausted_present", summary["quality_flags"])
self.assertIn("delivery_noise_policy_suppression_present", summary["quality_flags"])
self.assertIn("visual_negative_feedback_present", summary["quality_flags"])
self.assertIn("visual_mismatch_present", summary["quality_flags"])
def test_build_summary_reports_missing_feedback_samples(self) -> None:
class _EmptyDeliveryRepository:
def fetch_recent_deliveries_for_alerts(self, alert_ids, limit: int = 200):
return []
class _EmptyFeedbackRepository:
def fetch_feedback_for_alert_ids(self, alert_ids, limit: int = 200):
return []
def fetch_feedback_for_visual_review_ids(self, visual_review_ids, limit: int = 200):
return []
class _EmptyVisualRepository:
def fetch_recent_reviews_for_instrument(self, instrument_id: int, limit: int = 50):
return []
service = AlertQualitySummaryService(
decision_repository=_DecisionRepository(),
delivery_repository=_EmptyDeliveryRepository(),
feedback_repository=_EmptyFeedbackRepository(),
visual_repository=_EmptyVisualRepository(),
silent_window_service=_SilentWindowService(active=False),
)
summary = service.build_summary(instrument_id=1)
self.assertIsNone(summary["delivery_summary"]["success_rate"])
self.assertEqual(summary["alert_feedback_summary"]["count"], 0)
self.assertEqual(summary["visual_feedback_summary"]["count"], 0)
self.assertIn("feedback_sample_missing", summary["quality_flags"])
def test_build_summary_degrades_when_optional_tables_are_unavailable(self) -> None:
class _BrokenDeliveryRepository:
def fetch_recent_deliveries_for_alerts(self, alert_ids, limit: int = 200):
raise RuntimeError("should not be used for sqlalchemy path")
class _BrokenFeedbackRepository:
def fetch_feedback_for_alert_ids(self, alert_ids, limit: int = 200):
from sqlalchemy.exc import ProgrammingError
raise ProgrammingError("stmt", {}, Exception())
def fetch_feedback_for_visual_review_ids(self, visual_review_ids, limit: int = 200):
from sqlalchemy.exc import ProgrammingError
raise ProgrammingError("stmt", {}, Exception())
class _BrokenVisualRepository:
def fetch_recent_reviews_for_instrument(self, instrument_id: int, limit: int = 50):
from sqlalchemy.exc import ProgrammingError
raise ProgrammingError("stmt", {}, Exception())
service = AlertQualitySummaryService(
decision_repository=_DecisionRepository(),
delivery_repository=_DeliveryRepository(),
feedback_repository=_BrokenFeedbackRepository(),
visual_repository=_BrokenVisualRepository(),
silent_window_service=_SilentWindowService(active=False),
)
summary = service.build_summary(instrument_id=1)
self.assertIn("alert_feedback_unavailable", summary["warnings"])
self.assertIn("visual_reviews_unavailable", summary["warnings"])
self.assertIn("visual_feedback_unavailable", summary["warnings"])
if __name__ == "__main__":
unittest.main()