feat: Add wifi-densepose-mat disaster detection module

Implements WiFi-Mat (Mass Casualty Assessment Tool) for detecting and
localizing survivors trapped in rubble, earthquakes, and natural disasters.

Architecture:
- Domain-Driven Design with bounded contexts (Detection, Localization, Alerting)
- Modular Rust crate integrating with existing wifi-densepose-* crates
- Event-driven architecture for audit trails and distributed deployments

Features:
- Breathing pattern detection from CSI amplitude variations
- Heartbeat detection using micro-Doppler analysis
- Movement classification (gross, fine, tremor, periodic)
- START protocol-compatible triage classification
- 3D position estimation via triangulation and depth estimation
- Real-time alert generation with priority escalation

Documentation:
- ADR-001: Architecture Decision Record for wifi-Mat
- DDD domain model specification
This commit is contained in:
Claude
2026-01-13 17:24:50 +00:00
parent 0fa9a0b882
commit a17b630c02
31 changed files with 9042 additions and 0 deletions

View File

@@ -0,0 +1,339 @@
//! Alert dispatching and delivery.
use crate::domain::{Alert, AlertId, Priority, Survivor};
use crate::MatError;
use super::AlertGenerator;
use std::collections::HashMap;
/// Configuration for alert dispatch
#[derive(Debug, Clone)]
pub struct AlertConfig {
/// Enable audio alerts
pub audio_enabled: bool,
/// Enable visual alerts
pub visual_enabled: bool,
/// Escalation timeout in seconds
pub escalation_timeout_secs: u64,
/// Maximum pending alerts before forced escalation
pub max_pending_alerts: usize,
/// Auto-acknowledge after seconds (0 = disabled)
pub auto_ack_secs: u64,
}
impl Default for AlertConfig {
fn default() -> Self {
Self {
audio_enabled: true,
visual_enabled: true,
escalation_timeout_secs: 300, // 5 minutes
max_pending_alerts: 50,
auto_ack_secs: 0, // Disabled
}
}
}
/// Dispatcher for sending alerts to rescue teams
pub struct AlertDispatcher {
config: AlertConfig,
generator: AlertGenerator,
pending_alerts: parking_lot::RwLock<HashMap<AlertId, Alert>>,
handlers: Vec<Box<dyn AlertHandler>>,
}
impl AlertDispatcher {
/// Create a new alert dispatcher
pub fn new(config: AlertConfig) -> Self {
Self {
config,
generator: AlertGenerator::new(),
pending_alerts: parking_lot::RwLock::new(HashMap::new()),
handlers: Vec::new(),
}
}
/// Add an alert handler
pub fn add_handler(&mut self, handler: Box<dyn AlertHandler>) {
self.handlers.push(handler);
}
/// Generate an alert for a survivor
pub fn generate_alert(&self, survivor: &Survivor) -> Result<Alert, MatError> {
self.generator.generate(survivor)
}
/// Dispatch an alert
pub async fn dispatch(&self, alert: Alert) -> Result<(), MatError> {
let alert_id = alert.id().clone();
let priority = alert.priority();
// Store in pending alerts
self.pending_alerts.write().insert(alert_id.clone(), alert.clone());
// Log the alert
tracing::info!(
alert_id = %alert_id,
priority = ?priority,
title = %alert.payload().title,
"Dispatching alert"
);
// Send to all handlers
for handler in &self.handlers {
if let Err(e) = handler.handle(&alert).await {
tracing::warn!(
alert_id = %alert_id,
handler = %handler.name(),
error = %e,
"Handler failed to process alert"
);
}
}
// Check if we're at capacity
let pending_count = self.pending_alerts.read().len();
if pending_count >= self.config.max_pending_alerts {
tracing::warn!(
pending_count,
max = self.config.max_pending_alerts,
"Alert capacity reached - escalating oldest alerts"
);
self.escalate_oldest().await?;
}
Ok(())
}
/// Acknowledge an alert
pub fn acknowledge(&self, alert_id: &AlertId, by: &str) -> Result<(), MatError> {
let mut alerts = self.pending_alerts.write();
if let Some(alert) = alerts.get_mut(alert_id) {
alert.acknowledge(by);
tracing::info!(
alert_id = %alert_id,
acknowledged_by = by,
"Alert acknowledged"
);
Ok(())
} else {
Err(MatError::Alerting(format!("Alert {} not found", alert_id)))
}
}
/// Resolve an alert
pub fn resolve(&self, alert_id: &AlertId, resolution: crate::domain::AlertResolution) -> Result<(), MatError> {
let mut alerts = self.pending_alerts.write();
if let Some(alert) = alerts.remove(alert_id) {
let mut resolved_alert = alert;
resolved_alert.resolve(resolution);
tracing::info!(
alert_id = %alert_id,
"Alert resolved"
);
Ok(())
} else {
Err(MatError::Alerting(format!("Alert {} not found", alert_id)))
}
}
/// Get all pending alerts
pub fn pending(&self) -> Vec<Alert> {
self.pending_alerts.read().values().cloned().collect()
}
/// Get pending alerts by priority
pub fn pending_by_priority(&self, priority: Priority) -> Vec<Alert> {
self.pending_alerts
.read()
.values()
.filter(|a| a.priority() == priority)
.cloned()
.collect()
}
/// Get count of pending alerts
pub fn pending_count(&self) -> usize {
self.pending_alerts.read().len()
}
/// Check and escalate timed-out alerts
pub async fn check_escalations(&self) -> Result<u32, MatError> {
let timeout_secs = self.config.escalation_timeout_secs as i64;
let mut escalated = 0;
let mut to_escalate = Vec::new();
{
let alerts = self.pending_alerts.read();
for (id, alert) in alerts.iter() {
if alert.needs_escalation(timeout_secs) {
to_escalate.push(id.clone());
}
}
}
for id in to_escalate {
let mut alerts = self.pending_alerts.write();
if let Some(alert) = alerts.get_mut(&id) {
alert.escalate();
escalated += 1;
tracing::warn!(
alert_id = %id,
new_priority = ?alert.priority(),
"Alert escalated due to timeout"
);
}
}
Ok(escalated)
}
/// Escalate oldest pending alerts
async fn escalate_oldest(&self) -> Result<(), MatError> {
let mut alerts: Vec<_> = self.pending_alerts.read()
.iter()
.map(|(id, alert)| (id.clone(), *alert.created_at()))
.collect();
// Sort by creation time (oldest first)
alerts.sort_by_key(|(_, created)| *created);
// Escalate oldest 10%
let to_escalate = (alerts.len() / 10).max(1);
let mut pending = self.pending_alerts.write();
for (id, _) in alerts.into_iter().take(to_escalate) {
if let Some(alert) = pending.get_mut(&id) {
alert.escalate();
}
}
Ok(())
}
/// Get configuration
pub fn config(&self) -> &AlertConfig {
&self.config
}
}
/// Handler for processing alerts
#[async_trait::async_trait]
pub trait AlertHandler: Send + Sync {
/// Handler name
fn name(&self) -> &str;
/// Handle an alert
async fn handle(&self, alert: &Alert) -> Result<(), MatError>;
}
/// Console/logging alert handler
pub struct ConsoleAlertHandler;
#[async_trait::async_trait]
impl AlertHandler for ConsoleAlertHandler {
fn name(&self) -> &str {
"console"
}
async fn handle(&self, alert: &Alert) -> Result<(), MatError> {
let priority_indicator = match alert.priority() {
Priority::Critical => "🔴",
Priority::High => "🟠",
Priority::Medium => "🟡",
Priority::Low => "🔵",
};
println!("\n{} ALERT {}", priority_indicator, "=".repeat(50));
println!("ID: {}", alert.id());
println!("Priority: {:?}", alert.priority());
println!("Title: {}", alert.payload().title);
println!("{}", "=".repeat(60));
println!("{}", alert.payload().message);
println!("{}", "=".repeat(60));
println!("Recommended Action: {}", alert.payload().recommended_action);
println!("{}\n", "=".repeat(60));
Ok(())
}
}
/// Audio alert handler (placeholder)
pub struct AudioAlertHandler;
#[async_trait::async_trait]
impl AlertHandler for AudioAlertHandler {
fn name(&self) -> &str {
"audio"
}
async fn handle(&self, alert: &Alert) -> Result<(), MatError> {
// In production, this would trigger actual audio alerts
let pattern = alert.priority().audio_pattern();
tracing::debug!(
alert_id = %alert.id(),
pattern,
"Would play audio alert"
);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::{SurvivorId, TriageStatus, AlertPayload};
fn create_test_alert() -> Alert {
Alert::new(
SurvivorId::new(),
Priority::High,
AlertPayload::new("Test Alert", "Test message", TriageStatus::Delayed),
)
}
#[tokio::test]
async fn test_dispatch_alert() {
let dispatcher = AlertDispatcher::new(AlertConfig::default());
let alert = create_test_alert();
let result = dispatcher.dispatch(alert).await;
assert!(result.is_ok());
assert_eq!(dispatcher.pending_count(), 1);
}
#[tokio::test]
async fn test_acknowledge_alert() {
let dispatcher = AlertDispatcher::new(AlertConfig::default());
let alert = create_test_alert();
let alert_id = alert.id().clone();
dispatcher.dispatch(alert).await.unwrap();
let result = dispatcher.acknowledge(&alert_id, "Team Alpha");
assert!(result.is_ok());
let pending = dispatcher.pending();
assert!(pending.iter().any(|a| a.id() == &alert_id && a.acknowledged_by() == Some("Team Alpha")));
}
#[tokio::test]
async fn test_resolve_alert() {
let dispatcher = AlertDispatcher::new(AlertConfig::default());
let alert = create_test_alert();
let alert_id = alert.id().clone();
dispatcher.dispatch(alert).await.unwrap();
let resolution = crate::domain::AlertResolution {
resolution_type: crate::domain::ResolutionType::Rescued,
notes: "Survivor extracted successfully".to_string(),
resolved_by: Some("Team Alpha".to_string()),
resolved_at: chrono::Utc::now(),
};
dispatcher.resolve(&alert_id, resolution).unwrap();
assert_eq!(dispatcher.pending_count(), 0);
}
}

View File

@@ -0,0 +1,288 @@
//! Alert generation from survivor detections.
use crate::domain::{
Alert, AlertPayload, Priority, Survivor, TriageStatus, ScanZoneId,
};
use crate::MatError;
/// Generator for alerts based on survivor status
pub struct AlertGenerator {
/// Zone name lookup (would be connected to event in production)
zone_names: std::collections::HashMap<ScanZoneId, String>,
}
impl AlertGenerator {
/// Create a new alert generator
pub fn new() -> Self {
Self {
zone_names: std::collections::HashMap::new(),
}
}
/// Register a zone name
pub fn register_zone(&mut self, zone_id: ScanZoneId, name: String) {
self.zone_names.insert(zone_id, name);
}
/// Generate an alert for a survivor
pub fn generate(&self, survivor: &Survivor) -> Result<Alert, MatError> {
let priority = Priority::from_triage(survivor.triage_status());
let payload = self.create_payload(survivor);
Ok(Alert::new(survivor.id().clone(), priority, payload))
}
/// Generate an escalation alert
pub fn generate_escalation(
&self,
survivor: &Survivor,
reason: &str,
) -> Result<Alert, MatError> {
let mut payload = self.create_payload(survivor);
payload.title = format!("ESCALATED: {}", payload.title);
payload.message = format!(
"{}\n\nReason for escalation: {}",
payload.message, reason
);
// Escalated alerts are always at least high priority
let priority = match survivor.triage_status() {
TriageStatus::Immediate => Priority::Critical,
_ => Priority::High,
};
Ok(Alert::new(survivor.id().clone(), priority, payload))
}
/// Generate a status change alert
pub fn generate_status_change(
&self,
survivor: &Survivor,
previous_status: &TriageStatus,
) -> Result<Alert, MatError> {
let mut payload = self.create_payload(survivor);
payload.title = format!(
"Status Change: {}{}",
previous_status, survivor.triage_status()
);
// Determine if this is an upgrade (worse) or downgrade (better)
let is_upgrade = survivor.triage_status().priority() < previous_status.priority();
if is_upgrade {
payload.message = format!(
"URGENT: Survivor condition has WORSENED.\n{}\n\nPrevious: {}\nCurrent: {}",
payload.message,
previous_status,
survivor.triage_status()
);
} else {
payload.message = format!(
"Survivor condition has improved.\n{}\n\nPrevious: {}\nCurrent: {}",
payload.message,
previous_status,
survivor.triage_status()
);
}
let priority = if is_upgrade {
Priority::from_triage(survivor.triage_status())
} else {
Priority::Medium
};
Ok(Alert::new(survivor.id().clone(), priority, payload))
}
/// Create alert payload from survivor data
fn create_payload(&self, survivor: &Survivor) -> AlertPayload {
let zone_name = self.zone_names
.get(survivor.zone_id())
.map(String::as_str)
.unwrap_or("Unknown Zone");
let title = format!(
"{} Survivor Detected - {}",
survivor.triage_status(),
zone_name
);
let vital_info = self.format_vital_signs(survivor);
let location_info = self.format_location(survivor);
let message = format!(
"Survivor ID: {}\n\
Zone: {}\n\
Triage: {}\n\
Confidence: {:.0}%\n\n\
Vital Signs:\n{}\n\n\
Location:\n{}",
survivor.id(),
zone_name,
survivor.triage_status(),
survivor.confidence() * 100.0,
vital_info,
location_info
);
let recommended_action = self.recommend_action(survivor);
AlertPayload::new(title, message, survivor.triage_status().clone())
.with_action(recommended_action)
.with_metadata("zone_id", survivor.zone_id().to_string())
.with_metadata("confidence", format!("{:.2}", survivor.confidence()))
}
/// Format vital signs for display
fn format_vital_signs(&self, survivor: &Survivor) -> String {
let vitals = survivor.vital_signs();
let mut lines = Vec::new();
if let Some(reading) = vitals.latest() {
if let Some(breathing) = &reading.breathing {
lines.push(format!(
" Breathing: {:.1} BPM ({:?})",
breathing.rate_bpm, breathing.pattern_type
));
} else {
lines.push(" Breathing: Not detected".to_string());
}
if let Some(heartbeat) = &reading.heartbeat {
lines.push(format!(
" Heartbeat: {:.0} BPM ({:?})",
heartbeat.rate_bpm, heartbeat.strength
));
}
lines.push(format!(
" Movement: {:?} (intensity: {:.1})",
reading.movement.movement_type,
reading.movement.intensity
));
} else {
lines.push(" No recent readings".to_string());
}
lines.join("\n")
}
/// Format location for display
fn format_location(&self, survivor: &Survivor) -> String {
match survivor.location() {
Some(loc) => {
let depth_str = if loc.is_buried() {
format!("{:.1}m below surface", loc.depth())
} else {
"At surface level".to_string()
};
format!(
" Position: ({:.1}, {:.1})\n\
Depth: {}\n\
Uncertainty: ±{:.1}m",
loc.x, loc.y,
depth_str,
loc.uncertainty.horizontal_error
)
}
None => " Position not yet determined".to_string(),
}
}
/// Recommend action based on triage status
fn recommend_action(&self, survivor: &Survivor) -> String {
match survivor.triage_status() {
TriageStatus::Immediate => {
"IMMEDIATE RESCUE REQUIRED. Deploy heavy rescue team. \
Prepare for airway management and critical care on extraction."
}
TriageStatus::Delayed => {
"Rescue team required. Mark location. Provide reassurance \
if communication is possible. Monitor for status changes."
}
TriageStatus::Minor => {
"Lower priority. Guide to extraction if conscious and mobile. \
Assign walking wounded assistance team."
}
TriageStatus::Deceased => {
"Mark location for recovery. Do not allocate rescue resources. \
Document for incident report."
}
TriageStatus::Unknown => {
"Requires additional assessment. Deploy scout team with \
enhanced detection equipment to confirm status."
}
}
.to_string()
}
}
impl Default for AlertGenerator {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::{BreathingPattern, BreathingType, ConfidenceScore, VitalSignsReading};
use chrono::Utc;
fn create_test_survivor() -> Survivor {
let vitals = VitalSignsReading {
breathing: Some(BreathingPattern {
rate_bpm: 35.0,
amplitude: 0.7,
regularity: 0.5,
pattern_type: BreathingType::Labored,
}),
heartbeat: None,
movement: Default::default(),
timestamp: Utc::now(),
confidence: ConfidenceScore::new(0.8),
};
Survivor::new(ScanZoneId::new(), vitals, None)
}
#[test]
fn test_generate_alert() {
let generator = AlertGenerator::new();
let survivor = create_test_survivor();
let result = generator.generate(&survivor);
assert!(result.is_ok());
let alert = result.unwrap();
assert!(alert.is_pending());
}
#[test]
fn test_escalation_alert() {
let generator = AlertGenerator::new();
let survivor = create_test_survivor();
let alert = generator.generate_escalation(&survivor, "Vital signs deteriorating")
.unwrap();
assert!(alert.payload().title.contains("ESCALATED"));
assert!(matches!(alert.priority(), Priority::Critical | Priority::High));
}
#[test]
fn test_status_change_alert() {
let generator = AlertGenerator::new();
let survivor = create_test_survivor();
let alert = generator.generate_status_change(
&survivor,
&TriageStatus::Minor,
).unwrap();
assert!(alert.payload().title.contains("Status Change"));
}
}

View File

@@ -0,0 +1,9 @@
//! Alerting module for emergency notifications.
mod generator;
mod dispatcher;
mod triage_service;
pub use generator::AlertGenerator;
pub use dispatcher::{AlertDispatcher, AlertConfig};
pub use triage_service::{TriageService, PriorityCalculator};

View File

@@ -0,0 +1,317 @@
//! Triage service for calculating and updating survivor priority.
use crate::domain::{
Priority, Survivor, TriageStatus, VitalSignsReading,
triage::TriageCalculator,
};
/// Service for triage operations
pub struct TriageService;
impl TriageService {
/// Calculate triage status from vital signs
pub fn calculate_triage(vitals: &VitalSignsReading) -> TriageStatus {
TriageCalculator::calculate(vitals)
}
/// Check if survivor should be upgraded
pub fn should_upgrade(survivor: &Survivor) -> bool {
TriageCalculator::should_upgrade(
survivor.triage_status(),
survivor.is_deteriorating(),
)
}
/// Get upgraded status
pub fn upgrade_status(current: &TriageStatus) -> TriageStatus {
TriageCalculator::upgrade(current)
}
/// Evaluate overall severity for multiple survivors
pub fn evaluate_mass_casualty(survivors: &[&Survivor]) -> MassCasualtyAssessment {
let total = survivors.len() as u32;
let mut immediate = 0u32;
let mut delayed = 0u32;
let mut minor = 0u32;
let mut deceased = 0u32;
let mut unknown = 0u32;
for survivor in survivors {
match survivor.triage_status() {
TriageStatus::Immediate => immediate += 1,
TriageStatus::Delayed => delayed += 1,
TriageStatus::Minor => minor += 1,
TriageStatus::Deceased => deceased += 1,
TriageStatus::Unknown => unknown += 1,
}
}
let severity = Self::calculate_severity(immediate, delayed, total);
let resource_level = Self::calculate_resource_level(immediate, delayed, minor);
MassCasualtyAssessment {
total,
immediate,
delayed,
minor,
deceased,
unknown,
severity,
resource_level,
}
}
/// Calculate overall severity level
fn calculate_severity(immediate: u32, delayed: u32, total: u32) -> SeverityLevel {
if total == 0 {
return SeverityLevel::Minimal;
}
let critical_ratio = (immediate + delayed) as f64 / total as f64;
if immediate >= 10 || critical_ratio > 0.5 {
SeverityLevel::Critical
} else if immediate >= 5 || critical_ratio > 0.3 {
SeverityLevel::Major
} else if immediate >= 1 || critical_ratio > 0.1 {
SeverityLevel::Moderate
} else {
SeverityLevel::Minimal
}
}
/// Calculate resource level needed
fn calculate_resource_level(immediate: u32, delayed: u32, minor: u32) -> ResourceLevel {
// Each immediate needs ~4 rescuers
// Each delayed needs ~2 rescuers
// Each minor needs ~0.5 rescuers
let rescuers_needed = immediate * 4 + delayed * 2 + minor / 2;
if rescuers_needed >= 100 {
ResourceLevel::MutualAid
} else if rescuers_needed >= 50 {
ResourceLevel::MultiAgency
} else if rescuers_needed >= 20 {
ResourceLevel::Enhanced
} else if rescuers_needed >= 5 {
ResourceLevel::Standard
} else {
ResourceLevel::Minimal
}
}
}
/// Calculator for alert priority
pub struct PriorityCalculator;
impl PriorityCalculator {
/// Calculate priority from triage status
pub fn from_triage(status: &TriageStatus) -> Priority {
Priority::from_triage(status)
}
/// Calculate priority with additional factors
pub fn calculate_with_factors(
status: &TriageStatus,
deteriorating: bool,
time_since_detection_mins: u64,
depth_meters: Option<f64>,
) -> Priority {
let base_priority = Priority::from_triage(status);
// Adjust for deterioration
let priority = if deteriorating && base_priority != Priority::Critical {
match base_priority {
Priority::High => Priority::Critical,
Priority::Medium => Priority::High,
Priority::Low => Priority::Medium,
Priority::Critical => Priority::Critical,
}
} else {
base_priority
};
// Adjust for time (longer = more urgent)
let priority = if time_since_detection_mins > 30 && priority == Priority::Medium {
Priority::High
} else {
priority
};
// Adjust for depth (deeper = more complex rescue)
if let Some(depth) = depth_meters {
if depth > 3.0 && priority == Priority::High {
return Priority::Critical;
}
}
priority
}
}
/// Mass casualty assessment result
#[derive(Debug, Clone)]
pub struct MassCasualtyAssessment {
/// Total survivors detected
pub total: u32,
/// Immediate (Red) count
pub immediate: u32,
/// Delayed (Yellow) count
pub delayed: u32,
/// Minor (Green) count
pub minor: u32,
/// Deceased (Black) count
pub deceased: u32,
/// Unknown count
pub unknown: u32,
/// Overall severity level
pub severity: SeverityLevel,
/// Resource level needed
pub resource_level: ResourceLevel,
}
impl MassCasualtyAssessment {
/// Get count of living survivors
pub fn living(&self) -> u32 {
self.immediate + self.delayed + self.minor
}
/// Get count needing active rescue
pub fn needs_rescue(&self) -> u32 {
self.immediate + self.delayed
}
/// Get summary string
pub fn summary(&self) -> String {
format!(
"MCI Assessment:\n\
Total: {} (Living: {}, Deceased: {})\n\
Immediate: {}, Delayed: {}, Minor: {}\n\
Severity: {:?}, Resources: {:?}",
self.total, self.living(), self.deceased,
self.immediate, self.delayed, self.minor,
self.severity, self.resource_level
)
}
}
/// Severity levels for mass casualty incidents
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SeverityLevel {
/// Few or no critical patients
Minimal,
/// Some critical patients, manageable
Moderate,
/// Many critical patients, challenging
Major,
/// Overwhelming number of critical patients
Critical,
}
/// Resource levels for response
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ResourceLevel {
/// Standard response adequate
Minimal,
/// Standard response needed
Standard,
/// Enhanced response needed
Enhanced,
/// Multi-agency response needed
MultiAgency,
/// Regional mutual aid required
MutualAid,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::{
BreathingPattern, BreathingType, ConfidenceScore, ScanZoneId,
};
use chrono::Utc;
fn create_test_vitals(rate_bpm: f32) -> VitalSignsReading {
VitalSignsReading {
breathing: Some(BreathingPattern {
rate_bpm,
amplitude: 0.8,
regularity: 0.9,
pattern_type: BreathingType::Normal,
}),
heartbeat: None,
movement: Default::default(),
timestamp: Utc::now(),
confidence: ConfidenceScore::new(0.8),
}
}
#[test]
fn test_calculate_triage() {
let normal = create_test_vitals(16.0);
assert!(matches!(
TriageService::calculate_triage(&normal),
TriageStatus::Immediate | TriageStatus::Delayed | TriageStatus::Minor
));
let fast = create_test_vitals(35.0);
assert!(matches!(
TriageService::calculate_triage(&fast),
TriageStatus::Immediate
));
}
#[test]
fn test_priority_from_triage() {
assert_eq!(
PriorityCalculator::from_triage(&TriageStatus::Immediate),
Priority::Critical
);
assert_eq!(
PriorityCalculator::from_triage(&TriageStatus::Delayed),
Priority::High
);
}
#[test]
fn test_mass_casualty_assessment() {
let survivors: Vec<Survivor> = (0..10)
.map(|i| {
let rate = if i < 3 { 35.0 } else if i < 6 { 16.0 } else { 18.0 };
Survivor::new(
ScanZoneId::new(),
create_test_vitals(rate),
None,
)
})
.collect();
let survivor_refs: Vec<&Survivor> = survivors.iter().collect();
let assessment = TriageService::evaluate_mass_casualty(&survivor_refs);
assert_eq!(assessment.total, 10);
assert!(assessment.living() >= assessment.needs_rescue());
}
#[test]
fn test_priority_with_factors() {
// Deteriorating patient should be upgraded
let priority = PriorityCalculator::calculate_with_factors(
&TriageStatus::Delayed,
true,
0,
None,
);
assert_eq!(priority, Priority::Critical);
// Deep burial should upgrade
let priority = PriorityCalculator::calculate_with_factors(
&TriageStatus::Delayed,
false,
0,
Some(4.0),
);
assert_eq!(priority, Priority::Critical);
}
}