jellyflow-runtime 0.1.0

Headless store, rules, schema, profile, and change pipeline for Jellyflow.
Documentation
use super::super::fixtures::make_graph;
use super::super::harness::{HarnessCallbackEvent, HarnessEvent, InteractionHarness};
use super::support::{assert_conformance_trace, insert_input_port};

use crate::io::NodeGraphViewState;
use crate::rules::EdgeEndpoint;
use crate::runtime::conformance::{
    ConformanceAction, ConformanceCallbackEvent, ConformanceScenario, ConformanceTraceEvent,
    ConformanceViewChange,
};
use crate::runtime::connection::{
    CONNECT_EDGE_TRANSACTION_LABEL, ConnectEdgeRequest, RECONNECT_EDGE_TRANSACTION_LABEL,
    ReconnectEdgeRequest,
};
use crate::runtime::delete::DELETE_SELECTION_TRANSACTION_LABEL;
use crate::runtime::xyflow::callbacks::{ConnectionChange, EdgeConnection};
use crate::runtime::xyflow::changes::{EdgeChange, NodeChange, NodeGraphChanges};
use jellyflow_core::core::{EdgeId, EdgeKind};
use jellyflow_core::interaction::NodeGraphConnectionMode;
use jellyflow_core::ops::EdgeEndpoints;

#[test]
fn adapter_conformance_connect_dispatches_patch_and_xyflow_projection() {
    let (mut graph, _a, b, out_port, _in_port, _eid) = make_graph();
    let next_in = insert_input_port(&mut graph, b, "in2");
    let edge_id = EdgeId::from_u128(301);

    let connection = EdgeConnection::new(edge_id, out_port, next_in, EdgeKind::Data);
    let scenario = ConformanceScenario::new("connect dispatches patch and projection", graph)
        .with_xyflow_callbacks()
        .with_actions([ConformanceAction::apply_connect_edge(
            ConnectEdgeRequest::new(out_port, next_in, NodeGraphConnectionMode::Strict)
                .with_edge_id(edge_id),
        )])
        .with_expected_trace([
            ConformanceTraceEvent::graph_commit(Some(CONNECT_EDGE_TRANSACTION_LABEL), ["add_edge"]),
            ConformanceTraceEvent::callback(ConformanceCallbackEvent::GraphCommit {
                label: Some(CONNECT_EDGE_TRANSACTION_LABEL.to_owned()),
            }),
            ConformanceTraceEvent::callback(ConformanceCallbackEvent::NodeEdgeChanges {
                nodes: 0,
                edges: 1,
            }),
            ConformanceTraceEvent::callback(ConformanceCallbackEvent::EdgesChange { count: 1 }),
            ConformanceTraceEvent::callback(ConformanceCallbackEvent::ConnectionChange(
                ConnectionChange::Connected(connection),
            )),
            ConformanceTraceEvent::callback(ConformanceCallbackEvent::Connect(connection)),
        ]);

    assert_conformance_trace(&scenario);
}

#[test]
fn adapter_conformance_reconnect_preserves_edge_id_and_projects_endpoint_change() {
    let (mut graph, _a, b, out_port, in_port, edge_id) = make_graph();
    let next_in = insert_input_port(&mut graph, b, "in2");
    let mut harness = InteractionHarness::new("reconnect preserves edge id", graph);
    let from = EdgeEndpoints {
        from: out_port,
        to: in_port,
    };
    let to = EdgeEndpoints {
        from: out_port,
        to: next_in,
    };
    let _callback_token = harness.install_callback_trace();

    let outcome = harness
        .store_mut()
        .apply_reconnect_edge(ReconnectEdgeRequest::new(
            edge_id,
            EdgeEndpoint::To,
            next_in,
            NodeGraphConnectionMode::Strict,
        ))
        .expect("dispatch reconnect")
        .expect("reconnect should commit");
    let changes = NodeGraphChanges::from_patch(&outcome.patch);

    let edge = harness
        .store()
        .graph()
        .edges
        .get(&edge_id)
        .expect("edge remains");
    assert_eq!(edge.from, out_port);
    assert_eq!(edge.to, next_in);
    assert!(
        matches!(changes.edges(), [EdgeChange::Endpoints { id, from, to }]
            if *id == edge_id && *from == out_port && *to == next_in),
        "reconnect should project to one endpoint change",
    );
    harness.assert_events(&[
        HarnessEvent::graph_commit(
            Some(RECONNECT_EDGE_TRANSACTION_LABEL),
            ["set_edge_endpoints"],
        ),
        HarnessEvent::callback(HarnessCallbackEvent::GraphCommit {
            label: Some(RECONNECT_EDGE_TRANSACTION_LABEL.to_owned()),
        }),
        HarnessEvent::callback(HarnessCallbackEvent::NodeEdgeChanges { nodes: 0, edges: 1 }),
        HarnessEvent::callback(HarnessCallbackEvent::EdgesChange { count: 1 }),
        HarnessEvent::callback(HarnessCallbackEvent::ConnectionChange(
            ConnectionChange::Reconnected {
                edge: edge_id,
                from,
                to,
            },
        )),
        HarnessEvent::callback(HarnessCallbackEvent::Reconnect {
            edge: edge_id,
            from,
            to,
        }),
    ]);
}

#[test]
fn adapter_conformance_delete_node_cascades_edges_and_projects_delete_payload() {
    let (graph, node_id, _b, out_port, in_port, edge_id) = make_graph();
    let mut view_state = NodeGraphViewState::default();
    view_state.set_selection(vec![node_id], Vec::new(), Vec::new());
    let mut harness =
        InteractionHarness::with_view_state("delete node cascades edges", graph, view_state);
    let disconnected = EdgeConnection::new(edge_id, out_port, in_port, EdgeKind::Data);
    let _callback_token = harness.install_callback_trace();

    let outcome = harness
        .store_mut()
        .apply_delete_selection()
        .expect("dispatch delete node")
        .expect("delete should commit");
    let changes = NodeGraphChanges::from_patch(&outcome.patch);

    assert!(!harness.store().graph().nodes.contains_key(&node_id));
    assert!(!harness.store().graph().edges.contains_key(&edge_id));
    assert!(
        matches!(changes.nodes(), [NodeChange::Remove { id }] if *id == node_id),
        "delete should project to one node remove",
    );
    assert!(
        matches!(changes.edges(), [EdgeChange::Remove { id }] if *id == edge_id),
        "delete should project cascaded edge removal",
    );
    harness.assert_events(&[
        HarnessEvent::graph_commit(Some(DELETE_SELECTION_TRANSACTION_LABEL), ["remove_node"]),
        HarnessEvent::callback(HarnessCallbackEvent::GraphCommit {
            label: Some(DELETE_SELECTION_TRANSACTION_LABEL.to_owned()),
        }),
        HarnessEvent::callback(HarnessCallbackEvent::NodeEdgeChanges { nodes: 1, edges: 1 }),
        HarnessEvent::callback(HarnessCallbackEvent::NodesChange { count: 1 }),
        HarnessEvent::callback(HarnessCallbackEvent::EdgesChange { count: 1 }),
        HarnessEvent::callback(HarnessCallbackEvent::ConnectionChange(
            ConnectionChange::Disconnected(disconnected),
        )),
        HarnessEvent::callback(HarnessCallbackEvent::Disconnect(disconnected)),
        HarnessEvent::callback(HarnessCallbackEvent::NodesDelete { count: 1 }),
        HarnessEvent::callback(HarnessCallbackEvent::EdgesDelete { count: 1 }),
        HarnessEvent::callback(HarnessCallbackEvent::Delete {
            nodes: 1,
            edges: 1,
            groups: 0,
            sticky_notes: 0,
        }),
        HarnessEvent::selection(Vec::new(), Vec::new(), Vec::new()),
        HarnessEvent::callback(HarnessCallbackEvent::ViewChange {
            changes: vec![ConformanceViewChange::Selection {
                nodes: Vec::new(),
                edges: Vec::new(),
                groups: Vec::new(),
            }],
        }),
        HarnessEvent::callback(HarnessCallbackEvent::SelectionChange {
            nodes: Vec::new(),
            edges: Vec::new(),
            groups: Vec::new(),
        }),
    ]);
}