enact-core 0.0.2

Core agent runtime for Enact - Graph-Native AI agents
Documentation
//! Graph execution tests

use enact_core::graph::{EdgeTarget, NodeState, StateGraph};

#[tokio::test]
async fn test_simple_two_node_graph() {
    // Build a simple A -> B -> END graph
    let graph = StateGraph::new()
        .add_node("uppercase", |state: NodeState| async move {
            let input = state.as_str().unwrap_or_default();
            Ok(NodeState::from_string(&input.to_uppercase()))
        })
        .add_node("add_exclaim", |state: NodeState| async move {
            let input = state.as_str().unwrap_or_default();
            Ok(NodeState::from_string(&format!("{}!", input)))
        })
        .set_entry_point("uppercase")
        .add_edge("uppercase", "add_exclaim")
        .add_edge_to_end("add_exclaim")
        .compile()
        .expect("Failed to compile graph");

    // Run the graph
    let result = graph.run("hello world").await.expect("Failed to run graph");

    assert_eq!(result.as_str(), Some("HELLO WORLD!"));
}

#[tokio::test]
async fn test_conditional_routing() {
    // Build a graph with conditional routing
    let graph = StateGraph::new()
        .add_node("classify", |state: NodeState| async move {
            let input = state.as_str().unwrap_or_default();
            if input.contains("error") {
                Ok(NodeState::from_string("error"))
            } else {
                Ok(NodeState::from_string("success"))
            }
        })
        .add_node("handle_error", |_state: NodeState| async move {
            Ok(NodeState::from_string("Error handled"))
        })
        .add_node("handle_success", |_state: NodeState| async move {
            Ok(NodeState::from_string("Success!"))
        })
        .set_entry_point("classify")
        .add_conditional_edge("classify", |output: &str| match output {
            "error" => EdgeTarget::node("handle_error"),
            _ => EdgeTarget::node("handle_success"),
        })
        .add_edge_to_end("handle_error")
        .add_edge_to_end("handle_success")
        .compile()
        .expect("Failed to compile graph");

    // Test success path
    let result = graph.run("hello").await.expect("Failed to run");
    assert_eq!(result.as_str(), Some("Success!"));

    // Test error path
    let result = graph.run("this has an error").await.expect("Failed to run");
    assert_eq!(result.as_str(), Some("Error handled"));
}

#[tokio::test]
async fn test_graph_validation() {
    // Empty graph should fail
    let result = StateGraph::new().compile();
    assert!(result.is_err());

    // Missing entry point should use first node
    let graph = StateGraph::new()
        .add_node("a", |state: NodeState| async move { Ok(state) })
        .compile();
    assert!(graph.is_ok());
}

/// Test that circular graphs are detected and rejected (DAG invariant)
#[tokio::test]
async fn test_cycle_detection() {
    // Direct self-loop: A -> A
    let result = StateGraph::new()
        .add_node("a", |state: NodeState| async move { Ok(state) })
        .set_entry_point("a")
        .add_edge("a", "a")
        .compile();

    assert!(result.is_err());
    let err = result.err().unwrap().to_string();
    assert!(err.contains("cycle"), "Expected cycle error, got: {}", err);
}

/// Test that two-node cycle is detected: A -> B -> A
#[tokio::test]
async fn test_two_node_cycle_detection() {
    let result = StateGraph::new()
        .add_node("a", |state: NodeState| async move { Ok(state) })
        .add_node("b", |state: NodeState| async move { Ok(state) })
        .set_entry_point("a")
        .add_edge("a", "b")
        .add_edge("b", "a")
        .compile();

    assert!(result.is_err());
    let err = result.err().unwrap().to_string();
    assert!(err.contains("cycle"), "Expected cycle error, got: {}", err);
}

/// Test that valid DAGs compile successfully
#[tokio::test]
async fn test_valid_dag_compiles() {
    // Diamond DAG: A -> B, A -> C, B -> D, C -> D
    let result = StateGraph::new()
        .add_node("a", |state: NodeState| async move { Ok(state) })
        .add_node("b", |state: NodeState| async move { Ok(state) })
        .add_node("c", |state: NodeState| async move { Ok(state) })
        .add_node("d", |state: NodeState| async move { Ok(state) })
        .set_entry_point("a")
        .add_edge("a", "b")
        .add_edge("a", "c")
        .add_edge("b", "d")
        .add_edge("c", "d")
        .add_edge_to_end("d")
        .compile();

    assert!(
        result.is_ok(),
        "Valid DAG should compile: {:?}",
        result.err()
    );
}