From 55a7d7a03e5d1ed99526cb24610e75c3a3eff91c Mon Sep 17 00:00:00 2001 From: iven Date: Mon, 27 Apr 2026 21:48:12 +0800 Subject: [PATCH] =?UTF-8?q?test(health):=20=E5=91=8A=E8=AD=A6=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E9=9B=86=E6=88=90=E6=B5=8B=E8=AF=95=20=E2=80=94=208?= =?UTF-8?q?=20=E4=B8=AA=E6=B5=8B=E8=AF=95=E8=A6=86=E7=9B=96=E8=A7=84?= =?UTF-8?q?=E5=88=99=20CRUD/=E5=BC=95=E6=93=8E=E8=AF=84=E4=BC=B0/=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E6=B5=81=E8=BD=AC/cooldown/=E7=A7=9F=E6=88=B7?= =?UTF-8?q?=E9=9A=94=E7=A6=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/erp-server/tests/integration.rs | 2 + .../tests/integration/health_alert_tests.rs | 267 ++++++++++++++++++ 2 files changed, 269 insertions(+) create mode 100644 crates/erp-server/tests/integration/health_alert_tests.rs diff --git a/crates/erp-server/tests/integration.rs b/crates/erp-server/tests/integration.rs index 3890a48..9912402 100644 --- a/crates/erp-server/tests/integration.rs +++ b/crates/erp-server/tests/integration.rs @@ -18,3 +18,5 @@ mod health_pii_encryption_tests; mod health_points_tests; #[path = "integration/health_dialysis_tests.rs"] mod health_dialysis_tests; +#[path = "integration/health_alert_tests.rs"] +mod health_alert_tests; diff --git a/crates/erp-server/tests/integration/health_alert_tests.rs b/crates/erp-server/tests/integration/health_alert_tests.rs new file mode 100644 index 0000000..852d40c --- /dev/null +++ b/crates/erp-server/tests/integration/health_alert_tests.rs @@ -0,0 +1,267 @@ +//! 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()), + 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, 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, 1, 20, + ) + .await + .unwrap(); + assert_eq!(total_other, 0, "不同租户不应看到告警"); +}