pub struct TestEngine { /* private fields */ }Expand description
Runs a WorkflowHandler against an in-memory store with mocked steps.
See the module documentation for what the harness replaces and what it does not.
§Examples
use ironflow_engine::prelude::*;
use ironflow_engine::testing::{MockShellOutput, TestEngine};
use ironflow_store::models::RunStatus;
use serde_json::json;
let result = TestEngine::new()
.with_handler(Deploy)
.with_mock_shell(|_cfg| Ok(MockShellOutput::ok("deployed")))
.run(json!({"env": "prod"}))
.await?;
assert_eq!(result.status(), RunStatus::Completed);Implementations§
Source§impl TestEngine
impl TestEngine
Sourcepub fn new() -> Self
pub fn new() -> Self
A harness with no handler and no mock.
§Examples
use ironflow_engine::testing::TestEngine;
let harness = TestEngine::new();
assert!(format!("{harness:?}").contains("TestEngine"));Sourcepub fn with_handler(self, handler: impl WorkflowHandler + 'static) -> Self
pub fn with_handler(self, handler: impl WorkflowHandler + 'static) -> Self
Register a handler. The first one registered is what
run executes.
§Panics
Panics when called after the first run: the engine is built once, so a later registration would be silently ignored.
§Examples
use ironflow_engine::prelude::*;
use ironflow_engine::testing::TestEngine;
let harness = TestEngine::new().with_handler(Deploy);Sourcepub fn with_mock_shell(
self,
f: impl Fn(&ShellConfig) -> Result<MockShellOutput, OperationError> + Send + Sync + 'static,
) -> Self
pub fn with_mock_shell( self, f: impl Fn(&ShellConfig) -> Result<MockShellOutput, OperationError> + Send + Sync + 'static, ) -> Self
Answer every shell step with f instead of spawning a process.
Returning Err reproduces a shell failure the same way a non-zero
MockShellOutput::exit_code does.
§Panics
Panics when called after the first run.
§Examples
use ironflow_engine::testing::{MockShellOutput, TestEngine};
let harness = TestEngine::new().with_mock_shell(|cfg| {
if cfg.command.starts_with("git ") {
Ok(MockShellOutput::ok("abc1234"))
} else {
Ok(MockShellOutput::failed(127, "command not found"))
}
});Sourcepub fn with_mock_http(
self,
f: impl Fn(&HttpConfig) -> Result<MockHttpResponse, OperationError> + Send + Sync + 'static,
) -> Self
pub fn with_mock_http( self, f: impl Fn(&HttpConfig) -> Result<MockHttpResponse, OperationError> + Send + Sync + 'static, ) -> Self
Answer every HTTP step with f instead of sending a request.
A non-2xx MockHttpResponse is a normal output, like in production.
Return Err(OperationError::Http { status: None, .. }) to simulate a
transport failure.
§Panics
Panics when called after the first run.
§Examples
use ironflow_engine::testing::{MockHttpResponse, TestEngine};
use serde_json::json;
let harness = TestEngine::new()
.with_mock_http(|_cfg| Ok(MockHttpResponse::json(201, &json!({"id": 7}))));Sourcepub fn with_mock_approval(self, outcome: ApprovalOutcome) -> Self
pub fn with_mock_approval(self, outcome: ApprovalOutcome) -> Self
Resolve every approval gate with outcome instead of suspending.
Without this, a gated handler ends the run in
RunStatus::AwaitingApproval and resume continues it.
§Panics
Panics when called after the first run.
§Examples
use ironflow_engine::testing::{ApprovalOutcome, TestEngine};
let harness = TestEngine::new().with_mock_approval(ApprovalOutcome::Approved);Sourcepub fn with_mock_agent(
self,
f: impl Fn(&AgentConfig) -> Result<AgentOutput, AgentError> + Send + Sync + 'static,
) -> Self
pub fn with_mock_agent( self, f: impl Fn(&AgentConfig) -> Result<AgentOutput, AgentError> + Send + Sync + 'static, ) -> Self
Answer every agent step with f instead of invoking a backend.
§Panics
Panics when called after the first run.
§Examples
use ironflow_core::provider::AgentOutput;
use ironflow_engine::testing::TestEngine;
use serde_json::json;
let harness = TestEngine::new()
.with_mock_agent(|_cfg| Ok(AgentOutput::new(json!({"score": 9}))));Sourcepub fn with_recorded_agent(self, fixtures_dir: &str) -> Self
pub fn with_recorded_agent(self, fixtures_dir: &str) -> Self
Replay agent steps from fixtures recorded in fixtures_dir.
fixtures_dir is the directory, not a file:
RecordReplayProvider keys each fixture by a hash of the
AgentConfig and stores it as <hash>.json inside it. A missing
fixture falls back to MissingAgentProvider, so the step fails loudly
instead of reaching the real Claude CLI.
§Panics
Panics when called after the first run.
§Examples
use ironflow_engine::testing::TestEngine;
let harness = TestEngine::new().with_recorded_agent("tests/fixtures");Sourcepub fn with_agent_provider(self, provider: Arc<dyn AgentProvider>) -> Self
pub fn with_agent_provider(self, provider: Arc<dyn AgentProvider>) -> Self
Use an arbitrary AgentProvider for agent steps.
The escape hatch for anything the three with_mock_* methods do not
cover, such as recording new fixtures.
§Panics
Panics when called after the first run.
§Examples
use std::sync::Arc;
use ironflow_core::provider::AgentProvider;
use ironflow_engine::testing::TestEngine;
let harness = TestEngine::new().with_agent_provider(provider);Sourcepub fn with_decision_provider(self, provider: Arc<dyn DecisionProvider>) -> Self
pub fn with_decision_provider(self, provider: Arc<dyn DecisionProvider>) -> Self
Use a DecisionProvider for ctx.decision(...) steps.
Decision steps are not intercepted: without a provider they fail with
EngineError::NoDecisionProvider.
§Panics
Panics when called after the first run.
§Examples
use std::sync::Arc;
use ironflow_core::decision::DecisionProvider;
use ironflow_engine::testing::TestEngine;
let harness = TestEngine::new().with_decision_provider(provider);Sourcepub fn store(&self) -> Arc<InMemoryStore> ⓘ
pub fn store(&self) -> Arc<InMemoryStore> ⓘ
The store backing this harness, for assertions the accessors do not cover (child runs, step dependencies, logs).
§Examples
use ironflow_engine::error::EngineError;
use ironflow_engine::testing::TestEngine;
use ironflow_store::store::RunStore;
use uuid::Uuid;
let steps = harness.store().list_steps(run_id).await?;
assert!(!steps.is_empty());Sourcepub async fn run(&mut self, payload: Value) -> Result<TestResult, EngineError>
pub async fn run(&mut self, payload: Value) -> Result<TestResult, EngineError>
Run the first handler registered with with_handler.
A handler that fails is not an error: the returned TestResult then
carries RunStatus::Failed and the message in
TestResult::error.
§Errors
Returns EngineError::InvalidWorkflow when no handler was registered
or two handlers share a name, and EngineError::Store when the
in-memory store rejects a write.
§Examples
use ironflow_engine::prelude::*;
use ironflow_engine::testing::{MockShellOutput, TestEngine};
use serde_json::json;
let result = TestEngine::new()
.with_handler(Deploy)
.with_mock_shell(|_cfg| Ok(MockShellOutput::ok("done")))
.run(json!({}))
.await?;
assert!(result.is_completed());Sourcepub async fn run_workflow(
&mut self,
name: &str,
payload: Value,
) -> Result<TestResult, EngineError>
pub async fn run_workflow( &mut self, name: &str, payload: Value, ) -> Result<TestResult, EngineError>
Run a specific registered handler by name.
§Errors
Same as run, plus EngineError::InvalidWorkflow when
name matches no registered handler.
§Examples
use ironflow_engine::error::EngineError;
use ironflow_engine::testing::TestEngine;
use serde_json::json;
let result = harness.run_workflow("child", json!({"id": 1})).await?;
assert!(result.is_completed());Sourcepub async fn resume(&mut self, run_id: Uuid) -> Result<TestResult, EngineError>
pub async fn resume(&mut self, run_id: Uuid) -> Result<TestResult, EngineError>
Resume a run suspended on an approval gate, the way the API server does.
§Errors
Returns EngineError::Store when the run does not exist or is not
resumable, and EngineError::InvalidWorkflow when its handler is no
longer registered.
§Examples
use ironflow_engine::error::EngineError;
use ironflow_engine::testing::TestEngine;
use ironflow_store::models::RunStatus;
use serde_json::json;
let suspended = harness.run(json!({})).await?;
assert_eq!(suspended.status(), RunStatus::AwaitingApproval);
let resumed = harness.resume(suspended.run_id()).await?;
assert_eq!(resumed.status(), RunStatus::Completed);