use std::collections::HashMap;
use crate::engine::types::{
ExecutionEvent, RunState, RunStatus as EngineRunStatus, StepStatus as EngineStepStatus,
};
use crate::state::types::{
BranchState, BranchStatusPersisted, CallStackEntry, FailureDetails, HarnessEvent,
PersistedRunState, RunStatus, SafeBoundary, SafeBoundaryType, StepKind, StepStatePersisted,
StepStatusPersisted,
};
fn format_timestamp() -> String {
chrono::Utc::now()
.format("%Y-%m-%dT%H:%M:%S%.3fZ")
.to_string()
}
fn count_exec_starts_at_boundary(event_log: &[ExecutionEvent], target_boundary: usize) -> usize {
if event_log.is_empty() {
return 0;
}
let mut safe_boundary_count = 0usize;
let mut boundary_event_idx = event_log.len().saturating_sub(1);
for (i, event) in event_log.iter().enumerate() {
if matches!(event, ExecutionEvent::SafeBoundary { .. }) {
if safe_boundary_count == target_boundary {
boundary_event_idx = i;
break;
}
safe_boundary_count += 1;
}
}
event_log[..=boundary_event_idx]
.iter()
.filter(|event| {
matches!(
event,
ExecutionEvent::StepStarted { stepPath, .. }
if stepPath.last().is_some_and(|segment| segment.starts_with("exec:"))
)
})
.count()
}
fn map_boundary_type(boundary_type: &str) -> SafeBoundaryType {
match boundary_type {
"before-step-start" => SafeBoundaryType::BeforeStepStart,
"after-step-complete" => SafeBoundaryType::AfterStepComplete,
"before-conditional-body" => SafeBoundaryType::BeforeConditionalBody,
"after-conditional-body" => SafeBoundaryType::AfterConditionalBody,
"before-loop-iteration" => SafeBoundaryType::BeforeLoopIteration,
"after-loop-iteration" => SafeBoundaryType::AfterLoopIteration,
"after-branch-transition" => SafeBoundaryType::AfterBranchTransition,
"before-join" => SafeBoundaryType::BeforeJoin,
"after-join" => SafeBoundaryType::AfterJoin,
"before-match-arm" => SafeBoundaryType::BeforeMatchArm,
"after-match-arm" => SafeBoundaryType::AfterMatchArm,
_ => SafeBoundaryType::BeforeStepStart,
}
}
fn build_safe_boundaries(engine_state: &RunState) -> Vec<SafeBoundary> {
engine_state
.safeBoundaries
.iter()
.enumerate()
.map(|(position_index, &event_index)| {
let event = &engine_state.events[event_index];
match event {
ExecutionEvent::SafeBoundary {
boundaryType,
stepPath,
..
} => SafeBoundary {
boundary_type: map_boundary_type(boundaryType),
step_path: stepPath.clone(),
timestamp: None,
index: position_index,
},
_ => SafeBoundary {
boundary_type: SafeBoundaryType::BeforeStepStart,
step_path: vec![],
timestamp: None,
index: position_index,
},
}
})
.collect()
}
fn derive_current_boundary_index(engine_state: &RunState) -> i64 {
if engine_state.lastSafeBoundaryIndex < 0 {
return -1;
}
let event_idx = engine_state.lastSafeBoundaryIndex as usize;
engine_state
.safeBoundaries
.iter()
.position(|&idx| idx == event_idx)
.map(|pos| pos as i64)
.unwrap_or(-1)
}
fn build_steps_record(engine_state: &RunState) -> HashMap<String, StepStatePersisted> {
let mut steps: HashMap<String, StepStatePersisted> = HashMap::new();
for engine_step in &engine_state.steps {
let key = engine_step.stepPath.join("/");
let name = engine_step
.stepPath
.last()
.cloned()
.unwrap_or_else(|| key.clone());
let kind = if name.starts_with("exec:") {
StepKind::Exec
} else if name.starts_with("par-and:") {
StepKind::ParAnd
} else if name.starts_with("if:") {
StepKind::If
} else if name.starts_with("while:") {
StepKind::While
} else {
StepKind::Run
};
let status = match engine_step.status {
EngineStepStatus::Pending => StepStatusPersisted::Pending,
EngineStepStatus::InProgress => StepStatusPersisted::InProgress,
EngineStepStatus::Completed => StepStatusPersisted::Completed,
EngineStepStatus::Failed => StepStatusPersisted::Failed,
};
steps.insert(
key,
StepStatePersisted {
name,
kind,
status,
exec_meta: None,
check_result: None,
failure_details: None,
started_at: None,
completed_at: None,
},
);
}
for event in &engine_state.events {
if let ExecutionEvent::CheckEvaluated {
checkName,
result,
reason,
..
} = event
{
for (key, step) in steps.iter_mut() {
let step_name = key.split('/').last().unwrap_or("");
if step_name == checkName {
step.check_result = Some(crate::state::types::CheckResult {
result: *result,
reason: reason.clone(),
});
}
}
}
}
steps
}
fn build_call_stack(engine_state: &RunState) -> Vec<CallStackEntry> {
vec![CallStackEntry {
workflow: engine_state.rootWorkflow.clone(),
step_index: 0,
}]
}
fn branch_keys(branch_path: &[String]) -> (String, String, String) {
let workflow_name = branch_path
.last()
.map(|s| s.strip_prefix("par-and:").unwrap_or(s).to_string())
.unwrap_or_default();
let key = branch_path.join("/");
let parent_key = branch_path[..branch_path.len().saturating_sub(1)].join("/");
(workflow_name, key, parent_key)
}
fn default_branch_state(workflow_name: String) -> BranchState {
BranchState {
workflow_name,
status: BranchStatusPersisted::Running,
output: None,
failure_details: None,
}
}
fn build_branches(engine_state: &RunState) -> Option<HashMap<String, Vec<BranchState>>> {
let mut branches: HashMap<String, Vec<BranchState>> = HashMap::new();
let mut branch_map: HashMap<String, BranchState> = HashMap::new();
let mut parent_keys: HashMap<String, String> = HashMap::new();
for event in &engine_state.events {
match event {
ExecutionEvent::BranchStarted { branchPath, .. } => {
let (workflow_name, key, parent_key) = branch_keys(branchPath);
branch_map.insert(key.clone(), default_branch_state(workflow_name));
parent_keys.insert(key, parent_key.clone());
branches.entry(parent_key).or_default();
}
ExecutionEvent::BranchCompleted {
branchPath, output, ..
} => {
let (workflow_name, key, parent_key) = branch_keys(branchPath);
let entry = branch_map
.entry(key.clone())
.or_insert_with(|| default_branch_state(workflow_name));
entry.status = BranchStatusPersisted::Completed;
if let Some(out) = output {
entry.output = Some(out.clone());
}
parent_keys.insert(key, parent_key.clone());
branches.entry(parent_key).or_default();
}
ExecutionEvent::BranchFailed {
branchPath, error, ..
} => {
let (workflow_name, key, parent_key) = branch_keys(branchPath);
let entry = branch_map
.entry(key.clone())
.or_insert_with(|| default_branch_state(workflow_name));
entry.status = BranchStatusPersisted::Failed;
entry.failure_details = Some(FailureDetails {
message: error.clone(),
timestamp: None,
});
parent_keys.insert(key, parent_key.clone());
branches.entry(parent_key).or_default();
}
_ => {}
}
}
for (key, state) in branch_map {
if let Some(parent_key) = parent_keys.get(&key) {
let arr = branches.entry(parent_key.clone()).or_default();
if !arr.iter().any(|b| b.workflow_name == state.workflow_name) {
arr.push(state);
}
}
}
if branches.is_empty() {
None
} else {
Some(branches)
}
}
fn map_run_status(status: &EngineRunStatus) -> RunStatus {
match status {
EngineRunStatus::Running => RunStatus::Running,
EngineRunStatus::Paused => RunStatus::Paused,
EngineRunStatus::Completed => RunStatus::Completed,
EngineRunStatus::Failed => RunStatus::Failed,
}
}
pub fn to_persisted_state(
engine_state: &RunState,
harness_event_log: Option<Vec<HarnessEvent>>,
) -> PersistedRunState {
let safe_boundaries = build_safe_boundaries(engine_state);
let current_boundary_index = derive_current_boundary_index(engine_state);
let steps = build_steps_record(engine_state);
let call_stack = build_call_stack(engine_state);
let branches = build_branches(engine_state);
PersistedRunState {
schema_version: 1,
run_id: engine_state.runId.clone(),
timestamp: format_timestamp(),
root_workflow: engine_state.rootWorkflow.clone(),
status: map_run_status(&engine_state.status),
call_stack,
steps,
branches,
collected_outputs: None,
harness_event_log,
safe_boundaries,
current_boundary_index,
event_log: Some(engine_state.events.clone()),
}
}
pub fn truncate_to_last_boundary(
persisted: &PersistedRunState,
target_boundary: usize,
) -> PersistedRunState {
let mut truncated = persisted.clone();
truncated.status = RunStatus::Paused;
truncated.current_boundary_index = target_boundary as i64;
truncated.safe_boundaries.truncate(target_boundary + 1);
if let Some(ref mut event_log) = truncated.event_log {
let mut safe_boundary_count = 0usize;
let mut boundary_event_idx = event_log.len().saturating_sub(1);
for (i, event) in event_log.iter().enumerate() {
if matches!(event, ExecutionEvent::SafeBoundary { .. }) {
if safe_boundary_count == target_boundary {
boundary_event_idx = i;
break;
}
safe_boundary_count += 1;
}
}
event_log.truncate(boundary_event_idx + 1);
}
if let Some(ref event_log) = persisted.event_log
&& let Some(ref mut harness_event_log) = truncated.harness_event_log
{
let exec_count = count_exec_starts_at_boundary(event_log, target_boundary);
harness_event_log.retain(|event| event.exec_ordinal < exec_count);
}
truncated
}
pub fn to_engine_state(persisted: &PersistedRunState) -> Result<RunState, String> {
let events = persisted
.event_log
.as_ref()
.ok_or("Cannot restore engine state: eventLog is missing from persisted state")?;
let status = match &persisted.status {
RunStatus::Running => EngineRunStatus::Running,
RunStatus::Paused => EngineRunStatus::Paused,
RunStatus::Completed => EngineRunStatus::Completed,
RunStatus::Failed => EngineRunStatus::Failed,
RunStatus::Pending => {
return Err(
"Cannot restore engine state: status \"pending\" is not a valid engine run status"
.to_string(),
);
}
};
let safe_boundary_event_indices: Vec<usize> = events
.iter()
.enumerate()
.filter_map(|(i, e)| {
if matches!(e, ExecutionEvent::SafeBoundary { .. }) {
Some(i)
} else {
None
}
})
.collect();
let last_safe_boundary_index = if persisted.current_boundary_index >= 0
&& (persisted.current_boundary_index as usize) < safe_boundary_event_indices.len()
{
safe_boundary_event_indices[persisted.current_boundary_index as usize] as i64
} else {
-1
};
let mut steps_map: HashMap<String, crate::engine::types::StepState> = HashMap::new();
for event in events {
match event {
ExecutionEvent::StepStarted { stepPath, .. } => {
let key = stepPath.join("/");
steps_map.insert(
key,
crate::engine::types::StepState {
stepPath: stepPath.clone(),
status: EngineStepStatus::InProgress,
},
);
}
ExecutionEvent::StepCompleted { stepPath, .. } => {
let key = stepPath.join("/");
if let Some(entry) = steps_map.get_mut(&key) {
entry.status = EngineStepStatus::Completed;
}
}
ExecutionEvent::StepFailed { stepPath, .. } => {
let key = stepPath.join("/");
if let Some(entry) = steps_map.get_mut(&key) {
entry.status = EngineStepStatus::Failed;
}
}
_ => {}
}
}
let steps: Vec<crate::engine::types::StepState> = steps_map.into_values().collect();
Ok(RunState {
runId: persisted.run_id.clone(),
rootWorkflow: persisted.root_workflow.clone(),
status,
steps,
events: events.clone(),
safeBoundaries: safe_boundary_event_indices,
lastSafeBoundaryIndex: last_safe_boundary_index,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::engine::types::{
ExecutionEvent, RunState, RunStatus as EngineRunStatus, StepState as EngineStepState,
StepStatus as EngineStepStatus, safe_boundary_types,
};
fn make_engine_state() -> RunState {
RunState {
runId: "run-adapter-test".to_string(),
rootWorkflow: "main".to_string(),
status: EngineRunStatus::Completed,
steps: vec![
EngineStepState {
stepPath: vec!["main".to_string(), "build".to_string()],
status: EngineStepStatus::Completed,
},
EngineStepState {
stepPath: vec!["main".to_string(), "test".to_string()],
status: EngineStepStatus::Completed,
},
],
events: vec![
ExecutionEvent::StepStarted {
runId: "run-adapter-test".to_string(),
stepPath: vec!["main".to_string(), "build".to_string()],
},
ExecutionEvent::SafeBoundary {
runId: "run-adapter-test".to_string(),
boundaryType: safe_boundary_types::AFTER_STEP_COMPLETE.to_string(),
stepPath: vec!["main".to_string(), "build".to_string()],
},
ExecutionEvent::StepCompleted {
runId: "run-adapter-test".to_string(),
stepPath: vec!["main".to_string(), "build".to_string()],
},
ExecutionEvent::StepStarted {
runId: "run-adapter-test".to_string(),
stepPath: vec!["main".to_string(), "test".to_string()],
},
ExecutionEvent::SafeBoundary {
runId: "run-adapter-test".to_string(),
boundaryType: safe_boundary_types::BEFORE_STEP_START.to_string(),
stepPath: vec!["main".to_string(), "test".to_string()],
},
ExecutionEvent::StepCompleted {
runId: "run-adapter-test".to_string(),
stepPath: vec!["main".to_string(), "test".to_string()],
},
],
safeBoundaries: vec![1, 4], lastSafeBoundaryIndex: 4, }
}
#[test]
fn test_to_persisted_state() {
let engine = make_engine_state();
let persisted = to_persisted_state(&engine, None);
assert_eq!(persisted.schema_version, 1);
assert_eq!(persisted.run_id, "run-adapter-test");
assert_eq!(persisted.root_workflow, "main");
assert_eq!(persisted.status, RunStatus::Completed);
assert_eq!(persisted.call_stack.len(), 1);
assert_eq!(persisted.call_stack[0].workflow, "main");
assert_eq!(persisted.steps.len(), 2);
assert_eq!(persisted.safe_boundaries.len(), 2);
assert_eq!(persisted.current_boundary_index, 1); assert!(persisted.event_log.is_some());
assert_eq!(persisted.event_log.as_ref().unwrap().len(), 6);
}
#[test]
fn test_to_engine_state() {
let engine = make_engine_state();
let persisted = to_persisted_state(&engine, None);
let restored = to_engine_state(&persisted).unwrap();
assert_eq!(restored.runId, engine.runId);
assert_eq!(restored.rootWorkflow, engine.rootWorkflow);
assert_eq!(restored.status, engine.status);
assert_eq!(restored.events.len(), engine.events.len());
assert_eq!(restored.safeBoundaries, engine.safeBoundaries);
assert_eq!(restored.lastSafeBoundaryIndex, engine.lastSafeBoundaryIndex);
}
#[test]
fn test_round_trip() {
let engine = make_engine_state();
let persisted = to_persisted_state(&engine, None);
let restored = to_engine_state(&persisted).unwrap();
assert_eq!(restored.runId, engine.runId);
assert_eq!(restored.rootWorkflow, engine.rootWorkflow);
assert_eq!(restored.status, engine.status);
assert_eq!(restored.safeBoundaries, engine.safeBoundaries);
assert_eq!(restored.lastSafeBoundaryIndex, engine.lastSafeBoundaryIndex);
assert_eq!(restored.steps.len(), engine.steps.len());
for engine_step in &engine.steps {
let key = engine_step.stepPath.join("/");
let restored_step = restored
.steps
.iter()
.find(|s| s.stepPath.join("/") == key)
.expect(&format!("Step {} not found in restored state", key));
assert_eq!(restored_step.status, engine_step.status);
}
}
#[test]
fn test_to_engine_state_no_event_log() {
let persisted = PersistedRunState {
schema_version: 1,
run_id: "run-no-log".to_string(),
timestamp: "2026-04-09T00:00:00Z".to_string(),
root_workflow: "main".to_string(),
status: RunStatus::Completed,
call_stack: vec![],
steps: HashMap::new(),
branches: None,
collected_outputs: None,
harness_event_log: None,
safe_boundaries: vec![],
current_boundary_index: -1,
event_log: None,
};
let result = to_engine_state(&persisted);
assert!(result.is_err());
assert!(result.unwrap_err().contains("eventLog is missing"));
}
#[test]
fn test_to_engine_state_pending_status_rejected() {
let persisted = PersistedRunState {
schema_version: 1,
run_id: "run-pending".to_string(),
timestamp: "2026-04-09T00:00:00Z".to_string(),
root_workflow: "main".to_string(),
status: RunStatus::Pending,
call_stack: vec![],
steps: HashMap::new(),
branches: None,
collected_outputs: None,
harness_event_log: None,
safe_boundaries: vec![],
current_boundary_index: -1,
event_log: Some(vec![]),
};
let result = to_engine_state(&persisted);
assert!(result.is_err());
assert!(result.unwrap_err().contains("pending"));
}
#[test]
fn test_to_persisted_state_includes_harness_event_log() {
let engine = make_engine_state();
let harness_event_log = vec![HarnessEvent {
sequence: 0,
exec_ordinal: 0,
stream: crate::state::types::HarnessEventStream::Stdout,
kind: crate::state::types::HarnessEventKind::Json,
raw: "{\"type\":\"message\"}".to_string(),
parsed: Some(serde_json::json!({"type": "message"})),
step_path: Some(vec!["main".to_string(), "exec:test".to_string()]),
boundary_index: Some(1),
timestamp: None,
}];
let persisted = to_persisted_state(&engine, Some(harness_event_log.clone()));
assert_eq!(persisted.harness_event_log, Some(harness_event_log));
}
#[test]
fn test_truncate_to_last_boundary_truncates_harness_event_log_by_exec_ordinal() {
let engine = RunState {
runId: "run-harness-reset".to_string(),
rootWorkflow: "main".to_string(),
status: EngineRunStatus::Paused,
steps: vec![],
events: vec![
ExecutionEvent::SafeBoundary {
runId: "run-harness-reset".to_string(),
boundaryType: safe_boundary_types::BEFORE_STEP_START.to_string(),
stepPath: vec!["main".to_string(), "exec:first".to_string()],
},
ExecutionEvent::StepStarted {
runId: "run-harness-reset".to_string(),
stepPath: vec!["main".to_string(), "exec:first".to_string()],
},
ExecutionEvent::SafeBoundary {
runId: "run-harness-reset".to_string(),
boundaryType: safe_boundary_types::AFTER_STEP_COMPLETE.to_string(),
stepPath: vec!["main".to_string(), "exec:first".to_string()],
},
ExecutionEvent::StepStarted {
runId: "run-harness-reset".to_string(),
stepPath: vec!["main".to_string(), "exec:second".to_string()],
},
ExecutionEvent::SafeBoundary {
runId: "run-harness-reset".to_string(),
boundaryType: safe_boundary_types::AFTER_STEP_COMPLETE.to_string(),
stepPath: vec!["main".to_string(), "exec:second".to_string()],
},
],
safeBoundaries: vec![0, 2, 4],
lastSafeBoundaryIndex: 4,
};
let persisted = to_persisted_state(
&engine,
Some(vec![
HarnessEvent {
sequence: 0,
exec_ordinal: 0,
stream: crate::state::types::HarnessEventStream::Stdout,
kind: crate::state::types::HarnessEventKind::Json,
raw: "{\"type\":\"first\"}".to_string(),
parsed: Some(serde_json::json!({"type": "first"})),
step_path: Some(vec!["main".to_string(), "exec:first".to_string()]),
boundary_index: Some(1),
timestamp: None,
},
HarnessEvent {
sequence: 1,
exec_ordinal: 1,
stream: crate::state::types::HarnessEventStream::Stdout,
kind: crate::state::types::HarnessEventKind::Json,
raw: "{\"type\":\"second\"}".to_string(),
parsed: Some(serde_json::json!({"type": "second"})),
step_path: Some(vec!["main".to_string(), "exec:second".to_string()]),
boundary_index: Some(2),
timestamp: None,
},
]),
);
let truncated = truncate_to_last_boundary(&persisted, 1);
let harness_event_log = truncated.harness_event_log.unwrap();
assert_eq!(harness_event_log.len(), 1);
assert_eq!(harness_event_log[0].exec_ordinal, 0);
}
#[test]
fn test_truncate_to_last_boundary_handles_empty_event_log() {
let persisted = PersistedRunState {
schema_version: 1,
run_id: "run-empty-event-log".to_string(),
timestamp: "2026-04-10T00:00:00Z".to_string(),
root_workflow: "main".to_string(),
status: RunStatus::Paused,
call_stack: vec![],
steps: HashMap::new(),
branches: None,
collected_outputs: None,
harness_event_log: Some(vec![HarnessEvent {
sequence: 0,
exec_ordinal: 0,
stream: crate::state::types::HarnessEventStream::Stdout,
kind: crate::state::types::HarnessEventKind::Json,
raw: "{\"type\":\"message\"}".to_string(),
parsed: Some(serde_json::json!({"type": "message"})),
step_path: Some(vec!["main".to_string(), "exec:test".to_string()]),
boundary_index: Some(0),
timestamp: None,
}]),
safe_boundaries: vec![],
current_boundary_index: -1,
event_log: Some(vec![]),
};
let truncated = truncate_to_last_boundary(&persisted, 0);
assert_eq!(truncated.harness_event_log.unwrap().len(), 0);
}
#[test]
fn test_map_boundary_type_all_variants() {
assert_eq!(
map_boundary_type("before-step-start"),
SafeBoundaryType::BeforeStepStart
);
assert_eq!(
map_boundary_type("after-step-complete"),
SafeBoundaryType::AfterStepComplete
);
assert_eq!(
map_boundary_type("before-conditional-body"),
SafeBoundaryType::BeforeConditionalBody
);
assert_eq!(
map_boundary_type("after-conditional-body"),
SafeBoundaryType::AfterConditionalBody
);
assert_eq!(
map_boundary_type("before-loop-iteration"),
SafeBoundaryType::BeforeLoopIteration
);
assert_eq!(
map_boundary_type("after-loop-iteration"),
SafeBoundaryType::AfterLoopIteration
);
assert_eq!(
map_boundary_type("after-branch-transition"),
SafeBoundaryType::AfterBranchTransition
);
assert_eq!(
map_boundary_type("before-join"),
SafeBoundaryType::BeforeJoin
);
assert_eq!(map_boundary_type("after-join"), SafeBoundaryType::AfterJoin);
assert_eq!(
map_boundary_type("before-match-arm"),
SafeBoundaryType::BeforeMatchArm
);
assert_eq!(
map_boundary_type("after-match-arm"),
SafeBoundaryType::AfterMatchArm
);
assert_eq!(
map_boundary_type("unknown-type"),
SafeBoundaryType::BeforeStepStart
);
}
#[test]
fn test_build_branches_from_events() {
let engine = RunState {
runId: "run-branch-test".to_string(),
rootWorkflow: "main".to_string(),
status: EngineRunStatus::Completed,
steps: vec![],
events: vec![
ExecutionEvent::BranchStarted {
runId: "run-branch-test".to_string(),
branchPath: vec!["par".to_string(), "par-and:branch-a".to_string()],
},
ExecutionEvent::BranchCompleted {
runId: "run-branch-test".to_string(),
branchPath: vec!["par".to_string(), "par-and:branch-a".to_string()],
output: Some(serde_json::json!({"ok": true})),
},
ExecutionEvent::BranchStarted {
runId: "run-branch-test".to_string(),
branchPath: vec!["par".to_string(), "par-and:branch-b".to_string()],
},
ExecutionEvent::BranchFailed {
runId: "run-branch-test".to_string(),
branchPath: vec!["par".to_string(), "par-and:branch-b".to_string()],
error: "oops".to_string(),
},
],
safeBoundaries: vec![],
lastSafeBoundaryIndex: -1,
};
let persisted = to_persisted_state(&engine, None);
assert!(persisted.branches.is_some());
let branches = persisted.branches.as_ref().unwrap();
let par_branches = branches.get("par").unwrap();
assert_eq!(par_branches.len(), 2);
let branch_a = par_branches
.iter()
.find(|b| b.workflow_name == "branch-a")
.unwrap();
assert_eq!(branch_a.status, BranchStatusPersisted::Completed);
let branch_b = par_branches
.iter()
.find(|b| b.workflow_name == "branch-b")
.unwrap();
assert_eq!(branch_b.status, BranchStatusPersisted::Failed);
assert!(branch_b.failure_details.is_some());
}
#[test]
fn test_empty_engine_state_round_trip() {
let engine = RunState {
runId: "run-empty".to_string(),
rootWorkflow: "main".to_string(),
status: EngineRunStatus::Running,
steps: vec![],
events: vec![],
safeBoundaries: vec![],
lastSafeBoundaryIndex: -1,
};
let persisted = to_persisted_state(&engine, None);
assert_eq!(persisted.current_boundary_index, -1);
assert!(persisted.safe_boundaries.is_empty());
assert!(persisted.steps.is_empty());
assert!(persisted.branches.is_none());
let restored = to_engine_state(&persisted).unwrap();
assert_eq!(restored.runId, "run-empty");
assert_eq!(restored.status, EngineRunStatus::Running);
assert!(restored.steps.is_empty());
assert!(restored.safeBoundaries.is_empty());
assert_eq!(restored.lastSafeBoundaryIndex, -1);
}
}