use super::*;
#[tokio::test]
async fn result_unknown_workflow_returns_not_found() -> Result<(), Box<dyn std::error::Error>> {
let store: Arc<dyn EventStore> = Arc::new(InMemoryStore::default());
let engine = engine_with_loaded_workflow(store, "checkout", "checkout_deployed")?;
let workflow_id = aion_core::WorkflowId::new_v4();
let run_id = aion_core::RunId::new_v4();
let result = engine.result(&workflow_id, &run_id).await;
assert!(matches!(result, Err(EngineError::WorkflowNotFound { .. })));
engine.shutdown()?;
Ok(())
}
#[tokio::test]
async fn a_second_failing_engine_shutdown_step_is_reported_not_swallowed()
-> Result<(), Box<dyn std::error::Error>> {
let store: Arc<dyn EventStore> = Arc::new(InMemoryStore::default());
let engine = engine_with_loaded_workflow(store, "checkout", "checkout_deployed")?;
engine.shutdown_gate.force_close_failure();
engine.runtime().force_process_exit_drain_failure();
let (captured, subscriber) = crate::log_capture::LogCapture::new()?;
let returned = {
let _installed = tracing::subscriber::set_default(subscriber);
engine.shutdown()
};
let error = returned
.err()
.ok_or("both steps failed, so shutdown must not return Ok")?;
assert!(
matches!(error, EngineError::RegistryPoisoned),
"control: the FIRST failure is the one returned, and it is the gate's: {error:?}"
);
let reported: Vec<_> = captured
.at_level("ERROR")?
.into_iter()
.filter(|event| event.mentions("a further engine-shutdown step failed"))
.collect();
assert!(
!reported.is_empty(),
"the second failing step must be reported — a teardown failure with no trace at all \
is a swallowed Result, which this codebase forbids outright"
);
assert!(
reported
.iter()
.any(|event| event.field("step") == Some("runtime.shutdown")),
"the report must NAME the step, or the operator cannot tell which half failed: \
{reported:?}"
);
Ok(())
}
#[tokio::test]
async fn a_failing_teardown_step_does_not_skip_the_ones_after_it()
-> Result<(), Box<dyn std::error::Error>> {
let store: Arc<dyn EventStore> = Arc::new(InMemoryStore::default());
let engine = engine_with_loaded_workflow(store, "checkout", "checkout_deployed")?;
engine
.runtime()
.nif_state()
.set_workflow_catalog(Arc::clone(engine.workflow_catalog()));
assert!(
engine
.runtime()
.nif_state()
.installed_workflow_catalog()
.is_some(),
"control: the catalog must be installed before shutdown, or asserting it is \
cleared afterwards measures nothing"
);
engine.runtime().force_process_exit_drain_failure();
let error = engine
.shutdown()
.err()
.ok_or("an injected drain failure must be reported, not swallowed")?;
assert!(
matches!(error, EngineError::ProcessExitRegistryPoisoned),
"(a) the failure must reach the caller as itself: {error:?}"
);
assert!(
!engine.runtime().engine_tasks().is_epoch_open(),
"(b) the epoch must be closed — it is closed FIRST, so a shutdown that failed \
later must still leave it shut"
);
assert!(
engine
.runtime()
.nif_state()
.installed_workflow_catalog()
.is_none(),
"(c) THE DECISIVE ONE: `clear_engine_seams` is ordered AFTER the step that \
failed, so a still-installed catalog means the failure returned early and \
the engine leaked its seams"
);
Ok(())
}
#[tokio::test]
async fn continue_as_new_unknown_workflow_returns_not_found()
-> Result<(), Box<dyn std::error::Error>> {
let store: Arc<dyn EventStore> = Arc::new(InMemoryStore::default());
let engine = engine_with_loaded_workflow(store, "checkout", "checkout_deployed")?;
let workflow_id = aion_core::WorkflowId::new_v4();
let run_id = aion_core::RunId::new_v4();
let result = engine
.continue_as_new(&workflow_id, &run_id, payload("next")?, None)
.await;
assert!(matches!(result, Err(EngineError::WorkflowNotFound { .. })));
engine.shutdown()?;
Ok(())
}
#[tokio::test]
async fn list_workflows_merges_live_and_terminal_without_duplicates()
-> Result<(), Box<dyn std::error::Error>> {
let store: Arc<dyn EventStore> = Arc::new(InMemoryStore::default());
let engine = engine_with_loaded_workflow(Arc::clone(&store), "checkout", "checkout_deployed")?;
let running = insert_active_handle(&engine, Arc::clone(&store), "checkout").await?;
let completed = engine
.start_workflow(
"checkout",
payload("input")?,
HashMap::new(),
String::from("default"),
)
.await?;
terminate::complete(
termination_context(&engine),
completed.workflow_id(),
completed.run_id(),
payload("result")?,
)
.await?;
let summaries = engine.list_workflows(WorkflowFilter::default()).await?;
assert_eq!(summaries.len(), 2);
assert!(summaries.iter().any(|summary| {
&summary.workflow_id == running.workflow_id() && summary.status == WorkflowStatus::Running
}));
assert!(summaries.iter().any(|summary| {
&summary.workflow_id == completed.workflow_id()
&& summary.status == WorkflowStatus::Completed
}));
let completed_only = engine
.list_workflows(WorkflowFilter {
status: Some(WorkflowStatus::Completed),
..WorkflowFilter::default()
})
.await?;
assert_eq!(completed_only.len(), 1);
assert_eq!(&completed_only[0].workflow_id, completed.workflow_id());
engine.shutdown()?;
Ok(())
}
#[tokio::test]
async fn shutdown_rejects_subsequent_starts() -> Result<(), Box<dyn std::error::Error>> {
let store: Arc<dyn EventStore> = Arc::new(InMemoryStore::default());
let engine = engine_with_loaded_workflow(Arc::clone(&store), "checkout", "checkout_deployed")?;
let handle = engine
.start_workflow(
"checkout",
payload("input")?,
HashMap::new(),
String::from("default"),
)
.await?;
terminate::complete(
termination_context(&engine),
handle.workflow_id(),
handle.run_id(),
payload("result")?,
)
.await?;
engine.shutdown()?;
let result = engine
.start_workflow(
"checkout",
payload("after-shutdown")?,
HashMap::new(),
String::from("default"),
)
.await;
assert!(matches!(result, Err(EngineError::ShuttingDown)));
Ok(())
}
#[tokio::test]
async fn shutdown_is_idempotent() -> Result<(), Box<dyn std::error::Error>> {
let store: Arc<dyn EventStore> = Arc::new(InMemoryStore::default());
let engine = engine_with_loaded_workflow(Arc::clone(&store), "checkout", "checkout_deployed")?;
let handle = engine
.start_workflow(
"checkout",
payload("input")?,
HashMap::new(),
String::from("default"),
)
.await?;
terminate::complete(
termination_context(&engine),
handle.workflow_id(),
handle.run_id(),
payload("result")?,
)
.await?;
engine.shutdown()?;
let second = engine.shutdown();
assert!(
second.is_ok(),
"double shutdown should succeed; got {second:?}"
);
Ok(())
}
#[tokio::test]
async fn shutdown_rejects_schedule_creation() -> Result<(), Box<dyn std::error::Error>> {
let store: Arc<dyn EventStore> = Arc::new(InMemoryStore::default());
let engine = engine_with_loaded_workflow(Arc::clone(&store), "checkout", "checkout_deployed")?;
let handle = engine
.start_workflow(
"checkout",
payload("input")?,
HashMap::new(),
String::from("default"),
)
.await?;
terminate::complete(
termination_context(&engine),
handle.workflow_id(),
handle.run_id(),
payload("result")?,
)
.await?;
engine.shutdown()?;
let config = aion_core::ScheduleConfig {
trigger: aion_core::TriggerSpec::Interval {
period: Duration::from_secs(60),
},
overlap_policy: aion_core::OverlapPolicy::Skip,
catch_up_policy: aion_core::CatchUpPolicy::Skip,
workflow_type: String::from("checkout"),
input: payload("scheduled")?,
search_attributes: HashMap::new(),
};
let result = engine.create_schedule(config).await;
assert!(
matches!(result, Err(EngineError::ShuttingDown)),
"create_schedule after shutdown should return ShuttingDown; got {result:?}"
);
Ok(())
}
#[test]
fn engine_drop_releases_the_descriptor_owning_task_runtime() -> Result<(), EngineError> {
let store: Arc<dyn EventStore> = Arc::new(InMemoryStore::default());
let engine = engine_with_loaded_workflow(store, "checkout", "checkout_deployed")?;
let tasks = engine.runtime().engine_tasks();
assert!(
tasks.owns_runtime(),
"control: a live engine owns its task runtime"
);
drop(engine);
assert!(
!tasks.owns_runtime(),
"Engine Drop must release the task runtime so its I/O descriptors cannot accumulate"
);
Ok(())
}