mecha10-behavior-runtime 4.0.2

Behavior tree runtime for Mecha10 - unified AI and logic composition system
Documentation
//! Integration tests for seed file validation.
//!
//! Validates that seed behavior tree templates are valid JSON and parse correctly.

use mecha10_behavior_runtime::BehaviorConfig;
use std::fs;
use std::path::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/patrol_simple.json", "seeds/idle_wander.json"];

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

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

        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/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/patrol_simple.json", "seeds/idle_wander.json"];

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

        validate_composition(&config.root, seed_path);
    }
}

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

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

        let mut refs_used = std::collections::HashSet::new();
        collect_config_refs(&config.root, &mut refs_used);

        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() {
    let schema = BehaviorConfig::generate_schema().expect("Failed to generate schema");

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

    assert!(schema.contains("BehaviorConfig"));
    assert!(schema.contains("CompositionConfig"));
    assert!(schema.contains("ParallelPolicyConfig"));
}

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));

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

    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::Watch { children, .. } => {
            assert!(!children.is_empty(), "{}: watch should have children", context);
            for child in children {
                validate_composition(child, context);
            }
        }
        CompositionConfig::Subsumption { layers, .. } => {
            assert!(!layers.is_empty(), "{}: subsumption should have layers", context);
            for layer in layers {
                validate_composition(&layer.node, context);
            }
        }
        CompositionConfig::Node { node, .. } => {
            assert!(!node.is_empty(), "{}: node type must not be empty", context);
        }
        CompositionConfig::Inverter { child, .. }
        | CompositionConfig::ForceSuccess { child, .. }
        | CompositionConfig::ForceFailure { child, .. }
        | CompositionConfig::Repeat { child, .. }
        | CompositionConfig::Retry { child, .. }
        | CompositionConfig::Timeout { child, .. } => {
            validate_composition(child, 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::Watch { children, .. } => {
            for child in children {
                collect_config_refs(child, refs);
            }
        }
        CompositionConfig::Subsumption { layers, .. } => {
            for layer in layers {
                collect_config_refs(&layer.node, refs);
            }
        }
        CompositionConfig::Node { config_ref, .. } => {
            if let Some(ref_key) = config_ref {
                refs.insert(ref_key.clone());
            }
        }
        CompositionConfig::Inverter { child, .. }
        | CompositionConfig::ForceSuccess { child, .. }
        | CompositionConfig::ForceFailure { child, .. }
        | CompositionConfig::Repeat { child, .. }
        | CompositionConfig::Retry { child, .. }
        | CompositionConfig::Timeout { child, .. } => {
            collect_config_refs(child, refs);
        }
    }
}