mecha10-cli 0.1.47

Mecha10 CLI tool
Documentation
// Tests for mecha10_cli::context

use mecha10_cli::*;
use serial_test::serial;
use std::env;
use std::path::PathBuf;

/// Helper to clear all environment variables that affect CliContext
fn clear_test_env() {
    env::remove_var("REDIS_URL");
    env::remove_var("MECHA10_REDIS_URL");
    env::remove_var("DATABASE_URL");
    env::remove_var("POSTGRES_URL");
    env::remove_var("DEV_MODE");
    env::remove_var("MECHA10_DEV_MODE");
}

#[test]
#[serial]
fn test_builder_defaults() {
    clear_test_env();
    let ctx = CliContextBuilder::new().build().unwrap();
    assert_eq!(ctx.redis_url(), "redis://localhost:6379");
    assert_eq!(ctx.postgres_url(), None);
    assert!(!ctx.is_verbose());
    assert!(!ctx.is_dev_mode());
    assert_eq!(ctx.log_level, Level::INFO);
}

#[test]
fn test_builder_explicit_values() {
    let ctx = CliContextBuilder::new()
        .redis_url("redis://custom:1234".to_string())
        .postgres_url(Some("postgres://test".to_string()))
        .verbose(true)
        .dev_mode(true)
        .log_level(Level::DEBUG)
        .build()
        .unwrap();

    assert_eq!(ctx.redis_url(), "redis://custom:1234");
    assert_eq!(ctx.postgres_url(), Some("postgres://test"));
    assert!(ctx.is_verbose());
    assert!(ctx.is_dev_mode());
    assert_eq!(ctx.log_level, Level::DEBUG);
}

#[test]
#[serial]
fn test_builder_env_precedence() {
    clear_test_env();
    // Set env vars
    env::set_var("REDIS_URL", "redis://from-env:6379");
    env::set_var("DATABASE_URL", "postgres://from-env");
    env::set_var("MECHA10_DEV_MODE", "true");

    let ctx = CliContextBuilder::new().build().unwrap();

    // Should use env vars
    assert_eq!(ctx.redis_url(), "redis://from-env:6379");
    assert_eq!(ctx.postgres_url(), Some("postgres://from-env"));
    assert!(ctx.is_dev_mode());

    // Clean up
    clear_test_env();
}

#[test]
#[serial]
fn test_builder_explicit_overrides_env() {
    // Set env var
    env::set_var("REDIS_URL", "redis://from-env:6379");

    let ctx = CliContextBuilder::new()
        .redis_url("redis://explicit:1234".to_string())
        .build()
        .unwrap();

    // Explicit value should win
    assert_eq!(ctx.redis_url(), "redis://explicit:1234");

    // Clean up
    env::remove_var("REDIS_URL");
}

#[test]
#[serial]
fn test_builder_mecha10_env_priority() {
    // Set both MECHA10_REDIS_URL and REDIS_URL
    env::set_var("MECHA10_REDIS_URL", "redis://mecha10:6379");
    env::set_var("REDIS_URL", "redis://generic:6379");

    let ctx = CliContextBuilder::new().build().unwrap();

    // MECHA10_REDIS_URL should have priority
    assert_eq!(ctx.redis_url(), "redis://mecha10:6379");

    // Clean up
    env::remove_var("MECHA10_REDIS_URL");
    env::remove_var("REDIS_URL");
}

#[test]
#[serial]
fn test_convenience_methods() {
    clear_test_env();
    let ctx = CliContextBuilder::new()
        .verbose(true)
        .dev_mode(false)
        .redis_url("redis://test:6379".to_string())
        .build()
        .unwrap();

    assert!(ctx.is_verbose());
    assert!(!ctx.is_dev_mode());
    assert_eq!(ctx.redis_url(), "redis://test:6379");
    assert_eq!(ctx.postgres_url(), None);
}

#[test]
fn test_project_path() {
    let working_dir = PathBuf::from("/test/project");
    let ctx = CliContextBuilder::new()
        .working_dir(working_dir.clone())
        .build()
        .unwrap();

    assert_eq!(ctx.project_path("config"), PathBuf::from("/test/project/config"));
    assert_eq!(
        ctx.project_path("nodes/camera"),
        PathBuf::from("/test/project/nodes/camera")
    );
}

#[test]
#[serial]
fn test_dev_mode_env_variations() {
    clear_test_env();
    // Test DEV_MODE=1
    env::set_var("DEV_MODE", "1");
    let ctx = CliContextBuilder::new().build().unwrap();
    assert!(ctx.is_dev_mode());
    clear_test_env();

    // Test DEV_MODE=true
    env::set_var("DEV_MODE", "true");
    let ctx = CliContextBuilder::new().build().unwrap();
    assert!(ctx.is_dev_mode());
    clear_test_env();

    // Test DEV_MODE=True (case insensitive)
    env::set_var("DEV_MODE", "True");
    let ctx = CliContextBuilder::new().build().unwrap();
    assert!(ctx.is_dev_mode());
    clear_test_env();

    // Test DEV_MODE=false
    env::set_var("DEV_MODE", "false");
    let ctx = CliContextBuilder::new().build().unwrap();
    assert!(!ctx.is_dev_mode());
    clear_test_env();

    // Test DEV_MODE=0
    env::set_var("DEV_MODE", "0");
    let ctx = CliContextBuilder::new().build().unwrap();
    assert!(!ctx.is_dev_mode());
    clear_test_env();
}

#[test]
fn test_config_path_default() {
    let working_dir = PathBuf::from("/test/project");
    let ctx = CliContextBuilder::new()
        .working_dir(working_dir.clone())
        .build()
        .unwrap();

    assert_eq!(ctx.config_path, PathBuf::from("/test/project/mecha10.json"));
}

#[test]
fn test_config_path_explicit() {
    let custom_path = PathBuf::from("/custom/config.json");
    let ctx = CliContextBuilder::new()
        .config_path(Some(custom_path.clone()))
        .build()
        .unwrap();

    assert_eq!(ctx.config_path, custom_path);
}

#[test]
#[serial]
fn test_default_context() {
    clear_test_env();
    let ctx = CliContext::default();
    assert_eq!(ctx.log_level, Level::INFO);
    assert_eq!(ctx.redis_url(), "redis://localhost:6379");
    assert!(!ctx.is_verbose());
    assert!(!ctx.is_dev_mode());
}

// ========== Tests for Service Accessors ==========

#[test]
fn test_template_service_lazy_init() {
    let mut ctx = CliContextBuilder::new().build().unwrap();
    let _template1 = ctx.template();
    let _template2 = ctx.template();
    // Should create only once and reuse
}

#[test]
fn test_simulation_service_lazy_init() {
    let mut ctx = CliContextBuilder::new().build().unwrap();
    let _sim1 = ctx.simulation();
    let _sim2 = ctx.simulation();
    // Should create only once and reuse
}

#[test]
fn test_process_service_lazy_init() {
    let mut ctx = CliContextBuilder::new().build().unwrap();
    let _proc1 = ctx.process();
    let _proc2 = ctx.process();
    // Should create only once and reuse
}

#[test]
fn test_docker_service_lazy_init() {
    let mut ctx = CliContextBuilder::new().build().unwrap();
    let _docker1 = ctx.docker();
    let _docker2 = ctx.docker();
    // Should create only once and reuse
}

#[test]
fn test_deployment_service_lazy_init() {
    let mut ctx = CliContextBuilder::new().build().unwrap();
    let _deploy1 = ctx.deployment();
    let _deploy2 = ctx.deployment();
    // Should create only once and reuse
}

#[test]
fn test_component_catalog_service_lazy_init() {
    let mut ctx = CliContextBuilder::new().build().unwrap();
    let _catalog1 = ctx.component_catalog();
    let _catalog2 = ctx.component_catalog();
    // Should create only once and reuse
}

#[test]
fn test_discovery_service_lazy_init() {
    let mut ctx = CliContextBuilder::new().build().unwrap();
    let _discovery1 = ctx.discovery();
    let _discovery2 = ctx.discovery();
    // Should create only once and reuse
}

// ========== Tests for Environment Helpers ==========

#[test]
fn test_is_ci() {
    let ctx = CliContextBuilder::new().build().unwrap();

    // Save current state
    let ci_was_set = env::var("CI").is_ok();

    // Test CI detection
    env::set_var("CI", "true");
    assert!(ctx.is_ci());

    // Test GITHUB_ACTIONS
    env::remove_var("CI");
    env::set_var("GITHUB_ACTIONS", "true");
    assert!(ctx.is_ci());

    // Test GITLAB_CI
    env::remove_var("GITHUB_ACTIONS");
    env::set_var("GITLAB_CI", "true");
    assert!(ctx.is_ci());

    // Clean up
    env::remove_var("GITLAB_CI");
    if !ci_was_set {
        env::remove_var("CI");
    }
}

#[test]
fn test_path_helpers() {
    let working_dir = PathBuf::from("/test/project");
    let ctx = CliContextBuilder::new()
        .working_dir(working_dir.clone())
        .build()
        .unwrap();

    assert_eq!(ctx.logs_dir(), PathBuf::from("/test/project/logs"));
    assert_eq!(ctx.data_dir(), PathBuf::from("/test/project/data"));
    assert_eq!(ctx.recordings_dir(), PathBuf::from("/test/project/data/recordings"));
    assert_eq!(ctx.maps_dir(), PathBuf::from("/test/project/data/maps"));
    assert_eq!(ctx.telemetry_dir(), PathBuf::from("/test/project/data/telemetry"));
    assert_eq!(ctx.simulation_dir(), PathBuf::from("/test/project/simulation"));
    assert_eq!(ctx.target_debug_dir(), PathBuf::from("/test/project/target/debug"));
    assert_eq!(ctx.target_release_dir(), PathBuf::from("/test/project/target/release"));
    assert_eq!(ctx.packages_dir(), PathBuf::from("/test/project/target/packages"));
}

#[test]
fn test_ensure_dir() {
    use tempfile::TempDir;

    let temp = TempDir::new().unwrap();
    let ctx = CliContextBuilder::new()
        .working_dir(temp.path().to_path_buf())
        .build()
        .unwrap();

    let test_dir = temp.path().join("test_dir");
    assert!(!test_dir.exists());

    ctx.ensure_dir(&test_dir).unwrap();
    assert!(test_dir.exists());

    // Should not fail if directory already exists
    ctx.ensure_dir(&test_dir).unwrap();
    assert!(test_dir.exists());
}

// ========== Tests for Validation Methods ==========

#[test]
fn test_validate_project_structure_not_initialized() {
    use tempfile::TempDir;

    let temp = TempDir::new().unwrap();
    let ctx = CliContextBuilder::new()
        .working_dir(temp.path().to_path_buf())
        .build()
        .unwrap();

    let result = ctx.validate_project_structure();
    assert!(result.is_err());
    assert!(result.unwrap_err().to_string().contains("not initialized"));
}

#[test]
fn test_validate_project_structure_missing_dirs() {
    use tempfile::TempDir;

    let temp = TempDir::new().unwrap();
    let ctx = CliContextBuilder::new()
        .working_dir(temp.path().to_path_buf())
        .build()
        .unwrap();

    // Create mecha10.json to mark as initialized
    std::fs::write(temp.path().join("mecha10.json"), "{}").unwrap();

    let result = ctx.validate_project_structure();
    assert!(result.is_err());
    assert!(result.unwrap_err().to_string().contains("nodes"));
}

#[test]
fn test_validate_project_structure_success() {
    use tempfile::TempDir;

    let temp = TempDir::new().unwrap();
    let ctx = CliContextBuilder::new()
        .working_dir(temp.path().to_path_buf())
        .build()
        .unwrap();

    // Create required structure
    std::fs::write(temp.path().join("mecha10.json"), "{}").unwrap();
    std::fs::create_dir(temp.path().join("nodes")).unwrap();
    std::fs::create_dir(temp.path().join("drivers")).unwrap();

    let result = ctx.validate_project_structure();
    assert!(result.is_ok());
}