mecha10-behavior-runtime 0.1.25

Behavior tree runtime for Mecha10 - unified AI and logic composition system
Documentation
//! Integration tests for seed file validation
//!
//! This test suite validates that all seed behavior tree templates:
//! - Are valid JSON
//! - Parse correctly as BehaviorConfig
//! - Have all required fields
//! - Follow the correct schema

use mecha10_behavior_runtime::BehaviorConfig;
use std::fs;
use std::path::Path;

#[test]
fn test_basic_navigation_seed() {
    let seed_path = "seeds/basic_navigation.json";
    validate_seed_file(seed_path);
}

#[test]
fn test_obstacle_avoidance_seed() {
    let seed_path = "seeds/obstacle_avoidance.json";
    validate_seed_file(seed_path);
}

#[test]
fn test_patrol_simple_seed() {
    let seed_path = "seeds/patrol_simple.json";
    validate_seed_file(seed_path);
}

#[test]
fn test_idle_wander_seed() {
    let seed_path = "seeds/idle_wander.json";
    validate_seed_file(seed_path);
}

#[test]
fn test_all_seeds_have_required_fields() {
    let seed_files = vec![
        "seeds/basic_navigation.json",
        "seeds/obstacle_avoidance.json",
        "seeds/patrol_simple.json",
        "seeds/idle_wander.json",
    ];

    for seed_path in seed_files {
        let config = load_config(seed_path);

        // Check required fields
        assert!(!config.name.is_empty(), "{}: name must not be empty", seed_path);
        assert!(config.description.is_some(), "{}: should have description", seed_path);

        // Check schema reference
        assert!(config.schema.is_some(), "{}: should have $schema field", seed_path);

        if let Some(schema) = &config.schema {
            assert!(
                schema.contains("mecha10.dev/schemas/behavior-composition"),
                "{}: schema should reference mecha10.dev",
                seed_path
            );
        }
    }
}

#[test]
fn test_seed_names_match_filenames() {
    let seeds = vec![
        ("seeds/basic_navigation.json", "basic_navigation"),
        ("seeds/obstacle_avoidance.json", "obstacle_avoidance"),
        ("seeds/patrol_simple.json", "patrol_simple"),
        ("seeds/idle_wander.json", "idle_wander"),
    ];

    for (path, expected_name) in seeds {
        let config = load_config(path);
        assert_eq!(config.name, expected_name, "{}: name should match filename", path);
    }
}

#[test]
fn test_seeds_have_valid_composition() {
    let seed_files = vec![
        "seeds/basic_navigation.json",
        "seeds/obstacle_avoidance.json",
        "seeds/patrol_simple.json",
        "seeds/idle_wander.json",
    ];

    for seed_path in seed_files {
        let config = load_config(seed_path);

        // Validate that the root composition has children or is a valid leaf
        validate_composition(&config.root, seed_path);
    }
}

#[test]
fn test_seeds_configs_are_referenced() {
    let seed_files = vec![
        "seeds/basic_navigation.json",
        "seeds/obstacle_avoidance.json",
        "seeds/patrol_simple.json",
        "seeds/idle_wander.json",
    ];

    for seed_path in seed_files {
        let config = load_config(seed_path);

        // Collect all config_refs used in the tree
        let mut refs_used = std::collections::HashSet::new();
        collect_config_refs(&config.root, &mut refs_used);

        // Check that all refs point to valid configs
        for ref_key in refs_used {
            assert!(
                config.configs.contains_key(&ref_key),
                "{}: config_ref '{}' not found in configs map",
                seed_path,
                ref_key
            );
        }
    }
}

#[test]
fn test_json_schema_generation() {
    // Test that we can generate the JSON schema
    let schema = BehaviorConfig::generate_schema().expect("Failed to generate schema");

    // Verify it's valid JSON
    let _parsed: serde_json::Value = serde_json::from_str(&schema).expect("Generated schema is not valid JSON");

    // Verify it contains expected types
    assert!(schema.contains("BehaviorConfig"));
    assert!(schema.contains("CompositionConfig"));
    assert!(schema.contains("ParallelPolicyConfig"));
}

// Helper functions

fn validate_seed_file(seed_path: &str) {
    assert!(Path::new(seed_path).exists(), "Seed file not found: {}", seed_path);

    let content = fs::read_to_string(seed_path).unwrap_or_else(|e| panic!("Failed to read {}: {}", seed_path, e));

    // Test 1: Valid JSON
    let _json: serde_json::Value =
        serde_json::from_str(&content).unwrap_or_else(|e| panic!("{} is not valid JSON: {}", seed_path, e));

    // Test 2: Valid BehaviorConfig
    BehaviorConfig::from_json(&content)
        .unwrap_or_else(|e| panic!("{} failed to parse as BehaviorConfig: {}", seed_path, e));
}

fn load_config(seed_path: &str) -> BehaviorConfig {
    let content = fs::read_to_string(seed_path).unwrap_or_else(|e| panic!("Failed to read {}: {}", seed_path, e));

    BehaviorConfig::from_json(&content).unwrap_or_else(|e| panic!("Failed to parse {}: {}", seed_path, e))
}

fn validate_composition(comp: &mecha10_behavior_runtime::CompositionConfig, context: &str) {
    use mecha10_behavior_runtime::CompositionConfig;

    match comp {
        CompositionConfig::Sequence { children, .. } => {
            assert!(!children.is_empty(), "{}: sequence should have children", context);
            for child in children {
                validate_composition(child, context);
            }
        }
        CompositionConfig::Selector { children, .. } => {
            assert!(!children.is_empty(), "{}: selector should have children", context);
            for child in children {
                validate_composition(child, context);
            }
        }
        CompositionConfig::Parallel { children, .. } => {
            assert!(
                children.len() >= 2,
                "{}: parallel should have at least 2 children",
                context
            );
            for child in children {
                validate_composition(child, context);
            }
        }
        CompositionConfig::Node { node, .. } => {
            assert!(!node.is_empty(), "{}: node type must not be empty", context);
        }
    }
}

fn collect_config_refs(
    comp: &mecha10_behavior_runtime::CompositionConfig,
    refs: &mut std::collections::HashSet<String>,
) {
    use mecha10_behavior_runtime::CompositionConfig;

    match comp {
        CompositionConfig::Sequence { children, .. }
        | CompositionConfig::Selector { children, .. }
        | CompositionConfig::Parallel { children, .. } => {
            for child in children {
                collect_config_refs(child, refs);
            }
        }
        CompositionConfig::Node { config_ref, .. } => {
            if let Some(ref_key) = config_ref {
                refs.insert(ref_key.clone());
            }
        }
    }
}