#![allow(non_snake_case)]
use serde::{Deserialize, Serialize};
pub type RunId = String;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum StepStatus {
Pending,
InProgress,
Completed,
Failed,
}
pub type StepPath = Vec<String>;
#[derive(Debug, Clone)]
pub struct HarnessExecContext {
pub run_id: RunId,
pub step_path: StepPath,
pub exec_ordinal: usize,
}
pub mod safe_boundary_types {
pub const BEFORE_STEP_START: &str = "before-step-start";
pub const AFTER_STEP_COMPLETE: &str = "after-step-complete";
pub const BEFORE_CONDITIONAL_BODY: &str = "before-conditional-body";
pub const AFTER_CONDITIONAL_BODY: &str = "after-conditional-body";
pub const BEFORE_LOOP_ITERATION: &str = "before-loop-iteration";
pub const AFTER_LOOP_ITERATION: &str = "after-loop-iteration";
pub const AFTER_BRANCH_TRANSITION: &str = "after-branch-transition";
pub const BEFORE_JOIN: &str = "before-join";
pub const AFTER_JOIN: &str = "after-join";
pub const BEFORE_MATCH_ARM: &str = "before-match-arm";
pub const AFTER_MATCH_ARM: &str = "after-match-arm";
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum ExecutionEvent {
StepStarted {
runId: RunId,
stepPath: StepPath,
},
StepCompleted {
runId: RunId,
stepPath: StepPath,
},
StepFailed {
runId: RunId,
stepPath: StepPath,
error: String,
},
CheckEvaluated {
runId: RunId,
checkName: String,
result: bool,
#[serde(skip_serializing_if = "Option::is_none")]
reason: Option<String>,
},
MatchEvaluated {
runId: RunId,
checkName: String,
variant: String,
#[serde(skip_serializing_if = "Option::is_none")]
reason: Option<String>,
armIndex: Option<i64>,
},
BranchStarted {
runId: RunId,
branchPath: StepPath,
},
BranchCompleted {
runId: RunId,
branchPath: StepPath,
#[serde(skip_serializing_if = "Option::is_none")]
output: Option<serde_json::Value>,
},
BranchFailed {
runId: RunId,
branchPath: StepPath,
error: String,
},
JoinStarted {
runId: RunId,
joinWorkflow: String,
},
RunPaused {
runId: RunId,
position: StepPath,
},
RunCompleted {
runId: RunId,
},
RunFailed {
runId: RunId,
position: StepPath,
error: String,
},
SafeBoundary {
runId: RunId,
boundaryType: String,
stepPath: StepPath,
},
}
impl ExecutionEvent {
pub fn event_type(&self) -> &'static str {
match self {
ExecutionEvent::StepStarted { .. } => "StepStarted",
ExecutionEvent::StepCompleted { .. } => "StepCompleted",
ExecutionEvent::StepFailed { .. } => "StepFailed",
ExecutionEvent::CheckEvaluated { .. } => "CheckEvaluated",
ExecutionEvent::MatchEvaluated { .. } => "MatchEvaluated",
ExecutionEvent::BranchStarted { .. } => "BranchStarted",
ExecutionEvent::BranchCompleted { .. } => "BranchCompleted",
ExecutionEvent::BranchFailed { .. } => "BranchFailed",
ExecutionEvent::JoinStarted { .. } => "JoinStarted",
ExecutionEvent::RunPaused { .. } => "RunPaused",
ExecutionEvent::RunCompleted { .. } => "RunCompleted",
ExecutionEvent::RunFailed { .. } => "RunFailed",
ExecutionEvent::SafeBoundary { .. } => "SafeBoundary",
}
}
}
#[derive(Debug, Clone)]
pub struct BranchOutput {
pub workflow: String,
pub output: Option<String>,
}
pub type HarnessDispatchFn = std::sync::Arc<
dyn Fn(
&crate::parser::ast::ExecBlock,
HarnessExecContext,
) -> std::pin::Pin<
Box<
dyn std::future::Future<Output = Result<crate::harness::types::ExecResult, String>>
+ Send,
>,
> + Send
+ Sync,
>;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StepState {
pub stepPath: StepPath,
pub status: StepStatus,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum RunStatus {
Running,
Paused,
Completed,
Failed,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunState {
pub runId: RunId,
pub rootWorkflow: String,
pub status: RunStatus,
pub steps: Vec<StepState>,
pub events: Vec<ExecutionEvent>,
pub safeBoundaries: Vec<usize>,
pub lastSafeBoundaryIndex: i64,
}
pub type OnEventCallback = Box<dyn Fn(&ExecutionEvent) + Send + Sync>;
pub type OnSaveCallback = Box<dyn Fn(&RunState) + Send + Sync>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_step_status_serialization() {
assert_eq!(
serde_json::to_string(&StepStatus::Pending).unwrap(),
"\"pending\""
);
assert_eq!(
serde_json::to_string(&StepStatus::InProgress).unwrap(),
"\"in_progress\""
);
assert_eq!(
serde_json::to_string(&StepStatus::Completed).unwrap(),
"\"completed\""
);
assert_eq!(
serde_json::to_string(&StepStatus::Failed).unwrap(),
"\"failed\""
);
}
#[test]
fn test_step_status_deserialization() {
let pending: StepStatus = serde_json::from_str("\"pending\"").unwrap();
assert_eq!(pending, StepStatus::Pending);
let in_progress: StepStatus = serde_json::from_str("\"in_progress\"").unwrap();
assert_eq!(in_progress, StepStatus::InProgress);
}
#[test]
fn test_run_status_serialization() {
assert_eq!(
serde_json::to_string(&RunStatus::Running).unwrap(),
"\"running\""
);
assert_eq!(
serde_json::to_string(&RunStatus::Paused).unwrap(),
"\"paused\""
);
assert_eq!(
serde_json::to_string(&RunStatus::Completed).unwrap(),
"\"completed\""
);
assert_eq!(
serde_json::to_string(&RunStatus::Failed).unwrap(),
"\"failed\""
);
}
#[test]
fn test_execution_event_step_started_json() {
let event = ExecutionEvent::StepStarted {
runId: "run-1".to_string(),
stepPath: vec!["main".to_string(), "deploy".to_string()],
};
assert_eq!(event.event_type(), "StepStarted");
let json = serde_json::to_string(&event).unwrap();
assert!(json.contains("\"type\":\"StepStarted\""), "json: {}", json);
assert!(json.contains("\"runId\":\"run-1\""), "json: {}", json);
assert!(
json.contains("\"stepPath\":[\"main\",\"deploy\"]"),
"json: {}",
json
);
}
#[test]
fn test_execution_event_step_failed_json() {
let event = ExecutionEvent::StepFailed {
runId: "run-1".to_string(),
stepPath: vec!["main".to_string()],
error: "something broke".to_string(),
};
assert_eq!(event.event_type(), "StepFailed");
let json = serde_json::to_string(&event).unwrap();
assert!(
json.contains("\"error\":\"something broke\""),
"json: {}",
json
);
}
#[test]
fn test_execution_event_check_evaluated_with_reason() {
let event = ExecutionEvent::CheckEvaluated {
runId: "run-1".to_string(),
checkName: "is-ready".to_string(),
result: true,
reason: Some("all checks passed".to_string()),
};
assert_eq!(event.event_type(), "CheckEvaluated");
let json = serde_json::to_string(&event).unwrap();
assert!(
json.contains("\"checkName\":\"is-ready\""),
"json: {}",
json
);
assert!(json.contains("\"result\":true"), "json: {}", json);
assert!(
json.contains("\"reason\":\"all checks passed\""),
"json: {}",
json
);
}
#[test]
fn test_execution_event_check_evaluated_without_reason() {
let event = ExecutionEvent::CheckEvaluated {
runId: "run-1".to_string(),
checkName: "is-ready".to_string(),
result: false,
reason: None,
};
let json = serde_json::to_string(&event).unwrap();
assert!(
!json.contains("reason"),
"reason should be omitted: {}",
json
);
}
#[test]
fn test_execution_event_branch_completed_with_output() {
let event = ExecutionEvent::BranchCompleted {
runId: "run-1".to_string(),
branchPath: vec!["par".to_string(), "branch-a".to_string()],
output: Some(serde_json::json!({"status": "ok"})),
};
assert_eq!(event.event_type(), "BranchCompleted");
let json = serde_json::to_string(&event).unwrap();
assert!(
json.contains("\"output\":{\"status\":\"ok\"}"),
"json: {}",
json
);
}
#[test]
fn test_execution_event_branch_completed_without_output() {
let event = ExecutionEvent::BranchCompleted {
runId: "run-1".to_string(),
branchPath: vec!["par".to_string()],
output: None,
};
let json = serde_json::to_string(&event).unwrap();
assert!(
!json.contains("output"),
"output should be omitted: {}",
json
);
}
#[test]
fn test_execution_event_safe_boundary_json() {
let event = ExecutionEvent::SafeBoundary {
runId: "run-1".to_string(),
boundaryType: safe_boundary_types::BEFORE_STEP_START.to_string(),
stepPath: vec!["main".to_string()],
};
assert_eq!(event.event_type(), "SafeBoundary");
let json = serde_json::to_string(&event).unwrap();
assert!(
json.contains("\"boundaryType\":\"before-step-start\""),
"json: {}",
json
);
}
#[test]
fn test_execution_event_run_completed() {
let event = ExecutionEvent::RunCompleted {
runId: "run-1".to_string(),
};
assert_eq!(event.event_type(), "RunCompleted");
let json = serde_json::to_string(&event).unwrap();
assert!(json.contains("\"type\":\"RunCompleted\""), "json: {}", json);
assert!(json.contains("\"runId\":\"run-1\""), "json: {}", json);
}
#[test]
fn test_execution_event_all_types() {
let events: Vec<ExecutionEvent> = vec![
ExecutionEvent::StepStarted {
runId: "r".to_string(),
stepPath: vec![],
},
ExecutionEvent::StepCompleted {
runId: "r".to_string(),
stepPath: vec![],
},
ExecutionEvent::StepFailed {
runId: "r".to_string(),
stepPath: vec![],
error: "e".to_string(),
},
ExecutionEvent::CheckEvaluated {
runId: "r".to_string(),
checkName: "c".to_string(),
result: true,
reason: None,
},
ExecutionEvent::MatchEvaluated {
runId: "r".to_string(),
checkName: "c".to_string(),
variant: "v".to_string(),
reason: None,
armIndex: None,
},
ExecutionEvent::BranchStarted {
runId: "r".to_string(),
branchPath: vec![],
},
ExecutionEvent::BranchCompleted {
runId: "r".to_string(),
branchPath: vec![],
output: None,
},
ExecutionEvent::BranchFailed {
runId: "r".to_string(),
branchPath: vec![],
error: "e".to_string(),
},
ExecutionEvent::JoinStarted {
runId: "r".to_string(),
joinWorkflow: "w".to_string(),
},
ExecutionEvent::RunPaused {
runId: "r".to_string(),
position: vec![],
},
ExecutionEvent::RunCompleted {
runId: "r".to_string(),
},
ExecutionEvent::RunFailed {
runId: "r".to_string(),
position: vec![],
error: "e".to_string(),
},
ExecutionEvent::SafeBoundary {
runId: "r".to_string(),
boundaryType: "before-step-start".to_string(),
stepPath: vec![],
},
];
let expected_types = [
"StepStarted",
"StepCompleted",
"StepFailed",
"CheckEvaluated",
"MatchEvaluated",
"BranchStarted",
"BranchCompleted",
"BranchFailed",
"JoinStarted",
"RunPaused",
"RunCompleted",
"RunFailed",
"SafeBoundary",
];
for (event, expected) in events.iter().zip(expected_types.iter()) {
assert_eq!(event.event_type(), *expected);
}
}
#[test]
fn test_step_state_serialization() {
let state = StepState {
stepPath: vec!["main".to_string(), "step-1".to_string()],
status: StepStatus::Completed,
};
let json = serde_json::to_string(&state).unwrap();
assert!(
json.contains("\"stepPath\":[\"main\",\"step-1\"]"),
"json: {}",
json
);
assert!(json.contains("\"status\":\"completed\""), "json: {}", json);
}
#[test]
fn test_run_state_serialization() {
let state = RunState {
runId: "run-123".to_string(),
rootWorkflow: "main".to_string(),
status: RunStatus::Running,
steps: vec![StepState {
stepPath: vec!["main".to_string()],
status: StepStatus::Pending,
}],
events: vec![],
safeBoundaries: vec![],
lastSafeBoundaryIndex: -1,
};
let json = serde_json::to_string(&state).unwrap();
assert!(json.contains("\"runId\":\"run-123\""), "json: {}", json);
assert!(json.contains("\"rootWorkflow\":\"main\""), "json: {}", json);
assert!(json.contains("\"status\":\"running\""), "json: {}", json);
assert!(
json.contains("\"lastSafeBoundaryIndex\":-1"),
"json: {}",
json
);
}
#[test]
fn test_run_state_roundtrip() {
let state = RunState {
runId: "run-abc".to_string(),
rootWorkflow: "deploy".to_string(),
status: RunStatus::Completed,
steps: vec![
StepState {
stepPath: vec!["deploy".to_string(), "build".to_string()],
status: StepStatus::Completed,
},
StepState {
stepPath: vec!["deploy".to_string(), "test".to_string()],
status: StepStatus::Failed,
},
],
events: vec![
ExecutionEvent::StepStarted {
runId: "run-abc".to_string(),
stepPath: vec!["deploy".to_string(), "build".to_string()],
},
ExecutionEvent::SafeBoundary {
runId: "run-abc".to_string(),
boundaryType: safe_boundary_types::AFTER_STEP_COMPLETE.to_string(),
stepPath: vec!["deploy".to_string(), "build".to_string()],
},
],
safeBoundaries: vec![1],
lastSafeBoundaryIndex: 1,
};
let json = serde_json::to_string(&state).unwrap();
let deserialized: RunState = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.runId, "run-abc");
assert_eq!(deserialized.rootWorkflow, "deploy");
assert_eq!(deserialized.status, RunStatus::Completed);
assert_eq!(deserialized.steps.len(), 2);
assert_eq!(deserialized.events.len(), 2);
assert_eq!(deserialized.safeBoundaries, vec![1]);
assert_eq!(deserialized.lastSafeBoundaryIndex, 1);
}
#[test]
fn test_safe_boundary_type_constants() {
assert_eq!(safe_boundary_types::BEFORE_STEP_START, "before-step-start");
assert_eq!(
safe_boundary_types::AFTER_STEP_COMPLETE,
"after-step-complete"
);
assert_eq!(
safe_boundary_types::BEFORE_CONDITIONAL_BODY,
"before-conditional-body"
);
assert_eq!(
safe_boundary_types::AFTER_CONDITIONAL_BODY,
"after-conditional-body"
);
assert_eq!(
safe_boundary_types::BEFORE_LOOP_ITERATION,
"before-loop-iteration"
);
assert_eq!(
safe_boundary_types::AFTER_LOOP_ITERATION,
"after-loop-iteration"
);
assert_eq!(
safe_boundary_types::AFTER_BRANCH_TRANSITION,
"after-branch-transition"
);
assert_eq!(safe_boundary_types::BEFORE_JOIN, "before-join");
assert_eq!(safe_boundary_types::AFTER_JOIN, "after-join");
assert_eq!(safe_boundary_types::BEFORE_MATCH_ARM, "before-match-arm");
assert_eq!(safe_boundary_types::AFTER_MATCH_ARM, "after-match-arm");
}
#[test]
fn test_match_evaluated_with_reason() {
let event = ExecutionEvent::MatchEvaluated {
runId: "r".to_string(),
checkName: "size-check".to_string(),
variant: "small".to_string(),
reason: Some("under 50 lines".to_string()),
armIndex: Some(0),
};
assert_eq!(event.event_type(), "MatchEvaluated");
let json = serde_json::to_string(&event).unwrap();
assert!(json.contains("\"variant\":\"small\""));
assert!(json.contains("\"reason\":\"under 50 lines\""));
assert!(json.contains("\"armIndex\":0"));
}
#[test]
fn test_match_evaluated_else_arm() {
let event = ExecutionEvent::MatchEvaluated {
runId: "r".to_string(),
checkName: "size-check".to_string(),
variant: "unknown".to_string(),
reason: None,
armIndex: None,
};
let json = serde_json::to_string(&event).unwrap();
assert!(
!json.contains("reason"),
"reason should be omitted: {}",
json
);
assert!(
json.contains("\"armIndex\":null"),
"armIndex should be null: {}",
json
);
}
#[test]
fn test_execution_event_deserialization() {
let json = r#"{"type":"StepStarted","runId":"run-1","stepPath":["main"]}"#;
let event: ExecutionEvent = serde_json::from_str(json).unwrap();
assert_eq!(event.event_type(), "StepStarted");
match event {
ExecutionEvent::StepStarted { runId, stepPath } => {
assert_eq!(runId, "run-1");
assert_eq!(stepPath, vec!["main".to_string()]);
}
_ => panic!("wrong variant"),
}
}
#[test]
fn test_join_started_serialization() {
let event = ExecutionEvent::JoinStarted {
runId: "r".to_string(),
joinWorkflow: "merge-results".to_string(),
};
let json = serde_json::to_string(&event).unwrap();
assert!(
json.contains("\"joinWorkflow\":\"merge-results\""),
"json: {}",
json
);
}
#[test]
fn test_run_paused_serialization() {
let event = ExecutionEvent::RunPaused {
runId: "r".to_string(),
position: vec!["main".to_string(), "step-3".to_string()],
};
let json = serde_json::to_string(&event).unwrap();
assert!(
json.contains("\"position\":[\"main\",\"step-3\"]"),
"json: {}",
json
);
}
#[test]
fn test_run_failed_serialization() {
let event = ExecutionEvent::RunFailed {
runId: "r".to_string(),
position: vec!["main".to_string()],
error: "timeout".to_string(),
};
let json = serde_json::to_string(&event).unwrap();
assert!(json.contains("\"type\":\"RunFailed\""), "json: {}", json);
assert!(json.contains("\"error\":\"timeout\""), "json: {}", json);
}
#[test]
fn test_branch_failed_serialization() {
let event = ExecutionEvent::BranchFailed {
runId: "r".to_string(),
branchPath: vec!["par".to_string(), "b1".to_string()],
error: "branch error".to_string(),
};
let json = serde_json::to_string(&event).unwrap();
assert!(json.contains("\"type\":\"BranchFailed\""), "json: {}", json);
assert!(
json.contains("\"branchPath\":[\"par\",\"b1\"]"),
"json: {}",
json
);
}
#[test]
fn test_branch_started_serialization() {
let event = ExecutionEvent::BranchStarted {
runId: "r".to_string(),
branchPath: vec!["par".to_string(), "b1".to_string()],
};
let json = serde_json::to_string(&event).unwrap();
assert!(
json.contains("\"type\":\"BranchStarted\""),
"json: {}",
json
);
}
}