use async_trait::async_trait;
use mlua_flow_ir::{
eval_async_with_storage, AsyncDispatcher, CtxStorage, EvalError, Expr, MemoryCtx, Node,
};
use serde_json::{json, Value};
use std::sync::{Arc, Mutex};
use tokio::sync::oneshot;
struct ChannelDispatcher {
ready_tx: Mutex<Option<oneshot::Sender<()>>>,
release_rx: Mutex<Option<oneshot::Receiver<()>>>,
}
#[async_trait]
impl AsyncDispatcher for ChannelDispatcher {
async fn dispatch(&self, ref_: &str, input: Value) -> Result<Value, EvalError> {
match ref_ {
"wait_for_signal" => {
let ready = self
.ready_tx
.lock()
.unwrap()
.take()
.expect("wait_for_signal called twice");
ready.send(()).expect("ready receiver dropped");
let release = self
.release_rx
.lock()
.unwrap()
.take()
.expect("release receiver missing");
release.await.expect("release sender dropped");
Ok(json!({"signaled": true, "input": input}))
}
"echo" => Ok(input),
other => Err(EvalError::DispatcherError {
ref_: other.into(),
msg: format!("unknown ref: {other}"),
}),
}
}
}
#[tokio::test]
async fn external_task_injects_state_during_step_await() {
let storage: Arc<dyn CtxStorage> = MemoryCtx::shared(json!({ "seed": "start" }));
let (ready_tx, ready_rx) = oneshot::channel::<()>();
let (release_tx, release_rx) = oneshot::channel::<()>();
let dispatcher = ChannelDispatcher {
ready_tx: Mutex::new(Some(ready_tx)),
release_rx: Mutex::new(Some(release_rx)),
};
let flow = Node::Seq {
children: vec![
Node::Step {
ref_: "wait_for_signal".into(),
in_: Expr::Path {
at: "$.seed".into(),
},
out: Expr::Path {
at: "$.signal_received".into(),
},
},
Node::Step {
ref_: "echo".into(),
in_: Expr::Path {
at: "$.injected".into(),
},
out: Expr::Path {
at: "$.echoed".into(),
},
},
],
};
let storage_for_external = storage.clone();
let mutator = tokio::spawn(async move {
ready_rx.await.expect("ready not received");
storage_for_external
.write("$.injected", json!(42))
.expect("ctx.write failed");
release_tx.send(()).expect("release send failed");
});
eval_async_with_storage(&flow, storage.clone(), &dispatcher)
.await
.expect("eval_async_with_storage failed");
mutator.await.expect("mutator task panicked");
let final_ctx = storage.snapshot();
assert_eq!(
final_ctx.pointer("/signal_received/signaled"),
Some(&json!(true)),
"Step 1 dispatch result not written: {final_ctx:#?}"
);
assert_eq!(
final_ctx.pointer("/echoed"),
Some(&json!(42)),
"External write during suspend was NOT observed by next Step: {final_ctx:#?}"
);
assert_eq!(final_ctx.pointer("/injected"), Some(&json!(42)));
}
#[tokio::test]
async fn external_task_constructs_expr_at_runtime_and_writes() {
use mlua_flow_ir::eval_expr;
let storage: Arc<dyn CtxStorage> = MemoryCtx::shared(json!({ "base": 100, "multiplier": 3 }));
let (ready_tx, ready_rx) = oneshot::channel::<()>();
let (release_tx, release_rx) = oneshot::channel::<()>();
let dispatcher = ChannelDispatcher {
ready_tx: Mutex::new(Some(ready_tx)),
release_rx: Mutex::new(Some(release_rx)),
};
let flow = Node::Step {
ref_: "wait_for_signal".into(),
in_: Expr::Path {
at: "$.base".into(),
},
out: Expr::Path {
at: "$.done".into(),
},
};
let storage_for_external = storage.clone();
let mutator = tokio::spawn(async move {
ready_rx.await.expect("ready not received");
let dynamic_expr = Expr::Path {
at: "$.base".into(),
};
let snap = storage_for_external.snapshot();
let resolved = eval_expr(&dynamic_expr, &snap).expect("runtime eval_expr failed");
storage_for_external
.write("$.runtime_injected", resolved)
.expect("ctx.write failed");
release_tx.send(()).expect("release send failed");
});
eval_async_with_storage(&flow, storage.clone(), &dispatcher)
.await
.expect("eval failed");
mutator.await.expect("mutator panicked");
let final_ctx = storage.snapshot();
assert_eq!(
final_ctx.pointer("/runtime_injected"),
Some(&json!(100)),
"runtime-constructed Expr eval result not written: {final_ctx:#?}"
);
}
#[tokio::test]
async fn assign_node_writes_value_inline() {
struct EchoDispatcher;
#[async_trait]
impl AsyncDispatcher for EchoDispatcher {
async fn dispatch(&self, _ref_: &str, input: Value) -> Result<Value, EvalError> {
Ok(input)
}
}
let storage = MemoryCtx::shared(json!({ "x": 1 }));
let flow = Node::Seq {
children: vec![
Node::Assign {
at: Expr::Path { at: "$.y".into() },
value: Expr::Lit { value: json!(42) },
},
Node::Step {
ref_: "echo".into(),
in_: Expr::Path { at: "$.y".into() },
out: Expr::Path { at: "$.z".into() },
},
],
};
eval_async_with_storage(&flow, storage.clone(), &EchoDispatcher)
.await
.expect("eval failed");
let final_ctx = storage.snapshot();
assert_eq!(final_ctx.pointer("/y"), Some(&json!(42)));
assert_eq!(final_ctx.pointer("/z"), Some(&json!(42)));
}