//! erp-health 告警系统集成测试 //! //! 验证告警规则 CRUD、引擎评估(阈值/连续/趋势)、告警状态流转、cooldown、租户隔离。 use erp_health::dto::alert_dto::*; use erp_health::entity::{alert_rules, vital_signs_hourly}; use erp_health::service::{alert_engine, alert_rule_service, alert_service}; use sea_orm::ActiveValue::Set; use sea_orm::ActiveModelTrait; use super::test_fixture::TestApp; /// 创建告警规则(单阈值) async fn seed_threshold_rule(app: &TestApp, device_type: &str, threshold: f64) -> alert_rules::Model { alert_rule_service::create_rule( app.health_state(), app.tenant_id(), app.operator_id(), CreateAlertRuleRequest { name: "高收缩压".to_string(), description: Some("收缩压超过阈值".to_string()), device_type: device_type.to_string(), condition_type: "single_threshold".to_string(), condition_params: serde_json::json!({"direction": "above", "value": threshold}), severity: Some("warning".to_string()), apply_tags: None, notify_roles: None, cooldown_minutes: Some(60), }, ) .await .expect("创建规则应成功") } /// 插入一条 hourly 汇总记录 async fn seed_hourly( app: &TestApp, patient_id: uuid::Uuid, device_type: &str, avg_val: f64, ) { let model = vital_signs_hourly::ActiveModel { id: Set(uuid::Uuid::now_v7()), tenant_id: Set(app.tenant_id()), patient_id: Set(patient_id), device_type: Set(device_type.to_string()), hour_start: Set(chrono::Utc::now()), min_val: Set(Some(avg_val - 1.0)), max_val: Set(Some(avg_val + 1.0)), avg_val: Set(avg_val), sample_count: Set(1), created_at: Set(chrono::Utc::now()), updated_at: Set(chrono::Utc::now()), deleted_at: Set(None), version: Set(1), }; model.insert(app.db()).await.expect("插入 hourly 应成功"); } // --------------------------------------------------------------------------- // 测试 1: 告警规则 CRUD — 创建 + 列表 // --------------------------------------------------------------------------- #[tokio::test] async fn test_alert_rule_create_and_list() { let app = TestApp::new().await; let rule = seed_threshold_rule(&app, "heart_rate", 140.0).await; assert_eq!(rule.name, "高收缩压"); assert_eq!(rule.condition_type, "single_threshold"); assert!(rule.is_active); let (rules, total) = alert_rule_service::list_rules( app.health_state(), app.tenant_id(), None, 1, 20, ) .await .expect("列表应成功"); assert_eq!(total, 1); assert_eq!(rules[0].id, rule.id); } // --------------------------------------------------------------------------- // 测试 2: 告警规则停用 // --------------------------------------------------------------------------- #[tokio::test] async fn test_alert_rule_deactivate() { let app = TestApp::new().await; let rule = seed_threshold_rule(&app, "heart_rate", 140.0).await; assert!(rule.is_active); let deactivated = alert_rule_service::deactivate_rule( app.health_state(), app.tenant_id(), rule.id, rule.version, ) .await .expect("停用应成功"); assert!(!deactivated.is_active); } // --------------------------------------------------------------------------- // 测试 3: 单阈值引擎评估 — 超阈值触发告警 // --------------------------------------------------------------------------- #[tokio::test] async fn test_alert_engine_single_threshold_trigger() { let app = TestApp::new().await; let patient_id = app.create_patient("阈值患者").await; seed_threshold_rule(&app, "heart_rate", 140.0).await; seed_hourly(&app, patient_id, "heart_rate", 155.0).await; let triggered = alert_engine::evaluate_rules( app.health_state(), app.tenant_id(), patient_id, "heart_rate", ) .await .expect("评估应成功"); assert_eq!(triggered.len(), 1); assert_eq!(triggered[0].status, "pending"); assert_eq!(triggered[0].severity, "warning"); } // --------------------------------------------------------------------------- // 测试 4: 单阈值引擎评估 — 未超阈值不触发 // --------------------------------------------------------------------------- #[tokio::test] async fn test_alert_engine_single_threshold_no_trigger() { let app = TestApp::new().await; let patient_id = app.create_patient("正常患者").await; seed_threshold_rule(&app, "heart_rate", 140.0).await; seed_hourly(&app, patient_id, "heart_rate", 120.0).await; let triggered = alert_engine::evaluate_rules( app.health_state(), app.tenant_id(), patient_id, "heart_rate", ) .await .expect("评估应成功"); assert!(triggered.is_empty()); } // --------------------------------------------------------------------------- // 测试 5: cooldown — 重复评估不产生重复告警 // --------------------------------------------------------------------------- #[tokio::test] async fn test_alert_engine_cooldown_suppresses() { let app = TestApp::new().await; let patient_id = app.create_patient("冷却患者").await; seed_threshold_rule(&app, "heart_rate", 140.0).await; seed_hourly(&app, patient_id, "heart_rate", 160.0).await; let first = alert_engine::evaluate_rules( app.health_state(), app.tenant_id(), patient_id, "heart_rate", ) .await .expect("首次评估应成功"); assert_eq!(first.len(), 1); // 再次评估,cooldown 内不应重复 let second = alert_engine::evaluate_rules( app.health_state(), app.tenant_id(), patient_id, "heart_rate", ) .await .expect("二次评估应成功"); assert!(second.is_empty(), "cooldown 内不应重复触发"); } // --------------------------------------------------------------------------- // 测试 6: 告警状态流转 — pending → acknowledged → resolved // --------------------------------------------------------------------------- #[tokio::test] async fn test_alert_status_flow() { let app = TestApp::new().await; let patient_id = app.create_patient("流转患者").await; seed_threshold_rule(&app, "heart_rate", 140.0).await; seed_hourly(&app, patient_id, "heart_rate", 155.0).await; let triggered = alert_engine::evaluate_rules( app.health_state(), app.tenant_id(), patient_id, "heart_rate", ) .await .unwrap(); let alert = &triggered[0]; assert_eq!(alert.status, "pending"); // pending → acknowledged let acked = alert_service::acknowledge_alert( app.health_state(), app.tenant_id(), alert.id, app.operator_id(), alert.version, ) .await .expect("确认应成功"); assert_eq!(acked.status, "acknowledged"); assert!(acked.acknowledged_by.is_some()); // acknowledged → resolved let resolved = alert_service::resolve_alert( app.health_state(), app.tenant_id(), acked.id, acked.version, ) .await .expect("解决应成功"); assert_eq!(resolved.status, "resolved"); assert!(resolved.resolved_at.is_some()); } // --------------------------------------------------------------------------- // 测试 7: 告警状态流转 — pending → dismissed // --------------------------------------------------------------------------- #[tokio::test] async fn test_alert_dismiss_from_pending() { let app = TestApp::new().await; let patient_id = app.create_patient("忽略患者").await; seed_threshold_rule(&app, "heart_rate", 140.0).await; seed_hourly(&app, patient_id, "heart_rate", 155.0).await; let triggered = alert_engine::evaluate_rules( app.health_state(), app.tenant_id(), patient_id, "heart_rate", ) .await .unwrap(); let alert = &triggered[0]; let dismissed = alert_service::dismiss_alert( app.health_state(), app.tenant_id(), alert.id, app.operator_id(), alert.version, ) .await .expect("忽略应成功"); assert_eq!(dismissed.status, "dismissed"); } // --------------------------------------------------------------------------- // 测试 8: 告警列表按患者过滤 + 租户隔离 // --------------------------------------------------------------------------- #[tokio::test] async fn test_alert_list_filter_and_tenant_isolation() { let app = TestApp::new().await; let patient_a = app.create_patient("过滤患者A").await; let patient_b = app.create_patient("过滤患者B").await; seed_threshold_rule(&app, "heart_rate", 140.0).await; seed_hourly(&app, patient_a, "heart_rate", 155.0).await; seed_hourly(&app, patient_b, "heart_rate", 160.0).await; alert_engine::evaluate_rules( app.health_state(), app.tenant_id(), patient_a, "heart_rate", ) .await .unwrap(); alert_engine::evaluate_rules( app.health_state(), app.tenant_id(), patient_b, "heart_rate", ) .await .unwrap(); // 按患者 A 过滤 let (alerts_a, total_a) = alert_service::list_alerts( app.health_state(), app.tenant_id(), Some(patient_a), None, None, 1, 20, ) .await .unwrap(); assert_eq!(total_a, 1); assert_eq!(alerts_a[0].patient_id, patient_a); // 租户隔离 let other_tenant = uuid::Uuid::new_v4(); let (_alerts_other, total_other) = alert_service::list_alerts( app.health_state(), other_tenant, Some(patient_a), None, None, 1, 20, ) .await .unwrap(); assert_eq!(total_other, 0, "不同租户不应看到告警"); }