jellyflow-core 0.2.0

Headless graph model, IDs, type descriptors, and interaction policy primitives for Jellyflow.
Documentation
use super::*;

#[test]
fn graph_without_bindings_deserializes_with_empty_binding_map() {
    let graph_id = GraphId::from_u128(1);
    let json = serde_json::json!({
        "graph_id": graph_id,
        "graph_version": 1,
        "symbols": {},
        "nodes": {},
        "ports": {},
        "edges": {}
    });

    let graph: Graph = serde_json::from_value(json).expect("old graph shape must deserialize");

    assert!(graph.bindings.is_empty());
}

#[test]
fn graph_preserves_graph_local_and_source_binding_endpoints() {
    let mut graph = Graph::default();
    let node_id = NodeId::new();
    graph.nodes.insert(node_id, make_node("core.note"));

    let binding_id = BindingId::new();
    graph.bindings.insert(
        binding_id,
        Binding::node_to_source(
            node_id,
            "source.pdf",
            serde_json::json!({ "page": 3, "rect": [10, 20, 30, 40] }),
        )
        .with_kind("excerpt")
        .with_meta(serde_json::json!({ "color": "yellow" })),
    );

    let encoded = serde_json::to_value(&graph).expect("serialize graph with binding");
    let decoded: Graph = serde_json::from_value(encoded).expect("deserialize graph with binding");

    assert_eq!(decoded.bindings, graph.bindings);
    assert_eq!(
        decoded.nodes.get(&node_id).and_then(|node| node.origin),
        None
    );
}

#[test]
fn binding_helpers_build_common_node_to_source_relationships() {
    let node_id = NodeId::from_u128(5);
    let binding = Binding::node_to_source(
        node_id,
        "paper.pdf",
        serde_json::json!({ "page": 3, "quote": "headless" }),
    )
    .with_kind("excerpt")
    .with_meta(serde_json::json!({ "color": "yellow" }));

    assert_eq!(binding.subject, BindingEndpoint::node(node_id));
    assert_eq!(
        binding.subject.graph_local_target(),
        Some(GraphLocalBindingTarget::node(node_id))
    );
    assert_eq!(
        binding.target,
        BindingEndpoint::source_payload(
            "paper.pdf",
            serde_json::json!({ "page": 3, "quote": "headless" })
        )
    );
    assert_eq!(binding.kind.as_deref(), Some("excerpt"));
    assert_eq!(binding.meta, serde_json::json!({ "color": "yellow" }));
}

#[test]
fn validate_rejects_missing_graph_local_binding_targets() {
    let mut graph = Graph::default();
    let node_id = NodeId::new();
    let binding_id = BindingId::new();
    graph.bindings.insert(
        binding_id,
        Binding {
            subject: BindingEndpoint::graph_local(GraphLocalBindingTarget::Node { id: node_id }),
            target: BindingEndpoint::source(SourceAnchor::new(
                "source.pdf",
                serde_json::json!({ "page": 1 }),
            )),
            kind: None,
            meta: serde_json::Value::Null,
        },
    );

    let report = validate_graph(&graph);

    assert!(report.errors.iter().any(|error| matches!(
        error,
        GraphValidationError::BindingTargetMissing { binding, target }
            if *binding == binding_id
                && *target == GraphLocalBindingTarget::Node { id: node_id }
    )));

    graph.nodes.insert(node_id, make_node("core.note"));
    assert!(validate_graph(&graph).is_ok());
}

#[test]
fn validate_accepts_opaque_source_anchor_without_external_schema() {
    let mut graph = Graph::default();
    let binding_id = BindingId::new();
    graph.bindings.insert(
        binding_id,
        Binding {
            subject: BindingEndpoint::graph_local(GraphLocalBindingTarget::Graph),
            target: BindingEndpoint::source(SourceAnchor::new(
                "host-owned-image",
                serde_json::json!({ "region": { "x": 1, "y": 2 } }),
            )),
            kind: None,
            meta: serde_json::Value::Null,
        },
    );

    assert!(validate_graph(&graph).is_ok());
}