mecha10-runtime 0.6.5

Runtime supervisor for Mecha10 nodes - launching, monitoring, and lifecycle management
Documentation
//! Unit tests for lifecycle management

use anyhow::Result;
use mecha10_runtime::lifecycle::{LifecycleManager, ModeConfig, SupervisorTrait};
use std::collections::{HashMap, HashSet};
use std::sync::Arc;

/// Mock supervisor for testing
struct MockSupervisor {
    started: Arc<tokio::sync::Mutex<Vec<String>>>,
    stopped: Arc<tokio::sync::Mutex<Vec<String>>>,
    running: Arc<tokio::sync::Mutex<HashSet<String>>>,
}

impl MockSupervisor {
    fn new() -> Self {
        Self {
            started: Arc::new(tokio::sync::Mutex::new(Vec::new())),
            stopped: Arc::new(tokio::sync::Mutex::new(Vec::new())),
            running: Arc::new(tokio::sync::Mutex::new(HashSet::new())),
        }
    }
}

#[async_trait::async_trait]
impl SupervisorTrait for MockSupervisor {
    async fn start_node(&self, name: &str) -> Result<()> {
        self.started.lock().await.push(name.to_string());
        self.running.lock().await.insert(name.to_string());
        Ok(())
    }

    async fn stop_node(&self, name: &str) -> Result<()> {
        self.stopped.lock().await.push(name.to_string());
        self.running.lock().await.remove(name);
        Ok(())
    }

    fn get_running_nodes(&self) -> Vec<String> {
        vec![]
    }
}

fn create_test_modes() -> HashMap<String, ModeConfig> {
    let mut modes = HashMap::new();

    modes.insert(
        "startup".to_string(),
        ModeConfig {
            nodes: vec!["listener".to_string(), "speaker".to_string()],
            description: Some("Basic startup mode".to_string()),
        },
    );

    modes.insert(
        "simulation".to_string(),
        ModeConfig {
            nodes: vec![
                "listener".to_string(),
                "speaker".to_string(),
                "simulator".to_string(),
                "object-detector".to_string(),
            ],
            description: Some("Simulation mode".to_string()),
        },
    );

    modes.insert(
        "teleop".to_string(),
        ModeConfig {
            nodes: vec!["listener".to_string(), "speaker".to_string(), "teleop".to_string()],
            description: Some("Teleop mode".to_string()),
        },
    );

    modes
}

#[tokio::test]
async fn test_lifecycle_manager_creation() {
    let supervisor = Arc::new(MockSupervisor::new());
    let modes = create_test_modes();

    let manager = LifecycleManager::new(supervisor, modes, "startup".to_string());

    assert_eq!(manager.current_mode(), "startup");
    assert_eq!(manager.running_nodes().len(), 0);
}

#[tokio::test]
async fn test_mode_diff_calculation() {
    let supervisor = Arc::new(MockSupervisor::new());
    let modes = create_test_modes();
    let manager = LifecycleManager::new(supervisor, modes, "startup".to_string());

    let diff = manager.calculate_mode_diff("simulation").unwrap();

    assert_eq!(diff.start.len(), 4);
    assert!(diff.start.contains(&"simulator".to_string()));
    assert!(diff.start.contains(&"object-detector".to_string()));

    assert_eq!(diff.stop.len(), 0);
}

#[tokio::test]
async fn test_mode_transition() {
    let supervisor = Arc::new(MockSupervisor::new());
    let modes = create_test_modes();
    let mut manager = LifecycleManager::new(supervisor.clone(), modes, "startup".to_string());

    let result = manager.change_mode("startup").await.unwrap();

    assert_eq!(result.previous_mode, "startup");
    assert_eq!(result.current_mode, "startup");
    assert_eq!(result.nodes_started.len(), 2);
    assert!(result.nodes_started.contains(&"listener".to_string()));
    assert!(result.nodes_started.contains(&"speaker".to_string()));

    let result = manager.change_mode("simulation").await.unwrap();

    assert_eq!(result.previous_mode, "startup");
    assert_eq!(result.current_mode, "simulation");
    assert_eq!(result.nodes_started.len(), 2);
    assert!(result.nodes_started.contains(&"simulator".to_string()));
    assert_eq!(result.nodes_stopped.len(), 0);
}

#[tokio::test]
async fn test_invalid_mode() {
    let supervisor = Arc::new(MockSupervisor::new());
    let modes = create_test_modes();
    let mut manager = LifecycleManager::new(supervisor, modes, "startup".to_string());

    let result = manager.change_mode("invalid-mode").await;

    assert!(result.is_err());
    assert!(result.unwrap_err().to_string().contains("not found"));
}

#[tokio::test]
async fn test_stop_nodes_on_transition() {
    let supervisor = Arc::new(MockSupervisor::new());
    let modes = create_test_modes();

    let mut modes_with_hardware = modes;
    modes_with_hardware.insert(
        "hardware".to_string(),
        ModeConfig {
            nodes: vec!["imu".to_string(), "motor".to_string(), "listener".to_string()],
            description: Some("Hardware mode".to_string()),
        },
    );

    let mut manager = LifecycleManager::new(supervisor.clone(), modes_with_hardware, "startup".to_string());

    manager.change_mode("hardware").await.unwrap();

    let result = manager.change_mode("simulation").await.unwrap();

    assert!(result.nodes_stopped.contains(&"imu".to_string()));
    assert!(result.nodes_stopped.contains(&"motor".to_string()));
    assert!(!result.nodes_stopped.contains(&"listener".to_string()));
}

#[tokio::test]
async fn test_current_mode_getter() {
    let supervisor = Arc::new(MockSupervisor::new());
    let modes = create_test_modes();
    let manager = LifecycleManager::new(supervisor, modes, "teleop".to_string());

    assert_eq!(manager.current_mode(), "teleop");
}

#[tokio::test]
async fn test_running_nodes_getter() {
    let supervisor = Arc::new(MockSupervisor::new());
    let modes = create_test_modes();
    let mut manager = LifecycleManager::new(supervisor, modes, "startup".to_string());

    manager.change_mode("startup").await.unwrap();

    let running = manager.running_nodes();
    assert_eq!(running.len(), 2);
    assert!(running.contains("listener"));
    assert!(running.contains("speaker"));
}

#[tokio::test]
async fn test_mode_transition_result_structure() {
    let supervisor = Arc::new(MockSupervisor::new());
    let modes = create_test_modes();
    let mut manager = LifecycleManager::new(supervisor, modes, "startup".to_string());

    let result = manager.change_mode("teleop").await.unwrap();

    assert_eq!(result.previous_mode, "startup");
    assert_eq!(result.current_mode, "teleop");
    assert!(result.errors.is_empty());
    assert!(!result.nodes_started.is_empty());
}