1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
//! In-memory test harness for workflow handlers.
//!
//! [`TestEngine`] runs a real [`Engine`](crate::engine::Engine) against a real
//! [`InMemoryStore`](ironflow_store::memory::InMemoryStore): the run, the steps,
//! the FSM transitions and the persistence are the production ones. Only the
//! outside world is swapped out -- shell commands, HTTP requests, agent
//! invocations, approval gates and human inputs are answered from closures instead of
//! spawning processes, opening sockets or waiting for a human.
//!
//! What it does *not* start: no HTTP server, no background worker, no Postgres.
//! A run executes inline, in the calling task, and finishes before
//! [`run`](TestEngine::run) returns.
//!
//! # Examples
//!
//! ```no_run
//! use ironflow_engine::prelude::*;
//! use ironflow_engine::testing::{ApprovalOutcome, MockShellOutput, TestEngine};
//! use ironflow_store::models::RunStatus;
//! use serde_json::json;
//!
//! # struct Deploy;
//! # impl WorkflowHandler for Deploy {
//! # fn name(&self) -> &str { "deploy" }
//! # fn execute<'a>(&'a self, ctx: &'a mut WorkflowContext) -> HandlerFuture<'a> {
//! # Box::pin(async move { ctx.shell("deploy", ShellConfig::new("./deploy.sh")).await?; Ok(()) })
//! # }
//! # }
//! # async fn example() -> Result<(), EngineError> {
//! let result = TestEngine::new()
//! .with_handler(Deploy)
//! .with_mock_shell(|_cfg| Ok(MockShellOutput::ok(r#"{"version":"1.2.3"}"#)))
//! .with_mock_approval(ApprovalOutcome::Approved)
//! .run(json!({"env": "prod"}))
//! .await?;
//!
//! assert_eq!(result.status(), RunStatus::Completed);
//! assert_eq!(result.step("deploy").step_output().stdout(), r#"{"version":"1.2.3"}"#);
//! # Ok(())
//! # }
//! ```
//!
//! # What the harness covers
//!
//! | Step | How it is mocked |
//! |------|------------------|
//! | `ctx.shell` | [`TestEngine::with_mock_shell`] |
//! | `ctx.http` | [`TestEngine::with_mock_http`] |
//! | `ctx.agent` | [`TestEngine::with_mock_agent`] or [`TestEngine::with_recorded_agent`] |
//! | `ctx.approval` | [`TestEngine::with_mock_approval`], or [`TestEngine::resume`] |
//! | `ctx.human_input` | [`TestEngine::with_mock_human_input`], or [`TestEngine::resume`] after writing the answer on the step |
//! | `ctx.parallel`, `ctx.workflow`, `on_error` | the mocks above apply to the steps inside them |
//!
//! # Limitations
//!
//! * Custom operations ([`ctx.operation`](crate::context::WorkflowContext::operation))
//! are not intercepted. Mock one by passing a test-double
//! [`Operation`](crate::operation::Operation) to the handler.
//! * [`ctx.delay`](crate::context::WorkflowContext::delay) is not intercepted: a
//! non-zero delay still suspends the run with
//! [`RunStatus::Sleeping`](ironflow_store::models::RunStatus::Sleeping).
//! * [`ctx.decision`](crate::context::WorkflowContext::decision) needs a real
//! [`DecisionProvider`](ironflow_core::decision::DecisionProvider), wired with
//! [`TestEngine::with_decision_provider`].
pub use TestEngine;
pub use ;
pub use ;
// Re-exported so test code has a single import path for everything the harness
// needs.
pub use crate;