use dataflow_rs::engine::functions::{AsyncFunctionHandler, FunctionConfig};
use dataflow_rs::engine::message::Message;
use dataflow_rs::{Engine, Task, TaskContext, TaskOutcome, Workflow};
use serde_json::json;
use std::sync::Arc;
mod common;
use common::{AsyncLoggingTask, LoggingTask, dv};
#[tokio::test]
async fn test_async_task_execution() {
let task = LoggingTask;
let mut message = Message::from_value(&json!({}));
let datalogic = Arc::new(
datalogic_rs::Engine::builder()
.with_templating(true)
.build(),
);
let mut ctx = TaskContext::new(&mut message, &datalogic);
let outcome = task.execute(&mut ctx, &json!({})).await;
assert!(outcome.is_ok(), "Task execution should succeed");
assert_eq!(outcome.unwrap(), TaskOutcome::Success);
}
#[tokio::test]
async fn test_workflow_execution() {
let workflow = Workflow {
id: "test_workflow".to_string(),
name: "Test Workflow".to_string(),
priority: 0,
description: Some("A test workflow".to_string()),
tasks: vec![Task {
id: "log_task".to_string(),
id_arc: std::sync::Arc::from("log_task"),
name: "Log Task".to_string(),
description: Some("A test task".to_string()),
condition: json!(true),
compiled_condition: None,
continue_on_error: false,
function: FunctionConfig::Custom {
name: "log".to_string(),
input: json!({}),
compiled_input: None,
},
}],
condition: json!(true),
compiled_condition: None,
continue_on_error: false,
..Default::default()
};
let engine = Engine::builder()
.with_workflow(workflow)
.register("log", LoggingTask)
.build()
.unwrap();
let mut message = Message::from_value(&json!({}));
let result = engine.process_message(&mut message).await;
match &result {
Ok(_) => println!("Workflow executed successfully"),
Err(e) => println!("Workflow execution failed: {e:?}"),
}
assert!(result.is_ok(), "Workflow execution should succeed");
assert_eq!(
message.audit_trail().len(),
1,
"Message should have one audit trail entry"
);
assert_eq!(
message.audit_trail()[0].task_id.as_ref(),
"log_task",
"Audit trail should contain the executed task"
);
}
#[tokio::test]
async fn test_async_workflow_execution() {
let workflow = Workflow {
id: "async_workflow".to_string(),
name: "Async Test Workflow".to_string(),
priority: 0,
description: Some("An async test workflow".to_string()),
tasks: vec![Task {
id: "async_log_task".to_string(),
id_arc: std::sync::Arc::from("async_log_task"),
name: "Async Log Task".to_string(),
description: Some("An async test task".to_string()),
condition: json!(true),
compiled_condition: None,
continue_on_error: false,
function: FunctionConfig::Custom {
name: "async_log".to_string(),
input: json!({}),
compiled_input: None,
},
}],
condition: json!(true),
compiled_condition: None,
continue_on_error: false,
..Default::default()
};
let engine = Engine::builder()
.with_workflow(workflow)
.register("async_log", AsyncLoggingTask)
.build()
.unwrap();
let mut message = Message::from_value(&json!({}));
let result = engine.process_message(&mut message).await;
assert!(result.is_ok(), "Async workflow execution should succeed");
assert_eq!(
message.audit_trail().len(),
1,
"Message should have one audit trail entry"
);
assert_eq!(
message.audit_trail()[0].task_id.as_ref(),
"async_log_task",
"Audit trail should contain the executed async task"
);
}
#[tokio::test]
async fn log_builtin_runs_in_sync_stretch() {
let workflow_json = r#"{
"id": "log_only",
"name": "Log Only",
"tasks": [
{
"id": "log_task",
"name": "Log",
"function": {
"name": "log",
"input": {
"message": "hello"
}
}
}
]
}"#;
let workflow = Workflow::from_json(workflow_json).unwrap();
let engine = Engine::builder().with_workflow(workflow).build().unwrap();
let mut message = Message::from_value(&json!({}));
engine.process_message(&mut message).await.unwrap();
assert_eq!(message.audit_trail().len(), 1);
assert_eq!(message.audit_trail()[0].status, 200);
assert_eq!(message.audit_trail()[0].task_id.as_ref(), "log_task");
}
#[tokio::test]
async fn filter_builtin_runs_in_sync_stretch() {
let workflow_json = r#"{
"id": "filter_only",
"name": "Filter Only",
"tasks": [
{
"id": "filter_task",
"name": "Filter",
"function": {
"name": "filter",
"input": {
"condition": true,
"on_reject": "halt"
}
}
}
]
}"#;
let workflow = Workflow::from_json(workflow_json).unwrap();
let engine = Engine::builder().with_workflow(workflow).build().unwrap();
let mut message = Message::from_value(&json!({}));
engine.process_message(&mut message).await.unwrap();
assert_eq!(message.audit_trail().len(), 1);
assert_eq!(message.audit_trail()[0].status, 200);
}
#[tokio::test]
async fn filter_halt_in_sync_stretch_short_circuits_workflow() {
let workflow_json = r#"{
"id": "filter_halt",
"name": "Filter Halt",
"tasks": [
{
"id": "gate",
"name": "Gate",
"function": {
"name": "filter",
"input": {
"condition": false,
"on_reject": "halt"
}
}
},
{
"id": "after_halt",
"name": "After Halt",
"function": {
"name": "map",
"input": {
"mappings": [
{ "path": "data.should_not_run", "logic": true }
]
}
}
}
]
}"#;
let workflow = Workflow::from_json(workflow_json).unwrap();
let engine = Engine::builder().with_workflow(workflow).build().unwrap();
let mut message = Message::from_value(&json!({}));
engine.process_message(&mut message).await.unwrap();
assert_eq!(message.audit_trail().len(), 1);
assert_eq!(message.audit_trail()[0].task_id.as_ref(), "gate");
assert_eq!(
message.audit_trail()[0].status,
usize::from(dataflow_rs::HALT_STATUS_CODE)
);
assert!(message.context["data"].get("should_not_run").is_none());
}
#[tokio::test]
async fn log_filter_chained_with_map_share_one_arena() {
let workflow_json = r#"{
"id": "mixed_sync",
"name": "Mixed Sync Stretch",
"tasks": [
{
"id": "set_amount",
"name": "Set Amount",
"function": {
"name": "map",
"input": {
"mappings": [
{ "path": "data.amount", "logic": 100 }
]
}
}
},
{
"id": "gate",
"name": "Amount > 0",
"function": {
"name": "filter",
"input": {
"condition": { ">": [ { "var": "data.amount" }, 0 ] },
"on_reject": "halt"
}
}
},
{
"id": "double_amount",
"name": "Double Amount",
"function": {
"name": "map",
"input": {
"mappings": [
{
"path": "data.amount",
"logic": { "*": [ { "var": "data.amount" }, 2 ] }
}
]
}
}
},
{
"id": "log_result",
"name": "Log Result",
"function": {
"name": "log",
"input": {
"message": { "cat": [ "doubled=", { "var": "data.amount" } ] }
}
}
}
]
}"#;
let workflow = Workflow::from_json(workflow_json).unwrap();
let engine = Engine::builder().with_workflow(workflow).build().unwrap();
let mut message = Message::from_value(&json!({}));
engine.process_message(&mut message).await.unwrap();
assert_eq!(message.context["data"]["amount"], dv(json!(200)));
assert_eq!(message.audit_trail().len(), 4);
let task_ids: Vec<&str> = message
.audit_trail()
.iter()
.map(|a| a.task_id.as_ref())
.collect();
assert_eq!(
task_ids,
vec!["set_amount", "gate", "double_amount", "log_result"]
);
}
#[tokio::test]
async fn chained_fully_sync_workflows_advance_through_shared_arena() {
let wf_a = r#"{
"id": "wf_a",
"name": "A",
"priority": 0,
"condition": true,
"tasks": [{
"id": "map_a", "name": "A",
"function": { "name": "map", "input": { "mappings": [ { "path": "data.a", "logic": 1 } ] } }
}]
}"#;
let wf_b = r#"{
"id": "wf_b",
"name": "B",
"priority": 1,
"condition": { "==": [ { "var": "metadata.progress.workflow_id" }, "wf_a" ] },
"tasks": [{
"id": "map_b", "name": "B",
"function": { "name": "map", "input": { "mappings": [ { "path": "data.b", "logic": { "+": [ { "var": "data.a" }, 1 ] } } ] } }
}]
}"#;
let wf_c = r#"{
"id": "wf_c",
"name": "C",
"priority": 2,
"condition": { "==": [ { "var": "metadata.progress.workflow_id" }, "wf_b" ] },
"tasks": [{
"id": "map_c", "name": "C",
"function": { "name": "map", "input": { "mappings": [ { "path": "data.c", "logic": { "+": [ { "var": "data.b" }, 1 ] } } ] } }
}]
}"#;
let workflows = vec![
Workflow::from_json(wf_a).unwrap(),
Workflow::from_json(wf_b).unwrap(),
Workflow::from_json(wf_c).unwrap(),
];
let engine = Engine::builder().with_workflows(workflows).build().unwrap();
let mut message = Message::from_value(&json!({}));
engine.process_message(&mut message).await.unwrap();
assert_eq!(message.context["data"]["a"], dv(json!(1)));
assert_eq!(message.context["data"]["b"], dv(json!(2)));
assert_eq!(message.context["data"]["c"], dv(json!(3)));
let task_ids: Vec<&str> = message
.audit_trail()
.iter()
.map(|a| a.task_id.as_ref())
.collect();
assert_eq!(task_ids, vec!["map_a", "map_b", "map_c"]);
assert_eq!(
message.context["metadata"]["progress"]["workflow_id"],
dv(json!("wf_c"))
);
}
#[tokio::test]
async fn cross_workflow_false_condition_skips_only_that_workflow() {
let wf_x = r#"{
"id": "wf_x", "name": "X", "priority": 0, "condition": true,
"tasks": [{ "id": "map_x", "name": "X",
"function": { "name": "map", "input": { "mappings": [ { "path": "data.x", "logic": 1 } ] } } }]
}"#;
let wf_y = r#"{
"id": "wf_y", "name": "Y", "priority": 1,
"condition": { "==": [ { "var": "data.x" }, 999 ] },
"tasks": [{ "id": "map_y", "name": "Y",
"function": { "name": "map", "input": { "mappings": [ { "path": "data.y", "logic": 1 } ] } } }]
}"#;
let wf_z = r#"{
"id": "wf_z", "name": "Z", "priority": 2, "condition": true,
"tasks": [{ "id": "map_z", "name": "Z",
"function": { "name": "map", "input": { "mappings": [ { "path": "data.z", "logic": 1 } ] } } }]
}"#;
let workflows = vec![
Workflow::from_json(wf_x).unwrap(),
Workflow::from_json(wf_y).unwrap(),
Workflow::from_json(wf_z).unwrap(),
];
let engine = Engine::builder().with_workflows(workflows).build().unwrap();
let mut message = Message::from_value(&json!({}));
engine.process_message(&mut message).await.unwrap();
assert_eq!(message.context["data"]["x"], dv(json!(1)));
assert!(
message.context["data"].get("y").is_none(),
"wf_y condition was false — it must be skipped"
);
assert_eq!(message.context["data"]["z"], dv(json!(1)));
let task_ids: Vec<&str> = message
.audit_trail()
.iter()
.map(|a| a.task_id.as_ref())
.collect();
assert_eq!(task_ids, vec!["map_x", "map_z"]);
}