jellyflow-core 0.2.0

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

#[test]
fn graph_diff_roundtrips_when_deleting_a_port_with_incident_edges() {
    let mut from = Graph::default();
    let ids = insert_connected_pair_with_ids(
        &mut from,
        ConnectedPairIds {
            out: PortId::from_u128(30),
            inn: PortId::from_u128(31),
            edge: EdgeId::from_u128(789),
            ..ConnectedPairIds::new()
        },
    );

    let mut to = from.clone();
    to.nodes
        .get_mut(&ids.a)
        .unwrap()
        .ports
        .retain(|p| *p != ids.out);
    to.ports.remove(&ids.out);
    to.edges.remove(&ids.edge);

    let tx = graph_diff(&from, &to);
    assert!(
        tx.ops()
            .iter()
            .any(|op| matches!(op, GraphOp::RemovePort { id, .. } if *id == ids.out)),
        "diff must use reversible RemovePort for port deletion"
    );
    assert!(
        !tx.ops()
            .iter()
            .any(|op| matches!(op, GraphOp::RemoveEdge { id, .. } if *id == ids.edge)),
        "diff must not double-remove edges that are already removed by RemovePort"
    );

    let mut patched = from.clone();
    apply_transaction(&mut patched, &tx).expect("apply diff");
    assert_eq!(
        serde_json::to_value(&patched).unwrap(),
        serde_json::to_value(&to).unwrap(),
        "diff must roundtrip"
    );
}

#[test]
fn graph_diff_roundtrips_when_port_deletion_moves_incident_edge() {
    let mut from = Graph::default();
    let a = NodeId::new();
    let b = NodeId::new();
    insert_node(&mut from, a, "core.a");
    insert_node(&mut from, b, "core.b");

    let out1 = PortId::from_u128(32);
    let out2 = PortId::from_u128(33);
    let inn = PortId::from_u128(34);
    insert_port(&mut from, out1, a, "out1", PortDirection::Out);
    insert_port(&mut from, out2, a, "out2", PortDirection::Out);
    insert_port(&mut from, inn, b, "in", PortDirection::In);

    let edge_id = EdgeId::from_u128(790);
    from.edges.insert(edge_id, make_edge(out1, inn));

    let mut to = from.clone();
    to.nodes.get_mut(&a).unwrap().ports.retain(|p| *p != out1);
    to.ports.remove(&out1);
    to.edges.get_mut(&edge_id).unwrap().from = out2;

    let tx = graph_diff(&from, &to);
    assert!(
        tx.ops()
            .iter()
            .any(|op| matches!(op, GraphOp::AddEdge { id, .. } if *id == edge_id)),
        "diff must re-add a moved edge after its old endpoint is deleted"
    );
    assert!(
        !tx.ops()
            .iter()
            .any(|op| matches!(op, GraphOp::SetEdgeEndpoints { id, .. } if *id == edge_id)),
        "diff must not patch endpoints on an edge removed by a prior cascade"
    );

    let mut patched = from.clone();
    apply_transaction(&mut patched, &tx).expect("apply diff");
    assert_eq!(
        serde_json::to_value(&patched).unwrap(),
        serde_json::to_value(&to).unwrap(),
        "diff must roundtrip"
    );

    let inverse = invert_transaction(&tx);
    apply_transaction(&mut patched, &inverse).expect("apply inverse");
    assert_eq!(
        serde_json::to_value(&patched).unwrap(),
        serde_json::to_value(&from).unwrap(),
        "diff inverse must restore the source graph"
    );
}

#[test]
fn graph_diff_roundtrips_when_a_port_changes_structurally() {
    let mut from = Graph::default();
    let ids = insert_connected_pair_with_ids(
        &mut from,
        ConnectedPairIds {
            out: PortId::from_u128(40),
            inn: PortId::from_u128(41),
            edge: EdgeId::from_u128(1010),
            ..ConnectedPairIds::new()
        },
    );

    let mut to = from.clone();
    to.ports.get_mut(&ids.out).unwrap().key = PortKey::new("out2");

    let tx = graph_diff(&from, &to);
    assert!(
        tx.ops()
            .iter()
            .any(|op| matches!(op, GraphOp::RemovePort { id, .. } if *id == ids.out)),
        "diff must represent structural port changes as remove+add"
    );
    assert!(
        tx.ops()
            .iter()
            .any(|op| matches!(op, GraphOp::AddPort { id, .. } if *id == ids.out)),
        "diff must represent structural port changes as remove+add"
    );
    assert!(
        tx.ops()
            .iter()
            .any(|op| matches!(op, GraphOp::SetNodePorts { id, .. } if *id == ids.a)),
        "diff must restore node port ordering after remove+add"
    );
    assert!(
        tx.ops()
            .iter()
            .any(|op| matches!(op, GraphOp::AddEdge { id, .. } if *id == ids.edge)),
        "diff must re-add incident edges removed by RemovePort when they still exist in 'to'"
    );
    assert!(
        !tx.ops()
            .iter()
            .any(|op| matches!(op, GraphOp::RemoveEdge { id, .. } if *id == ids.edge)),
        "diff must not double-remove edges that are removed by RemovePort"
    );

    let mut patched = from.clone();
    apply_transaction(&mut patched, &tx).expect("apply diff");
    assert_eq!(
        serde_json::to_value(&patched).unwrap(),
        serde_json::to_value(&to).unwrap(),
        "diff must roundtrip"
    );

    let inverse = invert_transaction(&tx);
    apply_transaction(&mut patched, &inverse).expect("apply inverse");
    assert_eq!(
        serde_json::to_value(&patched).unwrap(),
        serde_json::to_value(&from).unwrap(),
        "diff inverse must restore the source graph"
    );
}

#[test]
fn graph_diff_inverse_roundtrips_when_structural_port_replacement_moves_edge() {
    let mut from = Graph::default();
    let a = NodeId::new();
    let b = NodeId::new();
    insert_node(&mut from, a, "core.a");
    insert_node(&mut from, b, "core.b");

    let out1 = PortId::from_u128(60);
    let out2 = PortId::from_u128(61);
    let inn = PortId::from_u128(62);
    insert_port(&mut from, out1, a, "out1", PortDirection::Out);
    insert_port(&mut from, out2, a, "out2", PortDirection::Out);
    insert_port(&mut from, inn, b, "in", PortDirection::In);

    let edge_id = EdgeId::from_u128(3030);
    from.edges.insert(edge_id, make_edge(out1, inn));

    let mut to = from.clone();
    to.ports.get_mut(&out1).unwrap().key = PortKey::new("out1-renamed");
    to.edges.get_mut(&edge_id).unwrap().from = out2;

    let tx = graph_diff(&from, &to);
    assert!(
        tx.ops()
            .iter()
            .any(|op| matches!(op, GraphOp::AddEdge { id, .. } if *id == edge_id)),
        "diff must re-add the cascaded edge from the target graph"
    );
    assert!(
        !tx.ops()
            .iter()
            .any(|op| matches!(op, GraphOp::SetEdgeEndpoints { id, .. } if *id == edge_id)),
        "diff must not emit endpoint setters for an edge already restored from the target graph"
    );

    let mut patched = from.clone();
    apply_transaction(&mut patched, &tx).expect("apply diff");
    assert_eq!(
        serde_json::to_value(&patched).unwrap(),
        serde_json::to_value(&to).unwrap(),
        "diff must roundtrip"
    );

    let inverse = invert_transaction(&tx);
    apply_transaction(&mut patched, &inverse).expect("apply inverse");
    assert_eq!(
        serde_json::to_value(&patched).unwrap(),
        serde_json::to_value(&from).unwrap(),
        "diff inverse must restore the source graph"
    );
}