jellyflow-runtime 0.1.0

Headless store, rules, schema, profile, and change pipeline for Jellyflow.
Documentation
use super::super::fixtures::{make_graph, make_store};

use crate::runtime::middleware::NodeGraphStoreMiddleware;
use jellyflow_core::core::CanvasPoint;
use jellyflow_core::ops::{GraphOp, GraphTransaction};

#[test]
fn store_middleware_can_rewrite_transactions() {
    #[derive(Debug, Default)]
    struct DropOps;

    impl NodeGraphStoreMiddleware for DropOps {
        fn before_dispatch(
            &mut self,
            _snapshot: crate::runtime::events::NodeGraphStoreSnapshot<'_>,
            tx: &mut GraphTransaction,
        ) -> Result<(), crate::profile::ApplyPipelineError> {
            tx.clear_ops();
            Ok(())
        }
    }

    let (g0, a, _b, _out_port, _in_port, _eid) = make_graph();
    let mut store = make_store(g0).with_middleware(DropOps);

    let tx = GraphTransaction::from_ops([GraphOp::SetNodePos {
        id: a,
        from: CanvasPoint { x: 0.0, y: 0.0 },
        to: CanvasPoint { x: 10.0, y: 20.0 },
    }]);

    let outcome = store.dispatch_transaction(&tx).expect("dispatch");
    assert!(outcome.patch.ops().is_empty());
    assert_eq!(
        store.graph().nodes.get(&a).unwrap().pos,
        CanvasPoint { x: 0.0, y: 0.0 }
    );
    assert!(!store.can_undo());
}

#[test]
fn store_middleware_can_reject_transactions() {
    use crate::rules::{Diagnostic, DiagnosticSeverity, DiagnosticTarget};

    #[derive(Debug, Default)]
    struct RejectAll;

    impl NodeGraphStoreMiddleware for RejectAll {
        fn before_dispatch(
            &mut self,
            _snapshot: crate::runtime::events::NodeGraphStoreSnapshot<'_>,
            _tx: &mut GraphTransaction,
        ) -> Result<(), crate::profile::ApplyPipelineError> {
            Err(crate::profile::ApplyPipelineError::Rejected {
                message: "rejected by middleware".to_string(),
                diagnostics: vec![Diagnostic {
                    key: "test.middleware.reject".to_string(),
                    severity: DiagnosticSeverity::Error,
                    target: DiagnosticTarget::Graph,
                    message: "rejected by middleware".to_string(),
                    fixes: Vec::new(),
                }],
            })
        }
    }

    let (g0, a, _b, _out_port, _in_port, _eid) = make_graph();
    let mut store = make_store(g0.clone()).with_middleware(RejectAll);

    let tx = GraphTransaction::from_ops([GraphOp::SetNodePos {
        id: a,
        from: CanvasPoint { x: 0.0, y: 0.0 },
        to: CanvasPoint { x: 10.0, y: 20.0 },
    }]);

    let err = store.dispatch_transaction(&tx).expect_err("reject");
    let crate::runtime::store::DispatchError::Apply(crate::profile::ApplyPipelineError::Rejected {
        diagnostics,
        ..
    }) = err
    else {
        panic!("unexpected error: {err:?}");
    };
    assert_eq!(diagnostics[0].key, "test.middleware.reject");
    assert_eq!(
        store.graph().nodes.get(&a).unwrap().pos,
        g0.nodes.get(&a).unwrap().pos
    );
    assert!(!store.can_undo());
}

#[test]
fn store_middleware_after_dispatch_observes_undo_and_redo() {
    use std::cell::{Cell, RefCell};
    use std::rc::Rc;

    #[derive(Debug)]
    struct TraceAfterDispatch {
        before_calls: Rc<Cell<usize>>,
        after_calls: Rc<RefCell<Vec<(bool, bool, usize)>>>,
    }

    impl NodeGraphStoreMiddleware for TraceAfterDispatch {
        fn before_dispatch(
            &mut self,
            _snapshot: crate::runtime::events::NodeGraphStoreSnapshot<'_>,
            _tx: &mut GraphTransaction,
        ) -> Result<(), crate::profile::ApplyPipelineError> {
            self.before_calls.set(self.before_calls.get() + 1);
            Ok(())
        }

        fn after_dispatch(
            &mut self,
            snapshot: crate::runtime::events::NodeGraphStoreSnapshot<'_>,
            patch: &crate::runtime::commit::NodeGraphPatch,
        ) {
            self.after_calls.borrow_mut().push((
                snapshot.history.can_undo(),
                snapshot.history.can_redo(),
                patch.ops().len(),
            ));
        }
    }

    let (g0, a, _b, _out_port, _in_port, _eid) = make_graph();
    let before_calls = Rc::new(Cell::new(0));
    let after_calls = Rc::new(RefCell::new(Vec::new()));
    let mut store = make_store(g0).with_middleware(TraceAfterDispatch {
        before_calls: before_calls.clone(),
        after_calls: after_calls.clone(),
    });

    let tx = GraphTransaction::from_ops([GraphOp::SetNodePos {
        id: a,
        from: CanvasPoint { x: 0.0, y: 0.0 },
        to: CanvasPoint { x: 10.0, y: 20.0 },
    }]);

    store.dispatch_transaction(&tx).expect("dispatch");
    store.undo().expect("undo").expect("undo outcome");
    store.redo().expect("redo").expect("redo outcome");

    assert_eq!(before_calls.get(), 1);
    assert_eq!(
        &*after_calls.borrow(),
        &[(true, false, 1), (false, true, 1), (true, false, 1)]
    );
}