mod common;
use common::{setup_tracing, TestContext, TestError};
use orka::test_util::{assert_run_outcome, ExecutionCounter, PipelineTestExt};
use orka::{ContextData, Pipeline, PipelineControl, PipelineResult, RunOutcome, TraceCollector, TraceEventKind};
fn two_step_pipeline() -> Pipeline<TestContext, TestError> {
let mut p: Pipeline<TestContext, TestError> = Pipeline::new(["alpha", "beta"]);
p.on_root("alpha", |ctx| async move {
ctx.write().steps_executed.push("alpha".into());
Ok(PipelineControl::Continue)
})
.on_root("beta", |ctx| async move {
ctx.write().steps_executed.push("beta".into());
Ok(PipelineControl::Continue)
});
p
}
#[tokio::test]
async fn on_finish_fires_on_completed() {
setup_tracing();
let mut p = two_step_pipeline();
let counter = ExecutionCounter::new();
let seen = ContextData::new(Vec::<RunOutcome>::new());
let seen_in_handler = seen.clone();
let c = counter.clone();
p.on_finish(move |_ctx, outcome| {
let c = c.clone();
let seen = seen_in_handler.clone();
async move {
c.increment();
seen.write().push(outcome);
Ok(())
}
});
let ctx = ContextData::new(TestContext::default());
let result = p.run(ctx).await.unwrap();
assert_eq!(result, PipelineResult::Completed);
assert_eq!(counter.get(), 1);
assert_eq!(seen.read().as_slice(), &[RunOutcome::Completed]);
}
#[tokio::test]
async fn on_finish_fires_on_stopped() {
setup_tracing();
let mut p = two_step_pipeline();
p.replace_on_root("alpha", |_ctx| async { Ok(PipelineControl::Stop) });
let seen = ContextData::new(Vec::<RunOutcome>::new());
let seen_in_handler = seen.clone();
p.on_finish(move |_ctx, outcome| {
let seen = seen_in_handler.clone();
async move {
seen.write().push(outcome);
Ok(())
}
});
let result = p.run(ContextData::new(TestContext::default())).await.unwrap();
assert_eq!(result, PipelineResult::Stopped);
assert_eq!(seen.read().as_slice(), &[RunOutcome::Stopped]);
}
#[tokio::test]
async fn on_finish_fires_on_handler_error_and_preserves_original_error() {
setup_tracing();
let mut p = two_step_pipeline();
p.fail_at("beta", || TestError::Handler("boom".into()));
let seen = ContextData::new(Vec::<RunOutcome>::new());
let seen_in_handler = seen.clone();
p.on_finish(move |_ctx, outcome| {
let seen = seen_in_handler.clone();
async move {
seen.write().push(outcome);
Err(TestError::Other("finish failure".into()))
}
});
let err = p.run(ContextData::new(TestContext::default())).await.unwrap_err();
assert_eq!(err, TestError::Handler("boom".into()));
let first_outcome = seen.read()[0].clone();
match first_outcome {
RunOutcome::Errored { step, .. } => assert_eq!(step, "beta"),
other => panic!("expected Errored outcome, got {:?}", other),
}
}
#[tokio::test]
async fn on_finish_fires_on_missing_handler_config_error() {
setup_tracing();
let mut p: Pipeline<TestContext, TestError> = Pipeline::new(["alpha", "beta"]);
p.on_root("alpha", |_ctx| async { Ok(PipelineControl::Continue) });
let counter = ExecutionCounter::new();
let c = counter.clone();
p.on_finish(move |_ctx, outcome| {
let c = c.clone();
async move {
assert!(matches!(outcome, RunOutcome::Errored { .. }));
c.increment();
Ok(())
}
});
let err = p.run(ContextData::new(TestContext::default())).await.unwrap_err();
assert!(matches!(err, TestError::Orka(_)));
assert_eq!(counter.get(), 1);
}
#[tokio::test]
async fn finish_error_on_ok_run_becomes_the_run_error_for_completed_and_stopped() {
setup_tracing();
for stop in [false, true] {
let mut p = two_step_pipeline();
if stop {
p.replace_on_root("beta", |_ctx| async { Ok(PipelineControl::Stop) });
}
p.on_finish(|_ctx, _outcome| async { Err(TestError::Other("cleanup failed".into())) });
let err = p.run(ContextData::new(TestContext::default())).await.unwrap_err();
assert_eq!(err, TestError::Other("cleanup failed".into()), "stop = {}", stop);
}
}
#[tokio::test]
async fn all_finish_handlers_run_in_order_even_when_one_fails() {
setup_tracing();
let mut p = two_step_pipeline();
let order = ContextData::new(Vec::<&'static str>::new());
let (o1, o2, o3) = (order.clone(), order.clone(), order.clone());
p.on_finish(move |_ctx, _outcome| {
let o = o1.clone();
async move {
o.write().push("first");
Ok(())
}
})
.on_finish(move |_ctx, _outcome| {
let o = o2.clone();
async move {
o.write().push("second (fails)");
Err(TestError::Other("first failure".into()))
}
})
.on_finish(move |_ctx, _outcome| {
let o = o3.clone();
async move {
o.write().push("third");
Err(TestError::Other("second failure".into()))
}
});
let err = p.run(ContextData::new(TestContext::default())).await.unwrap_err();
assert_eq!(err, TestError::Other("first failure".into()));
assert_eq!(order.read().as_slice(), &["first", "second (fails)", "third"]);
}
#[tokio::test]
async fn finish_ring_traces_finalizers_and_final_outcome() {
setup_tracing();
let mut p = two_step_pipeline();
p.on_finish(|_ctx, _outcome| async { Ok(()) });
p.on_finish(|_ctx, _outcome| async { Err(TestError::Other("late failure".into())) });
let trace = TraceCollector::new();
p.set_tracer(trace.clone());
let _ = p.run(ContextData::new(TestContext::default())).await;
let finalizer_events: Vec<_> = trace
.events()
.into_iter()
.filter(|e| matches!(e.kind, TraceEventKind::FinalizerFinished { .. }))
.collect();
assert_eq!(finalizer_events.len(), 2);
assert_run_outcome(
&trace,
RunOutcome::Errored {
step: "on_finish".to_string(),
message: "late failure".to_string(),
},
);
}
#[tokio::test]
async fn partial_runners_and_resolve_plan_never_fire_finish_handlers() {
setup_tracing();
let mut p: Pipeline<TestContext, TestError> = Pipeline::new(["drain", "install"]);
p.on_root("drain", |ctx| async move {
ctx.write().data_for_scoped = Some("drain-id-42".into());
Ok(PipelineControl::Continue)
})
.on_root("install", |_ctx| async { Ok(PipelineControl::Continue) });
let restores = ExecutionCounter::new();
let r = restores.clone();
p.on_finish(move |ctx, _outcome| {
let r = r.clone();
async move {
if ctx.write().data_for_scoped.take().is_some() {
r.increment();
}
Ok(())
}
});
let ctx = ContextData::new(TestContext::default());
p.run_step("drain", ctx.clone()).await.unwrap();
assert_eq!(ctx.read().data_for_scoped.as_deref(), Some("drain-id-42"));
assert_eq!(restores.get(), 0);
let ctx2 = ContextData::new(TestContext::default());
p.run_from("drain", ctx2.clone()).await.unwrap();
assert_eq!(ctx2.read().data_for_scoped.as_deref(), Some("drain-id-42"));
let ctx3 = ContextData::new(TestContext::default());
p.run_until("drain", ctx3.clone()).await.unwrap();
assert_eq!(ctx3.read().data_for_scoped.as_deref(), Some("drain-id-42"));
assert_eq!(restores.get(), 0);
let _plan = p.resolve_plan(&ContextData::new(TestContext::default()));
assert_eq!(restores.get(), 0);
let ctx4 = ContextData::new(TestContext::default());
p.run(ctx4.clone()).await.unwrap();
assert_eq!(ctx4.read().data_for_scoped, None);
assert_eq!(restores.get(), 1);
}