use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum RunStatus {
#[serde(rename = "pending")]
Pending,
#[serde(rename = "running")]
Running,
#[serde(rename = "completed")]
Completed,
#[serde(rename = "failed")]
Failed,
#[serde(rename = "paused")]
Paused,
}
impl fmt::Display for RunStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
RunStatus::Pending => write!(f, "pending"),
RunStatus::Running => write!(f, "running"),
RunStatus::Completed => write!(f, "completed"),
RunStatus::Failed => write!(f, "failed"),
RunStatus::Paused => write!(f, "paused"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum StepKind {
#[serde(rename = "run")]
Run,
#[serde(rename = "if")]
If,
#[serde(rename = "while")]
While,
#[serde(rename = "par-and")]
ParAnd,
#[serde(rename = "exec")]
Exec,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum StepStatusPersisted {
#[serde(rename = "pending")]
Pending,
#[serde(rename = "in_progress")]
InProgress,
#[serde(rename = "completed")]
Completed,
#[serde(rename = "failed")]
Failed,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum BranchStatusPersisted {
#[serde(rename = "pending")]
Pending,
#[serde(rename = "running")]
Running,
#[serde(rename = "completed")]
Completed,
#[serde(rename = "failed")]
Failed,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum SafeBoundaryType {
#[serde(rename = "before-step-start")]
BeforeStepStart,
#[serde(rename = "after-step-complete")]
AfterStepComplete,
#[serde(rename = "before-conditional-body")]
BeforeConditionalBody,
#[serde(rename = "after-conditional-body")]
AfterConditionalBody,
#[serde(rename = "before-loop-iteration")]
BeforeLoopIteration,
#[serde(rename = "after-loop-iteration")]
AfterLoopIteration,
#[serde(rename = "after-branch-transition")]
AfterBranchTransition,
#[serde(rename = "before-join")]
BeforeJoin,
#[serde(rename = "after-join")]
AfterJoin,
#[serde(rename = "before-match-arm")]
BeforeMatchArm,
#[serde(rename = "after-match-arm")]
AfterMatchArm,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExecMeta {
#[serde(skip_serializing_if = "Option::is_none")]
pub harness: Option<String>,
#[serde(rename = "promptRef", skip_serializing_if = "Option::is_none")]
pub prompt_ref: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub args: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CheckResult {
pub result: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FailureDetails {
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub timestamp: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct HarnessEvent {
pub sequence: usize,
#[serde(rename = "execOrdinal")]
pub exec_ordinal: usize,
pub stream: HarnessEventStream,
pub kind: HarnessEventKind,
pub raw: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub parsed: Option<serde_json::Value>,
#[serde(rename = "stepPath", skip_serializing_if = "Option::is_none")]
pub step_path: Option<Vec<String>>,
#[serde(rename = "boundaryIndex", skip_serializing_if = "Option::is_none")]
pub boundary_index: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub timestamp: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum HarnessEventStream {
#[serde(rename = "stdout")]
Stdout,
#[serde(rename = "stderr")]
Stderr,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum HarnessEventKind {
#[serde(rename = "json")]
Json,
#[serde(rename = "text")]
Text,
#[serde(rename = "status")]
Status,
#[serde(rename = "error")]
Error,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StepStatePersisted {
pub name: String,
pub kind: StepKind,
pub status: StepStatusPersisted,
#[serde(rename = "execMeta", skip_serializing_if = "Option::is_none")]
pub exec_meta: Option<ExecMeta>,
#[serde(rename = "checkResult", skip_serializing_if = "Option::is_none")]
pub check_result: Option<CheckResult>,
#[serde(rename = "failureDetails", skip_serializing_if = "Option::is_none")]
pub failure_details: Option<FailureDetails>,
#[serde(rename = "startedAt", skip_serializing_if = "Option::is_none")]
pub started_at: Option<String>,
#[serde(rename = "completedAt", skip_serializing_if = "Option::is_none")]
pub completed_at: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BranchState {
#[serde(rename = "workflowName")]
pub workflow_name: String,
pub status: BranchStatusPersisted,
#[serde(skip_serializing_if = "Option::is_none")]
pub output: Option<serde_json::Value>,
#[serde(rename = "failureDetails", skip_serializing_if = "Option::is_none")]
pub failure_details: Option<FailureDetails>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SafeBoundary {
#[serde(rename = "type")]
pub boundary_type: SafeBoundaryType,
#[serde(rename = "stepPath")]
pub step_path: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub timestamp: Option<String>,
pub index: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CallStackEntry {
pub workflow: String,
#[serde(rename = "stepIndex")]
pub step_index: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CollectedOutput {
pub workflow: String,
pub output: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PersistedRunState {
#[serde(rename = "schemaVersion")]
pub schema_version: u32,
#[serde(rename = "runId")]
pub run_id: String,
pub timestamp: String,
#[serde(rename = "rootWorkflow")]
pub root_workflow: String,
pub status: RunStatus,
#[serde(rename = "callStack")]
pub call_stack: Vec<CallStackEntry>,
pub steps: HashMap<String, StepStatePersisted>,
#[serde(skip_serializing_if = "Option::is_none")]
pub branches: Option<HashMap<String, Vec<BranchState>>>,
#[serde(rename = "collectedOutputs", skip_serializing_if = "Option::is_none")]
pub collected_outputs: Option<HashMap<String, Vec<CollectedOutput>>>,
#[serde(rename = "harnessEventLog", skip_serializing_if = "Option::is_none")]
pub harness_event_log: Option<Vec<HarnessEvent>>,
#[serde(rename = "safeBoundaries")]
pub safe_boundaries: Vec<SafeBoundary>,
#[serde(rename = "currentBoundaryIndex")]
pub current_boundary_index: i64,
#[serde(rename = "eventLog", skip_serializing_if = "Option::is_none")]
pub event_log: Option<Vec<crate::engine::types::ExecutionEvent>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunSummary {
#[serde(rename = "runId")]
pub run_id: String,
pub timestamp: String,
#[serde(rename = "rootWorkflow")]
pub root_workflow: String,
pub status: RunStatus,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_run_status_display() {
assert_eq!(format!("{}", RunStatus::Pending), "pending");
assert_eq!(format!("{}", RunStatus::Running), "running");
assert_eq!(format!("{}", RunStatus::Completed), "completed");
assert_eq!(format!("{}", RunStatus::Failed), "failed");
assert_eq!(format!("{}", RunStatus::Paused), "paused");
}
#[test]
fn test_run_status_serialization() {
assert_eq!(
serde_json::to_string(&RunStatus::Pending).unwrap(),
"\"pending\""
);
assert_eq!(
serde_json::to_string(&RunStatus::Running).unwrap(),
"\"running\""
);
assert_eq!(
serde_json::to_string(&RunStatus::Completed).unwrap(),
"\"completed\""
);
assert_eq!(
serde_json::to_string(&RunStatus::Failed).unwrap(),
"\"failed\""
);
assert_eq!(
serde_json::to_string(&RunStatus::Paused).unwrap(),
"\"paused\""
);
}
#[test]
fn test_run_status_deserialization() {
let pending: RunStatus = serde_json::from_str("\"pending\"").unwrap();
assert_eq!(pending, RunStatus::Pending);
let running: RunStatus = serde_json::from_str("\"running\"").unwrap();
assert_eq!(running, RunStatus::Running);
}
#[test]
fn test_step_kind_serialization() {
assert_eq!(serde_json::to_string(&StepKind::Run).unwrap(), "\"run\"");
assert_eq!(serde_json::to_string(&StepKind::If).unwrap(), "\"if\"");
assert_eq!(
serde_json::to_string(&StepKind::While).unwrap(),
"\"while\""
);
assert_eq!(
serde_json::to_string(&StepKind::ParAnd).unwrap(),
"\"par-and\""
);
assert_eq!(serde_json::to_string(&StepKind::Exec).unwrap(), "\"exec\"");
}
#[test]
fn test_safe_boundary_type_all_9_variants() {
let types = vec![
(SafeBoundaryType::BeforeStepStart, "before-step-start"),
(SafeBoundaryType::AfterStepComplete, "after-step-complete"),
(
SafeBoundaryType::BeforeConditionalBody,
"before-conditional-body",
),
(
SafeBoundaryType::AfterConditionalBody,
"after-conditional-body",
),
(
SafeBoundaryType::BeforeLoopIteration,
"before-loop-iteration",
),
(SafeBoundaryType::AfterLoopIteration, "after-loop-iteration"),
(
SafeBoundaryType::AfterBranchTransition,
"after-branch-transition",
),
(SafeBoundaryType::BeforeJoin, "before-join"),
(SafeBoundaryType::AfterJoin, "after-join"),
(SafeBoundaryType::BeforeMatchArm, "before-match-arm"),
(SafeBoundaryType::AfterMatchArm, "after-match-arm"),
];
for (variant, expected) in types {
let json = serde_json::to_string(&variant).unwrap();
assert_eq!(json, format!("\"{}\"", expected));
let deserialized: SafeBoundaryType = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized, variant);
}
}
#[test]
fn test_persisted_run_state_roundtrip() {
let state = PersistedRunState {
schema_version: 1,
run_id: "run-123".to_string(),
timestamp: "2026-04-09T00:00:00Z".to_string(),
root_workflow: "main".to_string(),
status: RunStatus::Completed,
call_stack: vec![CallStackEntry {
workflow: "main".to_string(),
step_index: 0,
}],
steps: {
let mut m = HashMap::new();
m.insert(
"main/deploy".to_string(),
StepStatePersisted {
name: "deploy".to_string(),
kind: StepKind::Exec,
status: StepStatusPersisted::Completed,
exec_meta: Some(ExecMeta {
harness: Some("claude".to_string()),
prompt_ref: None,
args: None,
}),
check_result: None,
failure_details: None,
started_at: Some("2026-04-09T00:00:01Z".to_string()),
completed_at: Some("2026-04-09T00:00:02Z".to_string()),
},
);
m
},
branches: None,
collected_outputs: None,
harness_event_log: None,
safe_boundaries: vec![SafeBoundary {
boundary_type: SafeBoundaryType::BeforeStepStart,
step_path: vec!["main".to_string(), "deploy".to_string()],
timestamp: None,
index: 0,
}],
current_boundary_index: 0,
event_log: None,
};
let json = serde_json::to_string_pretty(&state).unwrap();
let deserialized: PersistedRunState = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.run_id, "run-123");
assert_eq!(deserialized.schema_version, 1);
assert_eq!(deserialized.status, RunStatus::Completed);
assert_eq!(deserialized.call_stack.len(), 1);
assert_eq!(deserialized.steps.len(), 1);
assert_eq!(deserialized.safe_boundaries.len(), 1);
assert_eq!(deserialized.current_boundary_index, 0);
}
#[test]
fn test_persisted_run_state_json_field_names() {
let state = PersistedRunState {
schema_version: 1,
run_id: "r1".to_string(),
timestamp: "t".to_string(),
root_workflow: "main".to_string(),
status: RunStatus::Running,
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 json = serde_json::to_string(&state).unwrap();
assert!(json.contains("\"schemaVersion\""), "json: {}", json);
assert!(json.contains("\"runId\""), "json: {}", json);
assert!(json.contains("\"rootWorkflow\""), "json: {}", json);
assert!(json.contains("\"callStack\""), "json: {}", json);
assert!(json.contains("\"safeBoundaries\""), "json: {}", json);
assert!(json.contains("\"currentBoundaryIndex\""), "json: {}", json);
assert!(!json.contains("\"branches\""), "json: {}", json);
assert!(!json.contains("\"collectedOutputs\""), "json: {}", json);
assert!(!json.contains("\"eventLog\""), "json: {}", json);
}
#[test]
fn test_step_state_persisted_json_field_names() {
let step = StepStatePersisted {
name: "deploy".to_string(),
kind: StepKind::Exec,
status: StepStatusPersisted::InProgress,
exec_meta: Some(ExecMeta {
harness: Some("claude".to_string()),
prompt_ref: Some("prompts/deploy.md".to_string()),
args: None,
}),
check_result: None,
failure_details: None,
started_at: Some("2026-04-09T00:00:00Z".to_string()),
completed_at: None,
};
let json = serde_json::to_string(&step).unwrap();
assert!(json.contains("\"execMeta\""), "json: {}", json);
assert!(json.contains("\"promptRef\""), "json: {}", json);
assert!(json.contains("\"startedAt\""), "json: {}", json);
assert!(!json.contains("\"checkResult\""), "json: {}", json);
assert!(!json.contains("\"failureDetails\""), "json: {}", json);
assert!(!json.contains("\"completedAt\""), "json: {}", json);
}
#[test]
fn test_branch_state_serialization() {
let branch = BranchState {
workflow_name: "branch-a".to_string(),
status: BranchStatusPersisted::Completed,
output: Some(serde_json::json!({"result": "ok"})),
failure_details: None,
};
let json = serde_json::to_string(&branch).unwrap();
assert!(
json.contains("\"workflowName\":\"branch-a\""),
"json: {}",
json
);
assert!(json.contains("\"status\":\"completed\""), "json: {}", json);
}
#[test]
fn test_run_summary_serialization() {
let summary = RunSummary {
run_id: "run-1".to_string(),
timestamp: "2026-04-09T00:00:00Z".to_string(),
root_workflow: "main".to_string(),
status: RunStatus::Completed,
};
let json = serde_json::to_string(&summary).unwrap();
assert!(json.contains("\"runId\""), "json: {}", json);
assert!(json.contains("\"rootWorkflow\""), "json: {}", json);
}
}