diff --git a/crates/erp-health/src/event.rs b/crates/erp-health/src/event.rs index 10cea41..89e1b44 100644 --- a/crates/erp-health/src/event.rs +++ b/crates/erp-health/src/event.rs @@ -903,3 +903,821 @@ pub fn register_handlers_with_state(state: crate::state::HealthState) { } }); } + +// --------------------------------------------------------------------------- +// 单元测试 +// --------------------------------------------------------------------------- +// 事件处理器本身依赖 tokio::spawn + channel + DB,无法纯单元测试。 +// 以下测试覆盖: +// 1. 事件类型常量的正确性(防止拼写错误导致消费者不匹配) +// 2. register_handlers 不 panic(空函数) +// 3. 事件 payload 构造格式与消费者解析逻辑的契约 +// 4. EventBus 过滤订阅的内存行为(无需 DB) +// 5. 消费者从 payload 中提取字段的边界条件 +// --------------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use super::*; + use erp_core::events::{build_event_payload, DomainEvent, EventBus}; + use serde_json::json; + use std::collections::HashSet; + + // ── 事件类型常量 ────────────────────────────────────────────────────── + + /// 所有事件类型常量必须遵循 `{domain}.{action}` 格式 + fn assert_valid_event_type(name: &str) { + let parts: Vec<&str> = name.split('.').collect(); + assert!( + parts.len() >= 2, + "事件类型 '{}' 不符合 domain.action 格式", + name + ); + assert!( + !parts[0].is_empty() && !parts[1].is_empty(), + "事件类型 '{}' 的 domain 或 action 为空", + name + ); + } + + #[test] + fn event_constants_follow_naming_convention() { + let all_types = [ + APPOINTMENT_CREATED, + ALERT_TRIGGERED, + CONSENT_GRANTED, + CONSENT_REVOKED, + ARTICLE_PUBLISHED, + ARTICLE_REJECTED, + CONSULTATION_OPENED, + CONSULTATION_CLOSED, + CONSULTATION_NEW_MESSAGE, + DEVICE_READINGS_SYNCED, + DOCTOR_ONLINE_STATUS_CHANGED, + FOLLOW_UP_CREATED, + FOLLOW_UP_COMPLETED, + FOLLOW_UP_OVERDUE, + DAILY_MONITORING_CREATED, + LAB_REPORT_UPLOADED, + LAB_REPORT_REVIEWED, + HEALTH_DATA_CRITICAL_ALERT, + PATIENT_CREATED, + PATIENT_UPDATED, + PATIENT_VERIFIED, + PATIENT_DECEASED, + POINTS_EXPIRED, + POINTS_EARNED, + POINTS_EXCHANGED, + ]; + for t in &all_types { + assert_valid_event_type(t); + } + } + + #[test] + fn event_constants_are_unique() { + let all_types = [ + APPOINTMENT_CREATED, + ALERT_TRIGGERED, + CONSENT_GRANTED, + CONSENT_REVOKED, + ARTICLE_PUBLISHED, + ARTICLE_REJECTED, + CONSULTATION_OPENED, + CONSULTATION_CLOSED, + CONSULTATION_NEW_MESSAGE, + DEVICE_READINGS_SYNCED, + DOCTOR_ONLINE_STATUS_CHANGED, + FOLLOW_UP_CREATED, + FOLLOW_UP_COMPLETED, + FOLLOW_UP_OVERDUE, + DAILY_MONITORING_CREATED, + LAB_REPORT_UPLOADED, + LAB_REPORT_REVIEWED, + HEALTH_DATA_CRITICAL_ALERT, + PATIENT_CREATED, + PATIENT_UPDATED, + PATIENT_VERIFIED, + PATIENT_DECEASED, + POINTS_EXPIRED, + POINTS_EARNED, + POINTS_EXCHANGED, + ]; + let set: HashSet<&&str> = all_types.iter().collect(); + assert_eq!( + set.len(), + all_types.len(), + "存在重复的事件类型常量" + ); + } + + #[test] + fn event_constants_match_expected_values() { + // 确保常量值与消费者 switch 匹配中使用的硬编码字符串一致 + assert_eq!(APPOINTMENT_CREATED, "appointment.created"); + assert_eq!(ALERT_TRIGGERED, "alert.triggered"); + assert_eq!(CONSENT_GRANTED, "consent.granted"); + assert_eq!(CONSENT_REVOKED, "consent.revoked"); + assert_eq!(ARTICLE_PUBLISHED, "article.published"); + assert_eq!(ARTICLE_REJECTED, "article.rejected"); + assert_eq!(CONSULTATION_OPENED, "consultation.opened"); + assert_eq!(CONSULTATION_CLOSED, "consultation.closed"); + assert_eq!(CONSULTATION_NEW_MESSAGE, "consultation.new_message"); + assert_eq!(DEVICE_READINGS_SYNCED, "device.readings.synced"); + assert_eq!(DOCTOR_ONLINE_STATUS_CHANGED, "doctor.online_status_changed"); + assert_eq!(FOLLOW_UP_CREATED, "follow_up.created"); + assert_eq!(FOLLOW_UP_COMPLETED, "follow_up.completed"); + assert_eq!(FOLLOW_UP_OVERDUE, "follow_up.overdue"); + assert_eq!(DAILY_MONITORING_CREATED, "daily_monitoring.created"); + assert_eq!(LAB_REPORT_UPLOADED, "lab_report.uploaded"); + assert_eq!(LAB_REPORT_REVIEWED, "lab_report.reviewed"); + assert_eq!(HEALTH_DATA_CRITICAL_ALERT, "health_data.critical_alert"); + assert_eq!(PATIENT_CREATED, "patient.created"); + assert_eq!(PATIENT_UPDATED, "patient.updated"); + assert_eq!(PATIENT_VERIFIED, "patient.verified"); + assert_eq!(PATIENT_DECEASED, "patient.deceased"); + assert_eq!(POINTS_EXPIRED, "points.expired"); + assert_eq!(POINTS_EARNED, "points.earned"); + assert_eq!(POINTS_EXCHANGED, "points.exchanged"); + } + + /// 消费者中硬编码的事件类型(非通过常量引用)也必须可被常量覆盖 + #[test] + fn hardcoded_event_types_in_consumers_are_covered() { + // event.rs 中消费者使用的事件类型字符串(未通过常量引用) + let hardcoded = [ + "workflow.task.completed", + "appointment.confirmed", + "appointment.cancelled", + "ai.analysis.completed", + "dialysis.record.created", + // 消费者产出的事件类型 + "message.send", + "ai.analysis.requested", + "ai.reanalysis.requested", + ]; + // 这些硬编码类型不与 erp-health 常量重复 + // 它们来自 erp-workflow / erp-core / erp-ai 等其他模块 + // 验证 erp-health 常量不会与外部模块事件类型冲突 + let health_types = [ + APPOINTMENT_CREATED, + ALERT_TRIGGERED, + CONSENT_GRANTED, + CONSENT_REVOKED, + CONSULTATION_OPENED, + FOLLOW_UP_CREATED, + FOLLOW_UP_COMPLETED, + FOLLOW_UP_OVERDUE, + DEVICE_READINGS_SYNCED, + LAB_REPORT_UPLOADED, + HEALTH_DATA_CRITICAL_ALERT, + PATIENT_CREATED, + PATIENT_UPDATED, + ]; + for t in &hardcoded { + for ht in &health_types { + assert_ne!( + *t, *ht, + "外部模块事件类型 '{}' 与 erp-health 常量 '{}' 冲突", + t, ht + ); + } + } + } + + // ── register_handlers 不 panic ────────────────────────────────────── + + #[test] + fn register_handlers_does_not_panic() { + let bus = EventBus::new(64); + // register_handlers 是空函数,不应 panic + register_handlers(&bus); + } + + // ── DomainEvent 构造与 payload 契约 ───────────────────────────────── + + #[test] + fn domain_event_new_sets_correct_event_type() { + let tenant_id = Uuid::now_v7(); + let event = DomainEvent::new(PATIENT_CREATED, tenant_id, json!({})); + assert_eq!(event.event_type, PATIENT_CREATED); + assert_eq!(event.tenant_id, tenant_id); + } + + #[test] + fn domain_event_new_generates_unique_ids() { + let tenant_id = Uuid::now_v7(); + let e1 = DomainEvent::new("test.a", tenant_id, json!({})); + let e2 = DomainEvent::new("test.b", tenant_id, json!({})); + assert_ne!(e1.id, e2.id, "每个事件应有唯一 ID"); + assert_ne!(e1.correlation_id, e2.correlation_id, "每个事件应有唯一 correlation_id"); + } + + #[test] + fn build_event_payload_injects_schema_version() { + let payload = build_event_payload(json!({ "patient_id": "abc" })); + assert_eq!(payload["schema_version"], "v1"); + assert!(payload.get("occurred_at").is_some(), "必须包含 occurred_at 时间戳"); + } + + #[test] + fn build_event_payload_merges_data_fields() { + let payload = build_event_payload(json!({ + "patient_id": "test-123", + "severity": "critical", + })); + assert_eq!(payload["schema_version"], "v1"); + assert_eq!(payload["patient_id"], "test-123"); + assert_eq!(payload["severity"], "critical"); + } + + // ── 消费者 payload 解析契约测试 ───────────────────────────────────── + // + // 验证消费者从 payload 中提取字段的方式与 build_event_payload 的输出兼容。 + // 这些测试模拟消费者使用的 serde_json::Value 提取模式。 + + /// 模拟消费者提取 patient_id(UUID 字符串) + fn extract_patient_id(payload: &serde_json::Value) -> Option { + payload + .get("patient_id") + .and_then(|v| v.as_str()) + .and_then(|s| Uuid::parse_str(s).ok()) + } + + #[test] + fn payload_extraction_patient_id_from_uuid_string() { + let pid = Uuid::now_v7(); + let payload = build_event_payload(json!({ + "patient_id": pid.to_string(), + })); + assert_eq!(extract_patient_id(&payload), Some(pid)); + } + + #[test] + fn payload_extraction_patient_id_missing_field() { + let payload = build_event_payload(json!({})); + assert_eq!(extract_patient_id(&payload), None); + } + + #[test] + fn payload_extraction_patient_id_invalid_uuid() { + let payload = build_event_payload(json!({ + "patient_id": "not-a-uuid", + })); + assert_eq!(extract_patient_id(&payload), None); + } + + #[test] + fn payload_extraction_patient_id_wrong_type() { + // patient_id 是数字而非字符串 + let payload = build_event_payload(json!({ + "patient_id": 12345, + })); + assert_eq!(extract_patient_id(&payload), None); + } + + /// 模拟消费者提取 severity(带默认值) + fn extract_severity(payload: &serde_json::Value) -> &str { + payload + .get("severity") + .and_then(|v| v.as_str()) + .unwrap_or("warning") + } + + #[test] + fn payload_extraction_severity_present() { + let payload = build_event_payload(json!({ "severity": "critical" })); + assert_eq!(extract_severity(&payload), "critical"); + } + + #[test] + fn payload_extraction_severity_defaults_to_warning() { + let payload = build_event_payload(json!({})); + assert_eq!(extract_severity(&payload), "warning"); + } + + /// 模拟消费者提取 task_id(UUID 字符串) + fn extract_task_id(payload: &serde_json::Value) -> Option { + payload + .get("task_id") + .and_then(|v| v.as_str()) + .and_then(|s| Uuid::parse_str(s).ok()) + } + + #[test] + fn payload_extraction_task_id_valid() { + let tid = Uuid::now_v7(); + let payload = build_event_payload(json!({ "task_id": tid.to_string() })); + assert_eq!(extract_task_id(&payload), Some(tid)); + } + + #[test] + fn payload_extraction_task_id_missing() { + let payload = build_event_payload(json!({ "other_field": "value" })); + assert_eq!(extract_task_id(&payload), None); + } + + /// 模拟消费者提取 amount(u64) + fn extract_amount(payload: &serde_json::Value) -> Option { + payload.get("amount").and_then(|v| v.as_u64()) + } + + #[test] + fn payload_extraction_amount_valid() { + let payload = build_event_payload(json!({ "amount": 100 })); + assert_eq!(extract_amount(&payload), Some(100)); + } + + #[test] + fn payload_extraction_amount_zero() { + let payload = build_event_payload(json!({ "amount": 0 })); + assert_eq!(extract_amount(&payload), Some(0)); + } + + #[test] + fn payload_extraction_amount_missing() { + let payload = build_event_payload(json!({})); + assert_eq!(extract_amount(&payload), None); + } + + #[test] + fn payload_extraction_amount_negative_returns_none() { + // serde_json u64 不能表示负数 + let payload = build_event_payload(json!({ "amount": -5 })); + assert_eq!(extract_amount(&payload), None); + } + + /// 模拟消费者提取 suggestion_count(u64)用于条件判断 + #[test] + fn payload_extraction_suggestion_count_zero() { + let payload = build_event_payload(json!({ "suggestion_count": 0 })); + let count = payload.get("suggestion_count").and_then(|v| v.as_u64()).unwrap_or(0); + assert_eq!(count, 0); + assert!(!(count > 0), "suggestion_count=0 时不触发行动分发"); + } + + #[test] + fn payload_extraction_suggestion_count_positive() { + let payload = build_event_payload(json!({ "suggestion_count": 3 })); + let count = payload.get("suggestion_count").and_then(|v| v.as_u64()).unwrap_or(0); + assert!(count > 0, "suggestion_count>0 时应触发行动分发"); + } + + // ── 完整 payload 契约测试 ───────────────────────────────────────── + // + // 验证 service 层构建的 payload 能被消费者正确解析。 + // 这些测试模拟 service 层的 build_event_payload 调用,然后 + // 用消费者中的提取逻辑验证字段可达。 + + /// appointment.created 事件 payload 契约 + #[test] + fn appointment_created_payload_contract() { + let patient_id = Uuid::now_v7(); + let appointment_id = Uuid::now_v7(); + // 模拟 appointment_service 的 payload 构造 + let payload = build_event_payload(json!({ + "appointment_id": appointment_id.to_string(), + "patient_id": patient_id.to_string(), + "status": "pending", + })); + + // 消费者提取 patient_id(字符串形式) + let pid_str = payload.get("patient_id").and_then(|v| v.as_str()); + assert!(pid_str.is_some(), "消费者需要 patient_id 字符串"); + assert_eq!(pid_str.unwrap(), patient_id.to_string()); + } + + /// patient.created 事件 payload 契约 + #[test] + fn patient_created_payload_contract() { + let patient_id = Uuid::now_v7(); + // 模拟 patient_service 的 payload 构造 + let payload = build_event_payload(json!({ + "patient_id": patient_id.to_string(), + })); + + let extracted = extract_patient_id(&payload); + assert_eq!(extracted, Some(patient_id)); + } + + /// alert.triggered 事件 payload 契约 + #[test] + fn alert_triggered_payload_contract() { + let alert_id = Uuid::now_v7(); + let patient_id = Uuid::now_v7(); + // 模拟 alert_engine 的 payload 构造 + let payload = build_event_payload(json!({ + "alert_id": alert_id.to_string(), + "patient_id": patient_id.to_string(), + "rule_name": "心率过高", + "severity": "critical", + "detail": "心率超过阈值", + "notify_roles": ["doctor"], + })); + + let pid = payload.get("patient_id").and_then(|v| v.as_str()); + assert!(pid.is_some(), "消费者需要 patient_id"); + + let severity = extract_severity(&payload); + assert_eq!(severity, "critical"); + + let rule_name = payload.get("rule_name").and_then(|v| v.as_str()).unwrap_or("健康告警"); + assert_eq!(rule_name, "心率过高"); + } + + /// health_data.critical_alert 事件 payload 契约 + #[test] + fn critical_alert_payload_contract() { + let patient_id = Uuid::now_v7(); + let payload = build_event_payload(json!({ + "patient_id": patient_id.to_string(), + "alert_type": "vital_sign", + "metric_name": "heart_rate", + "metric_value": "180", + "threshold_value": "150", + })); + + assert_eq!(extract_patient_id(&payload), Some(patient_id)); + assert_eq!( + payload.get("alert_type").and_then(|v| v.as_str()).unwrap_or("vital_sign"), + "vital_sign" + ); + assert_eq!( + payload.get("metric_name").and_then(|v| v.as_str()).unwrap_or("unknown"), + "heart_rate" + ); + assert_eq!( + payload.get("metric_value").and_then(|v| v.as_str()).unwrap_or(""), + "180" + ); + assert_eq!( + payload.get("threshold_value").and_then(|v| v.as_str()).unwrap_or(""), + "150" + ); + } + + /// follow_up.overdue 事件 payload 契约 + #[test] + fn follow_up_overdue_payload_contract() { + let task_id = Uuid::now_v7(); + let assigned_to = Uuid::now_v7(); + let payload = build_event_payload(json!({ + "task_id": task_id.to_string(), + "assigned_to": assigned_to.to_string(), + })); + + let tid = payload.get("task_id").and_then(|v| v.as_str()); + let uid = payload.get("assigned_to").and_then(|v| v.as_str()); + assert!(tid.is_some(), "消费者需要 task_id"); + assert!(uid.is_some(), "消费者需要 assigned_to"); + } + + /// consultation.new_message 事件 payload 契约 — sender_role 决定通知目标 + #[test] + fn consultation_new_message_recipient_logic() { + // 患者发送 → 通知医生 + let payload_patient = build_event_payload(json!({ + "recipient_id": "doctor-123", + "sender_role": "patient", + })); + let sender_role = payload_patient.get("sender_role").and_then(|v| v.as_str()).unwrap_or("unknown"); + let recipient_type = if sender_role == "patient" { "doctor" } else { "patient" }; + assert_eq!(recipient_type, "doctor"); + + // 医生发送 → 通知患者 + let payload_doctor = build_event_payload(json!({ + "recipient_id": "patient-456", + "sender_role": "doctor", + })); + let sender_role = payload_doctor.get("sender_role").and_then(|v| v.as_str()).unwrap_or("unknown"); + let recipient_type = if sender_role == "patient" { "doctor" } else { "patient" }; + assert_eq!(recipient_type, "patient"); + } + + /// lab_report.uploaded 事件 payload 契约 — 触发 AI 分析 + #[test] + fn lab_report_uploaded_payload_contract() { + let report_id = Uuid::now_v7(); + let patient_id = Uuid::now_v7(); + let payload = build_event_payload(json!({ + "source_type": "lab_report", + "source_id": report_id.to_string(), + "patient_id": patient_id.to_string(), + })); + + let rid = payload.get("source_id").and_then(|v| v.as_str()); + let pid = payload.get("patient_id").and_then(|v| v.as_str()); + assert!(rid.is_some(), "消费者需要 source_id (report_id)"); + assert!(pid.is_some(), "消费者需要 patient_id"); + } + + /// points.earned 事件 payload 契约 + #[test] + fn points_earned_payload_contract() { + let patient_id = Uuid::now_v7(); + let payload = build_event_payload(json!({ + "patient_id": patient_id.to_string(), + "amount": 50, + })); + + let pid = payload.get("patient_id").and_then(|v| v.as_str()); + let amt = payload.get("amount").and_then(|v| v.as_u64()); + assert!(pid.is_some()); + assert_eq!(amt, Some(50)); + } + + /// ai.analysis.completed 事件 payload 契约 — 含 suggestion_count + #[test] + fn ai_analysis_completed_payload_contract() { + let analysis_id = Uuid::now_v7(); + let patient_id = Uuid::now_v7(); + let doctor_id = Uuid::now_v7(); + let payload = build_event_payload(json!({ + "analysis_id": analysis_id.to_string(), + "analysis_type": "lab_report", + "patient_id": patient_id.to_string(), + "doctor_id": doctor_id.to_string(), + "risk_level": "high", + "suggestion_count": 2, + })); + + // AI 分析完成通知消费者需要的字段 + let did = payload.get("doctor_id").and_then(|v| v.as_str()); + let pid = payload.get("patient_id").and_then(|v| v.as_str()); + assert!(did.is_some(), "通知消费者需要 doctor_id"); + assert!(pid.is_some(), "通知消费者需要 patient_id"); + + // 行动分发消费者需要的字段 + let aid = payload.get("analysis_id").and_then(|v| v.as_str()).and_then(|s| Uuid::parse_str(s).ok()); + assert_eq!(aid, Some(analysis_id)); + + let suggestion_count = payload.get("suggestion_count").and_then(|v| v.as_u64()).unwrap_or(0); + assert!(suggestion_count > 0, "有建议时应触发行动分发"); + + let risk_level = payload.get("risk_level").and_then(|v| v.as_str()).unwrap_or("medium"); + assert_eq!(risk_level, "high"); + } + + // ── EventBus 过滤订阅行为测试 ────────────────────────────────────── + + #[tokio::test] + async fn eventbus_broadcast_and_subscribe_filtered() { + let bus = EventBus::new(64); + let (mut rx, _handle) = bus.subscribe_filtered("patient.".to_string()); + + let tenant_id = Uuid::now_v7(); + + // 广播一个匹配的事件 + let patient_event = DomainEvent::new(PATIENT_CREATED, tenant_id, json!({})); + bus.broadcast(patient_event.clone()); + + // 广播一个不匹配的事件 + let other_event = DomainEvent::new(APPOINTMENT_CREATED, tenant_id, json!({})); + bus.broadcast(other_event); + + // 只应收到 patient.created + let received = tokio::time::timeout( + std::time::Duration::from_millis(100), + rx.recv(), + ) + .await + .expect("超时:应收到匹配的事件") + .expect("channel 不应关闭"); + assert_eq!(received.event_type, PATIENT_CREATED); + } + + #[tokio::test] + async fn eventbus_subscribe_filtered_ignores_non_matching() { + let bus = EventBus::new(64); + let (mut rx, _handle) = bus.subscribe_filtered("follow_up.".to_string()); + + let tenant_id = Uuid::now_v7(); + + // 广播不匹配的事件 + let unmatched = DomainEvent::new(PATIENT_CREATED, tenant_id, json!({})); + bus.broadcast(unmatched); + + // 应该收不到任何事件 + let result = tokio::time::timeout( + std::time::Duration::from_millis(50), + rx.recv(), + ) + .await; + assert!(result.is_err(), "不应收到不匹配的事件"); + } + + #[tokio::test] + async fn eventbus_subscribe_filtered_receives_multiple_matching() { + let bus = EventBus::new(64); + let (mut rx, _handle) = bus.subscribe_filtered("appointment.".to_string()); + + let tenant_id = Uuid::now_v7(); + + // 广播多个匹配前缀的事件 + let types = [ + APPOINTMENT_CREATED, + "appointment.confirmed", + "appointment.cancelled", + ]; + for t in &types { + let event = DomainEvent::new(*t, tenant_id, json!({})); + bus.broadcast(event); + } + + // 应收到全部 3 个 + let mut received_types = Vec::new(); + for _ in 0..3 { + let event = tokio::time::timeout( + std::time::Duration::from_millis(100), + rx.recv(), + ) + .await + .expect("超时") + .expect("channel 关闭"); + received_types.push(event.event_type); + } + assert_eq!(received_types.len(), 3); + assert!(received_types.contains(&APPOINTMENT_CREATED.to_string())); + assert!(received_types.contains(&"appointment.confirmed".to_string())); + assert!(received_types.contains(&"appointment.cancelled".to_string())); + } + + // ── 消费者前缀与常量匹配测试 ───────────────────────────────────── + // + // 验证 subscribe_filtered 的前缀能覆盖该通道需要接收的所有事件类型。 + + #[test] + fn subscribe_prefix_covers_all_workflow_events() { + let prefix = "workflow.task."; + let event_type = "workflow.task.completed"; + assert!( + event_type.starts_with(prefix), + "前缀 '{}' 应覆盖 '{}'", + prefix, + event_type + ); + } + + #[test] + fn subscribe_prefix_covers_all_device_events() { + let prefix = "device.readings."; + assert!( + DEVICE_READINGS_SYNCED.starts_with(prefix), + "前缀 '{}' 应覆盖 '{}'", + prefix, + DEVICE_READINGS_SYNCED + ); + } + + #[test] + fn subscribe_prefix_covers_all_alert_events() { + let prefix = "alert."; + assert!( + ALERT_TRIGGERED.starts_with(prefix), + "前缀 '{}' 应覆盖 '{}'", + prefix, + ALERT_TRIGGERED + ); + } + + #[test] + fn subscribe_prefix_covers_all_patient_events() { + let prefix = "patient."; + assert!(PATIENT_CREATED.starts_with(prefix)); + assert!(PATIENT_UPDATED.starts_with(prefix)); + } + + #[test] + fn subscribe_prefix_covers_all_appointment_events() { + let prefix = "appointment."; + assert!(APPOINTMENT_CREATED.starts_with(prefix)); + assert!("appointment.confirmed".starts_with(prefix)); + assert!("appointment.cancelled".starts_with(prefix)); + } + + #[test] + fn subscribe_prefix_covers_all_follow_up_events() { + let prefix = "follow_up."; + assert!(FOLLOW_UP_CREATED.starts_with(prefix)); + assert!(FOLLOW_UP_COMPLETED.starts_with(prefix)); + assert!(FOLLOW_UP_OVERDUE.starts_with(prefix)); + } + + #[test] + fn subscribe_prefix_covers_all_health_data_events() { + let prefix = "health_data."; + assert!( + HEALTH_DATA_CRITICAL_ALERT.starts_with(prefix), + "前缀 '{}' 应覆盖 '{}'", + prefix, + HEALTH_DATA_CRITICAL_ALERT + ); + } + + #[test] + fn subscribe_prefix_covers_all_ai_events() { + let prefix = "ai."; + assert!("ai.analysis.completed".starts_with(prefix)); + assert!("ai.reanalysis.requested".starts_with(prefix)); + } + + #[test] + fn subscribe_prefix_covers_all_consent_events() { + let prefix = "consent."; + assert!(CONSENT_GRANTED.starts_with(prefix)); + assert!(CONSENT_REVOKED.starts_with(prefix)); + } + + #[test] + fn subscribe_prefix_covers_all_consultation_events() { + let prefix = "consultation."; + assert!(CONSULTATION_OPENED.starts_with(prefix)); + assert!(CONSULTATION_CLOSED.starts_with(prefix)); + assert!(CONSULTATION_NEW_MESSAGE.starts_with(prefix)); + } + + #[test] + fn subscribe_prefix_covers_all_points_events() { + let prefix = "points."; + assert!(POINTS_EARNED.starts_with(prefix)); + assert!(POINTS_EXCHANGED.starts_with(prefix)); + assert!(POINTS_EXPIRED.starts_with(prefix)); + } + + #[test] + fn subscribe_prefix_covers_all_lab_report_events() { + let prefix = "lab_report."; + assert!(LAB_REPORT_UPLOADED.starts_with(prefix)); + assert!(LAB_REPORT_REVIEWED.starts_with(prefix)); + } + + // ── device.readings.synced 消费者设备类型列表测试 ────────────────── + + #[test] + fn device_types_for_alert_evaluation_are_comprehensive() { + // 消费者硬编码的设备类型列表 + let device_types = ["heart_rate", "blood_oxygen", "temperature", "blood_pressure", "blood_glucose"]; + assert_eq!(device_types.len(), 5, "设备类型列表应包含 5 种类型"); + // 确保没有重复 + let set: HashSet<&&str> = device_types.iter().collect(); + assert_eq!(set.len(), device_types.len(), "设备类型列表不应有重复"); + } + + // ── 告警严重度模板选择逻辑 ───────────────────────────────────── + + #[test] + fn alert_severity_template_selection() { + let severity_critical = "critical"; + let template = if severity_critical == "critical" { + "CRITICAL_HEALTH_ALERT" + } else { + "HEALTH_DATA_ABNORMAL" + }; + assert_eq!(template, "CRITICAL_HEALTH_ALERT"); + + let severity_warning = "warning"; + let template = if severity_warning == "critical" { + "CRITICAL_HEALTH_ALERT" + } else { + "HEALTH_DATA_ABNORMAL" + }; + assert_eq!(template, "HEALTH_DATA_ABNORMAL"); + } + + // ── 消费者幂等 consumer_id 唯一性 ────────────────────────────────── + + #[test] + fn consumer_ids_are_unique() { + // 收集所有消费者的 consumer_id(从 mark_event_processed 调用中提取) + let consumer_ids = [ + "workflow_task_consumer", + "alert_notifier", + "patient_welcome", + "appt_created_notifier", + "appointment_notifier", + "appointment_cancel_handler", + "follow_up_escalator", + "critical_alert_consumer", + "ai_analysis_notifier", + "dialysis_notifier", + "ai_action_dispatcher", + "consent_notifier", + "consult_opened_notifier", + "consult_msg_notifier", + "consult_closed_notifier", + "fu_created_notifier", + "points_earned_notifier", + "points_exchanged_notifier", + "points_expired_notifier", + "lab_upload_ai_trigger", + "lab_reviewed_notifier", + "patient_updated_audit", + ]; + let set: HashSet<&&str> = consumer_ids.iter().collect(); + assert_eq!( + set.len(), + consumer_ids.len(), + "存在重复的 consumer_id" + ); + } +}