mecha10-core 4.2.3

Mecha10 core runtime and foundational types
Documentation
#[cfg(feature = "schema-validation")]
mod tests {
    use mecha10_core::schema_validation::*;

    #[test]
    fn test_valid_minimal_config() {
        let config = serde_json::json!({
            "name": "test-robot",
            "version": "1.0.0",
        });

        assert!(validate_project_json(&config).is_ok());
    }

    #[test]
    fn test_invalid_name_format() {
        let config = serde_json::json!({
            "name": "Test Robot",
            "version": "1.0.0"
        });

        assert!(validate_project_json(&config).is_err());
    }

    #[test]
    fn test_invalid_version_format() {
        let config = serde_json::json!({
            "name": "test-robot",
            "version": "1.0"
        });

        assert!(validate_project_json(&config).is_err());
    }

    #[test]
    fn test_duplicate_node_names() {
        let _config = serde_json::json!({
            "name": "test",
            "version": "1.0.0",
            "nodes": {
                "drivers": [
                    {
                        "name": "camera",
                        "path": "./camera1"
                    }
                ],
                "custom": [
                    {
                        "name": "camera",
                        "path": "./camera2"
                    }
                ]
            }
        });
    }

    /// `LAB-4131`: two bare node entries for the same node under one target is a duplicate,
    /// just like before multi-instance nodes existed.
    #[test]
    fn test_duplicate_plain_node_in_target_is_rejected() {
        let config = serde_json::json!({
            "name": "test-robot",
            "version": "1.0.0",
            "targets": {
                "robot": { "nodes": ["@mecha10/camera", "@mecha10/camera"] }
            }
        });

        let err = validate_project_json(&config).unwrap_err();
        assert!(err.to_string().contains("@mecha10/camera"));
    }

    /// Two keyed entries for the same node *and* the same `instance_id` is a duplicate.
    #[test]
    fn test_duplicate_keyed_instance_in_target_is_rejected() {
        let config = serde_json::json!({
            "name": "test-robot",
            "version": "1.0.0",
            "targets": {
                "robot": {
                    "nodes": [
                        { "name": "@mecha10/camera", "instance_id": "real" },
                        { "name": "@mecha10/camera", "instance_id": "real" }
                    ]
                }
            }
        });

        let err = validate_project_json(&config).unwrap_err();
        assert!(err.to_string().contains("real"));
    }

    /// Two keyed entries for the same node with *different* `instance_id`s is the whole point
    /// of multi-instance nodes, and must validate cleanly.
    #[test]
    fn test_distinct_keyed_instances_in_target_are_allowed() {
        let config = serde_json::json!({
            "name": "test-robot",
            "version": "1.0.0",
            "targets": {
                "robot": {
                    "nodes": [
                        { "name": "@mecha10/camera", "instance_id": "real" },
                        { "name": "@mecha10/camera", "instance_id": "sim" }
                    ]
                }
            }
        });

        assert!(validate_project_json(&config).is_ok());
    }

    /// LAB-4066: `validate_node_config_json` no longer validates against a generic envelope --
    /// every node (built-in or custom) supplies its own schema, so these tests declare one
    /// explicitly (the shape the old generic envelope used to check unconditionally).
    fn node_config_schema() -> serde_json::Value {
        serde_json::json!({
            "type": "object",
            "required": ["id", "executable"],
            "properties": {
                "id": {"type": "string"},
                "executable": {"type": "string"},
                "mode": {"type": "string", "enum": ["local", "remote", "simulation"]},
                "auto_restart": {"type": "boolean"},
                "health_check_interval_s": {"type": "integer", "minimum": 1, "maximum": 300}
            }
        })
    }

    #[test]
    fn test_node_config_validation() {
        let config = serde_json::json!({
            "id": "test_node",
            "executable": "/usr/bin/test",
            "mode": "local",
            "auto_restart": true,
            "health_check_interval_s": 5
        });

        assert!(validate_node_config_json(&config, &node_config_schema()).is_ok());
    }

    #[test]
    fn test_node_config_invalid_mode() {
        let config = serde_json::json!({
            "id": "test_node",
            "executable": "/usr/bin/test",
            "mode": "invalid_mode"
        });

        assert!(validate_node_config_json(&config, &node_config_schema()).is_err());
    }
}