Skip to main content

TestEngine

Struct TestEngine 

Source
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

Source

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"));
Source

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);
Source

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"))
    }
});
Source

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}))));
Source

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);
Source

pub fn with_mock_human_input( self, f: impl Fn(&str, &HumanInputConfig) -> HumanInputOutcome + Send + Sync + 'static, ) -> Self

Answer every human input step with f instead of suspending.

f receives the step name and its config. Without this, a handler that asks for a human input ends the run in RunStatus::AwaitingApproval; write the answer on the step through the store, then call resume.

§Panics

Panics when called after the first run.

§Examples
use ironflow_engine::testing::{HumanInputOutcome, TestEngine};
use serde_json::json;

let harness = TestEngine::new().with_mock_human_input(|_name, _cfg| {
    HumanInputOutcome::Provided(json!({"answers": ["staging"]}))
});
Source

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}))));
Source

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");
Source

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);
Source

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);
Source

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());
Source

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());
Source

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());
Source

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);

Trait Implementations§

Source§

impl Debug for TestEngine

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Default for TestEngine

Source§

fn default() -> Self

Returns the “default value” for a type. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self> ⓘ

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self> ⓘ

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> PolicyExt for T
where T: ?Sized,

Source§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow only if self and other return Action::Follow. Read more
Source§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow if either self or other returns Action::Follow. Read more
Source§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V

Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self> ⓘ
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self> ⓘ

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more