mecha10-cli 0.1.47

Mecha10 CLI tool
Documentation
// Tests for mecha10_cli::services::config

use mecha10_cli::services::config::*;

use serial_test::serial;
use tempfile::TempDir;

fn create_test_config(dir: &Path) -> PathBuf {
    let config_path = dir.join("mecha10.json");
    std::fs::write(
        &config_path,
        r#"{
            "robot": {
                "id": "test-robot-001"
            },
            "nodes": {
                "drivers": [],
                "custom": []
            },
            "services": {}
        }"#,
    )
    .unwrap();
    config_path
}

#[test]
fn test_is_initialized() {
    let temp_dir = TempDir::new().unwrap();
    assert!(!ConfigService::is_initialized(temp_dir.path()));

    create_test_config(temp_dir.path());
    assert!(ConfigService::is_initialized(temp_dir.path()));
}

#[tokio::test]
async fn test_load_from() {
    let temp_dir = TempDir::new().unwrap();
    let config_path = create_test_config(temp_dir.path());

    let config = ConfigService::load_from(&config_path).await.unwrap();
    assert_eq!(config.robot.id, "test-robot-001");
}

#[tokio::test]
async fn test_load_from_nonexistent() {
    let result = ConfigService::load_from(&PathBuf::from("nonexistent.json")).await;
    assert!(result.is_err());
    assert!(result.unwrap_err().to_string().contains("not found"));
}

#[tokio::test]
async fn test_load_robot_id() {
    let temp_dir = TempDir::new().unwrap();
    let config_path = create_test_config(temp_dir.path());

    let robot_id = ConfigService::load_robot_id(&config_path).await.unwrap();
    assert_eq!(robot_id, "test-robot-001");
}

#[test]
fn test_find_config_from() {
    let temp_dir = TempDir::new().unwrap();
    create_test_config(temp_dir.path());

    // Should find config in current directory
    let found = ConfigService::find_config_from(temp_dir.path()).unwrap();
    assert_eq!(found, temp_dir.path().join("mecha10.json"));

    // Should find config in parent directory
    let sub_dir = temp_dir.path().join("subdir");
    std::fs::create_dir(&sub_dir).unwrap();
    let found = ConfigService::find_config_from(&sub_dir).unwrap();
    assert_eq!(found, temp_dir.path().join("mecha10.json"));
}

#[test]
fn test_find_config_not_found() {
    let temp_dir = TempDir::new().unwrap();
    let result = ConfigService::find_config_from(temp_dir.path());
    assert!(result.is_err());
    assert!(result.unwrap_err().to_string().contains("No mecha10.json found"));
}

#[test]
fn test_default_config_paths() {
    let paths = ConfigService::default_config_paths();
    assert!(!paths.is_empty());
    assert!(paths.contains(&PathBuf::from("mecha10.json")));
}

#[tokio::test]
#[serial]
async fn test_try_load_from_defaults() {
    let temp_dir = TempDir::new().unwrap();
    std::env::set_current_dir(temp_dir.path()).unwrap();

    create_test_config(temp_dir.path());

    let (path, config) = ConfigService::try_load_from_defaults().await.unwrap();
    assert_eq!(path, PathBuf::from("mecha10.json"));
    assert_eq!(config.robot.id, "test-robot-001");
}

// === New Comprehensive Tests ===

#[tokio::test]
#[serial]
async fn test_load_default() {
    let temp_dir = TempDir::new().unwrap();
    std::env::set_current_dir(temp_dir.path()).unwrap();

    create_test_config(temp_dir.path());

    let config = ConfigService::load_default().await.unwrap();
    assert_eq!(config.robot.id, "test-robot-001");
}

#[tokio::test]
#[serial]
async fn test_load_default_not_found() {
    let temp_dir = TempDir::new().unwrap();
    std::env::set_current_dir(temp_dir.path()).unwrap();

    let result = ConfigService::load_default().await;
    assert!(result.is_err());
    assert!(result.unwrap_err().to_string().contains("not found"));
}

#[tokio::test]
async fn test_load_from_invalid_json() {
    let temp_dir = TempDir::new().unwrap();
    let config_path = temp_dir.path().join("mecha10.json");
    std::fs::write(&config_path, "{ invalid json }").unwrap();

    let result = ConfigService::load_from(&config_path).await;
    assert!(result.is_err());
    assert!(result.unwrap_err().to_string().contains("Failed to parse"));
}

#[tokio::test]
async fn test_load_from_missing_robot_field() {
    let temp_dir = TempDir::new().unwrap();
    let config_path = temp_dir.path().join("mecha10.json");
    std::fs::write(
        &config_path,
        r#"{
            "nodes": {
                "drivers": [],
                "custom": []
            },
            "services": {}
        }"#,
    )
    .unwrap();

    let result = ConfigService::load_from(&config_path).await;
    assert!(result.is_err());
}

#[tokio::test]
async fn test_load_from_empty_file() {
    let temp_dir = TempDir::new().unwrap();
    let config_path = temp_dir.path().join("mecha10.json");
    std::fs::write(&config_path, "").unwrap();

    let result = ConfigService::load_from(&config_path).await;
    assert!(result.is_err());
}

#[test]
#[serial]
fn test_is_initialized_here() {
    let temp_dir = TempDir::new().unwrap();
    std::env::set_current_dir(temp_dir.path()).unwrap();

    assert!(!ConfigService::is_initialized_here());

    create_test_config(temp_dir.path());
    assert!(ConfigService::is_initialized_here());
}

#[test]
fn test_find_config_from_nested_subdirectory() {
    let temp_dir = TempDir::new().unwrap();
    create_test_config(temp_dir.path());

    // Create nested subdirectories
    let level1 = temp_dir.path().join("level1");
    let level2 = level1.join("level2");
    let level3 = level2.join("level3");
    std::fs::create_dir_all(&level3).unwrap();

    // Should find config from 3 levels deep
    let found = ConfigService::find_config_from(&level3).unwrap();
    assert_eq!(found, temp_dir.path().join("mecha10.json"));
}

#[test]
fn test_find_config_from_prefers_nearest() {
    let temp_dir = TempDir::new().unwrap();

    // Create config in root
    create_test_config(temp_dir.path());

    // Create config in subdirectory
    let subdir = temp_dir.path().join("subdir");
    std::fs::create_dir(&subdir).unwrap();
    let subdir_config = create_test_config(&subdir);

    // Should find the nearest one (in subdir)
    let found = ConfigService::find_config_from(&subdir).unwrap();
    assert_eq!(found, subdir_config);
}

#[tokio::test]
#[serial]
async fn test_try_load_from_defaults_config_subdirectory() {
    let temp_dir = TempDir::new().unwrap();
    std::env::set_current_dir(temp_dir.path()).unwrap();

    // Create config in config/ subdirectory
    let config_dir = temp_dir.path().join("config");
    std::fs::create_dir(&config_dir).unwrap();
    create_test_config(&config_dir);

    let (path, config) = ConfigService::try_load_from_defaults().await.unwrap();
    assert_eq!(path, PathBuf::from("config/mecha10.json"));
    assert_eq!(config.robot.id, "test-robot-001");
}

#[tokio::test]
#[serial]
async fn test_try_load_from_defaults_mecha10_subdirectory() {
    let temp_dir = TempDir::new().unwrap();
    std::env::set_current_dir(temp_dir.path()).unwrap();

    // Create config in .mecha10/ subdirectory
    let mecha10_dir = temp_dir.path().join(".mecha10");
    std::fs::create_dir(&mecha10_dir).unwrap();
    create_test_config(&mecha10_dir);

    let (path, config) = ConfigService::try_load_from_defaults().await.unwrap();
    assert_eq!(path, PathBuf::from(".mecha10/mecha10.json"));
    assert_eq!(config.robot.id, "test-robot-001");
}

#[tokio::test]
#[serial]
async fn test_try_load_from_defaults_no_valid_config() {
    let temp_dir = TempDir::new().unwrap();
    std::env::set_current_dir(temp_dir.path()).unwrap();

    let result = ConfigService::try_load_from_defaults().await;
    assert!(result.is_err());
    let err_msg = result.unwrap_err().to_string();
    assert!(err_msg.contains("No valid mecha10.json"));
    assert!(err_msg.contains("mecha10 init"));
}

#[tokio::test]
#[serial]
async fn test_try_load_from_defaults_skips_invalid() {
    let temp_dir = TempDir::new().unwrap();
    std::env::set_current_dir(temp_dir.path()).unwrap();

    // Create invalid config at root
    let invalid_path = temp_dir.path().join("mecha10.json");
    std::fs::write(&invalid_path, "{ invalid }").unwrap();

    // Create valid config in config/
    let config_dir = temp_dir.path().join("config");
    std::fs::create_dir(&config_dir).unwrap();
    create_test_config(&config_dir);

    // Should skip the invalid one and find the valid one
    let (path, config) = ConfigService::try_load_from_defaults().await.unwrap();
    assert_eq!(path, PathBuf::from("config/mecha10.json"));
    assert_eq!(config.robot.id, "test-robot-001");
}

#[tokio::test]
async fn test_load_robot_id_from_nonexistent() {
    let result = ConfigService::load_robot_id(&PathBuf::from("nonexistent.json")).await;
    assert!(result.is_err());
}

#[tokio::test]
async fn test_load_from_permission_denied() {
    // This test is Unix-specific due to permission handling
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;

        let temp_dir = TempDir::new().unwrap();
        let config_path = create_test_config(temp_dir.path());

        // Remove read permissions
        let mut perms = std::fs::metadata(&config_path).unwrap().permissions();
        perms.set_mode(0o000);
        std::fs::set_permissions(&config_path, perms).unwrap();

        let result = ConfigService::load_from(&config_path).await;
        assert!(result.is_err());

        // Restore permissions for cleanup
        let mut perms = std::fs::metadata(&config_path).unwrap().permissions();
        perms.set_mode(0o644);
        std::fs::set_permissions(&config_path, perms).unwrap();
    }
}

#[test]
fn test_default_config_paths_order() {
    let paths = ConfigService::default_config_paths();
    assert_eq!(paths.len(), 3);
    // Should prioritize root mecha10.json first
    assert_eq!(paths[0], PathBuf::from("mecha10.json"));
    assert_eq!(paths[1], PathBuf::from("config/mecha10.json"));
    assert_eq!(paths[2], PathBuf::from(".mecha10/mecha10.json"));
}

#[test]
fn test_validate_nonexistent_file() {
    let result = ConfigService::validate(&PathBuf::from("nonexistent.json"));
    assert!(result.is_err());
    let err_msg = result.unwrap_err().to_string();
    assert!(err_msg.contains("not found"));
    assert!(err_msg.contains("mecha10 init"));
}

// Note: Full validation testing would require valid/invalid configs
// that match mecha10-core's schema. These are integration-level tests.