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_node_with_ports_and_edges() {
    let mut from = Graph::default();
    let ids = insert_connected_pair_with_ids(
        &mut from,
        ConnectedPairIds {
            out: PortId::from_u128(20),
            inn: PortId::from_u128(21),
            edge: EdgeId::from_u128(456),
            ..ConnectedPairIds::new()
        },
    );

    let mut to = from.clone();
    to.nodes.remove(&ids.b);
    to.ports.remove(&ids.inn);
    to.edges.remove(&ids.edge);

    let tx = graph_diff(&from, &to);
    assert!(
        tx.ops()
            .iter()
            .any(|op| matches!(op, GraphOp::RemoveNode { id, .. } if *id == ids.b)),
        "diff must use reversible RemoveNode for node deletion"
    );
    assert!(
        !tx.ops()
            .iter()
            .any(|op| matches!(op, GraphOp::RemovePort { id, .. } if *id == ids.inn)),
        "diff must not double-remove ports that are already removed by RemoveNode"
    );
    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 RemoveNode"
    );

    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_deleting_a_group_with_child_nodes() {
    let mut from = Graph::default();
    let group_id = GroupId::from_u128(10);
    from.groups.insert(
        group_id,
        Group {
            title: "Group".to_string(),
            rect: CanvasRect {
                origin: CanvasPoint { x: 0.0, y: 0.0 },
                size: CanvasSize {
                    width: 100.0,
                    height: 100.0,
                },
            },
            color: None,
        },
    );

    let child_a = NodeId::from_u128(200);
    let child_b = NodeId::from_u128(100);
    let mut node_a = make_node("core.a");
    node_a.parent = Some(group_id);
    from.nodes.insert(child_a, node_a);
    let mut node_b = make_node("core.b");
    node_b.parent = Some(group_id);
    from.nodes.insert(child_b, node_b);

    from.nodes
        .insert(NodeId::from_u128(300), make_node("core.c"));

    let mut to = from.clone();
    to.groups.remove(&group_id);
    to.nodes.get_mut(&child_a).unwrap().parent = None;
    to.nodes.get_mut(&child_b).unwrap().parent = None;

    let tx = graph_diff(&from, &to);
    let expected_detached = vec![(child_b, Some(group_id)), (child_a, Some(group_id))];
    assert!(
        tx.ops().iter().any(|op| match op {
            GraphOp::RemoveGroup { id, detached, .. } if *id == group_id => {
                detached == &expected_detached
            }
            _ => false,
        }),
        "diff must use reversible RemoveGroup and detach child nodes deterministically"
    );
    assert!(
        !tx.ops().iter().any(|op| matches!(
            op,
            GraphOp::SetNodeParent { id, .. } if *id == child_a || *id == child_b
        )),
        "diff must not emit SetNodeParent when the parent group is being removed"
    );

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