mecha10-cli 0.7.0

Mecha10 CLI tool
Documentation
//! Unit tests for CLI lifecycle adapter

use mecha10_cli::dev::lifecycle_adapter::CliLifecycleManager;
use mecha10_cli::types::project::{NodeRef, NodesConfig, ProjectConfig, RobotConfig, TargetEntry, TargetsConfig};

fn create_test_targets_config() -> TargetsConfig {
    TargetsConfig {
        dev: Some(TargetEntry {
            nodes: vec![NodeRef::from("listener"), NodeRef::from("speaker")],
            platforms: None,
        }),
        simulation: Some(TargetEntry {
            nodes: vec![
                NodeRef::from("listener"),
                NodeRef::from("speaker"),
                NodeRef::from("camera"),
            ],
            platforms: None,
        }),
        robot: Some(TargetEntry {
            nodes: vec![
                NodeRef::from("listener"),
                NodeRef::from("speaker"),
                NodeRef::from("imu"),
                NodeRef::from("motor"),
            ],
            platforms: None,
        }),
        remote: None,
        rl: None,
    }
}

fn create_project_config(targets: TargetsConfig) -> ProjectConfig {
    ProjectConfig {
        name: "test".to_string(),
        version: "0.1.0".to_string(),
        template_version: None,
        robot: RobotConfig {
            id: "test".to_string(),
            name: "test".to_string(),
            platform: None,
            description: None,
        },
        simulation: None,
        nodes: NodesConfig::new(),
        targets,
        behaviors: None,
        docker: Default::default(),
        environments: Default::default(),
        actuator: Default::default(),
    }
}

#[test]
fn test_from_project_config_with_targets() {
    let config = create_test_targets_config();
    let project_config = create_project_config(config);

    let manager = CliLifecycleManager::from_project_config(&project_config);

    assert!(manager.is_some());
    let manager = manager.unwrap();
    assert_eq!(manager.current_mode(), "dev");
}

#[test]
fn test_from_project_config_without_targets() {
    let project_config = create_project_config(TargetsConfig::default());

    let manager = CliLifecycleManager::from_project_config(&project_config);

    assert!(manager.is_none());
}

#[test]
fn test_nodes_for_current_mode() {
    let config = create_test_targets_config();
    let project_config = create_project_config(config);

    let manager = CliLifecycleManager::from_project_config(&project_config).unwrap();

    let nodes = manager.nodes_for_current_mode();
    assert_eq!(nodes.len(), 2);
    assert!(nodes.contains(&"listener".to_string()));
    assert!(nodes.contains(&"speaker".to_string()));
}

#[test]
fn test_mark_nodes_running() {
    let config = create_test_targets_config();
    let project_config = create_project_config(config);

    let mut manager = CliLifecycleManager::from_project_config(&project_config).unwrap();

    manager.mark_nodes_running(&["listener".to_string(), "speaker".to_string()]);

    let diff = manager.calculate_mode_diff("dev").unwrap();
    assert_eq!(diff.start.len(), 0);
    assert_eq!(diff.stop.len(), 0);
}

#[test]
fn test_calculate_mode_diff_start_nodes() {
    let config = create_test_targets_config();
    let project_config = create_project_config(config);

    let mut manager = CliLifecycleManager::from_project_config(&project_config).unwrap();

    manager.mark_nodes_running(&["listener".to_string(), "speaker".to_string()]);

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

    assert_eq!(diff.start.len(), 1);
    assert!(diff.start.contains(&"camera".to_string()));
    assert_eq!(diff.stop.len(), 0);
}

#[test]
fn test_calculate_mode_diff_stop_nodes() {
    let config = create_test_targets_config();
    let project_config = create_project_config(config);

    let mut manager = CliLifecycleManager::from_project_config(&project_config).unwrap();

    manager.mark_nodes_running(&["listener".to_string(), "speaker".to_string(), "camera".to_string()]);

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

    assert_eq!(diff.start.len(), 0);
    assert_eq!(diff.stop.len(), 1);
    assert!(diff.stop.contains(&"camera".to_string()));
}

#[test]
fn test_calculate_mode_diff_stops_nodes_not_in_target() {
    let config = create_test_targets_config();
    let project_config = create_project_config(config);

    let mut manager = CliLifecycleManager::from_project_config(&project_config).unwrap();

    manager.mark_nodes_running(&["listener".to_string(), "speaker".to_string(), "camera".to_string()]);

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

    assert!(diff.start.contains(&"imu".to_string()));
    assert!(diff.start.contains(&"motor".to_string()));
    assert!(diff.stop.contains(&"camera".to_string()));
}

#[test]
fn test_calculate_mode_diff_keeps_common_node_running() {
    let config = create_test_targets_config();
    let project_config = create_project_config(config);

    let mut manager = CliLifecycleManager::from_project_config(&project_config).unwrap();

    manager.mark_nodes_running(&["listener".to_string(), "speaker".to_string()]);

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

    assert!(
        !diff.stop.contains(&"listener".to_string()),
        "listener is common to dev and simulation and must not be stopped, got stop={:?}",
        diff.stop
    );
    assert!(
        !diff.stop.contains(&"speaker".to_string()),
        "speaker is common to dev and simulation and must not be stopped, got stop={:?}",
        diff.stop
    );
    assert!(
        !diff.start.contains(&"listener".to_string()),
        "listener is already running and must not be restarted, got start={:?}",
        diff.start
    );
}

#[test]
fn test_change_mode() {
    let config = create_test_targets_config();
    let project_config = create_project_config(config);

    let mut manager = CliLifecycleManager::from_project_config(&project_config).unwrap();

    assert_eq!(manager.current_mode(), "dev");

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

    assert_eq!(manager.current_mode(), "simulation");
    assert_eq!(diff.start.len(), 3);
    assert_eq!(diff.stop.len(), 0);
}

#[test]
fn test_change_mode_invalid() {
    let config = create_test_targets_config();
    let project_config = create_project_config(config);

    let mut manager = CliLifecycleManager::from_project_config(&project_config).unwrap();

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

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

#[test]
fn test_available_modes() {
    let config = create_test_targets_config();
    let project_config = create_project_config(config);

    let manager = CliLifecycleManager::from_project_config(&project_config).unwrap();

    let modes = manager.available_modes();
    assert_eq!(modes.len(), 3);
    assert!(modes.contains(&"dev"));
    assert!(modes.contains(&"simulation"));
    assert!(modes.contains(&"robot"));
}

#[test]
fn test_validate_valid_config() {
    let config = create_test_targets_config();
    let available_nodes = vec![
        "listener".to_string(),
        "speaker".to_string(),
        "camera".to_string(),
        "imu".to_string(),
        "motor".to_string(),
    ];

    let result = CliLifecycleManager::validate(&config, &available_nodes);
    assert!(result.is_ok());
}

#[test]
fn test_validate_missing_node() {
    let config = create_test_targets_config();
    let available_nodes = vec!["listener".to_string(), "speaker".to_string()];

    let result = CliLifecycleManager::validate(&config, &available_nodes);
    assert!(result.is_err());
    assert!(result.unwrap_err().to_string().contains("unknown node"));
}

/// `LAB-4131`: two keyed instances of the same node type under one target each get their own
/// entry in `nodes_for_current_mode()`/mode-diffing, distinguished by the instance id encoded
/// into `NodeRef::wire_identifier()`.
#[test]
fn test_nodes_for_current_mode_distinguishes_keyed_instances() {
    let config = TargetsConfig {
        dev: Some(TargetEntry {
            nodes: vec![
                NodeRef::Keyed {
                    name: "@mecha10/camera".to_string(),
                    instance_id: Some("real".to_string()),
                    camera_id: None,
                },
                NodeRef::Keyed {
                    name: "@mecha10/camera".to_string(),
                    instance_id: Some("sim".to_string()),
                    camera_id: None,
                },
            ],
            platforms: None,
        }),
        simulation: None,
        robot: None,
        remote: None,
        rl: None,
    };
    let project_config = create_project_config(config);

    let manager = CliLifecycleManager::from_project_config(&project_config).unwrap();

    let nodes = manager.nodes_for_current_mode();
    assert_eq!(nodes.len(), 2);
    assert!(nodes.contains(&"@mecha10/camera::real".to_string()));
    assert!(nodes.contains(&"@mecha10/camera::sim".to_string()));
}

#[test]
fn test_mark_nodes_stopped() {
    let config = create_test_targets_config();
    let project_config = create_project_config(config);

    let mut manager = CliLifecycleManager::from_project_config(&project_config).unwrap();

    manager.mark_nodes_running(&["listener".to_string(), "speaker".to_string()]);

    manager.mark_nodes_stopped(&["listener".to_string()]);

    let diff = manager.calculate_mode_diff("dev").unwrap();
    assert_eq!(diff.start.len(), 1);
    assert!(diff.start.contains(&"listener".to_string()));
}