use crate::cook::workflow::checkpoint::{WorkflowCheckpoint, WorkflowStatus};
#[derive(Debug, Clone)]
pub struct ResumePlan {
pub start_index: usize,
pub retry_failed: bool,
pub skip_indices: Vec<usize>,
pub restore_variables: bool,
pub warnings: Vec<String>,
}
pub fn plan_resume(checkpoint: &WorkflowCheckpoint) -> ResumePlan {
let completed_indices: Vec<usize> = checkpoint
.completed_steps
.iter()
.map(|s| s.step_index)
.collect();
let start_index = checkpoint.execution_state.current_step_index;
let retry_failed = matches!(
checkpoint.execution_state.status,
WorkflowStatus::Failed | WorkflowStatus::Interrupted
);
let mut warnings = Vec::new();
if retry_failed {
warnings.push(format!(
"Resuming from step {}. Verify step is safe to retry.",
start_index
));
}
ResumePlan {
start_index,
retry_failed,
skip_indices: completed_indices,
restore_variables: true,
warnings,
}
}
pub fn validate_checkpoint_compatibility(
checkpoint: &WorkflowCheckpoint,
workflow_hash: &str,
total_steps: usize,
) -> Result<(), String> {
if checkpoint.workflow_hash != workflow_hash {
return Err(format!(
"Workflow has changed since checkpoint (hash mismatch). \
Checkpoint has {} steps, current workflow has {} steps.",
checkpoint.total_steps, total_steps
));
}
if checkpoint.execution_state.current_step_index > total_steps {
return Err(format!(
"Checkpoint step index {} exceeds workflow steps {}",
checkpoint.execution_state.current_step_index, total_steps
));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cook::workflow::checkpoint::{CompletedStep, ExecutionState};
use chrono::Utc;
use std::collections::HashMap;
use std::time::Duration;
fn create_test_checkpoint(
current_step: usize,
status: WorkflowStatus,
completed: Vec<usize>,
workflow_hash: &str,
total_steps: usize,
) -> WorkflowCheckpoint {
let completed_steps = completed
.into_iter()
.map(|idx| CompletedStep {
step_index: idx,
command: format!("step-{}", idx),
success: true,
output: None,
captured_variables: HashMap::new(),
duration: Duration::from_millis(100),
completed_at: Utc::now(),
retry_state: None,
})
.collect();
WorkflowCheckpoint {
workflow_id: "test-workflow".to_string(),
execution_state: ExecutionState {
current_step_index: current_step,
total_steps,
status,
start_time: Utc::now(),
last_checkpoint: Utc::now(),
current_iteration: None,
total_iterations: None,
},
completed_steps,
variable_state: HashMap::new(),
mapreduce_state: None,
timestamp: Utc::now(),
version: 1,
workflow_hash: workflow_hash.to_string(),
total_steps,
workflow_name: Some("test".to_string()),
workflow_path: None,
error_recovery_state: None,
retry_checkpoint_state: None,
variable_checkpoint_state: None,
}
}
#[test]
fn test_plan_resume_from_failed() {
let checkpoint = create_test_checkpoint(2, WorkflowStatus::Failed, vec![0, 1], "hash", 5);
let plan = plan_resume(&checkpoint);
assert_eq!(plan.start_index, 2);
assert!(plan.retry_failed);
assert_eq!(plan.skip_indices, vec![0, 1]);
assert!(plan.restore_variables);
assert!(!plan.warnings.is_empty());
}
#[test]
fn test_plan_resume_from_interrupted() {
let checkpoint =
create_test_checkpoint(3, WorkflowStatus::Interrupted, vec![0, 1, 2], "hash", 5);
let plan = plan_resume(&checkpoint);
assert_eq!(plan.start_index, 3);
assert!(plan.retry_failed);
assert_eq!(plan.skip_indices, vec![0, 1, 2]);
}
#[test]
fn test_plan_resume_from_paused() {
let checkpoint = create_test_checkpoint(1, WorkflowStatus::Paused, vec![0], "hash", 5);
let plan = plan_resume(&checkpoint);
assert_eq!(plan.start_index, 1);
assert!(!plan.retry_failed);
assert_eq!(plan.skip_indices, vec![0]);
}
#[test]
fn test_plan_resume_no_completed_steps() {
let checkpoint = create_test_checkpoint(0, WorkflowStatus::Failed, vec![], "hash", 5);
let plan = plan_resume(&checkpoint);
assert_eq!(plan.start_index, 0);
assert!(plan.retry_failed);
assert_eq!(plan.skip_indices, Vec::<usize>::new());
}
#[test]
fn test_validate_checkpoint_compatibility_success() {
let checkpoint = create_test_checkpoint(2, WorkflowStatus::Running, vec![0, 1], "hash", 5);
let result = validate_checkpoint_compatibility(&checkpoint, "hash", 5);
assert!(result.is_ok());
}
#[test]
fn test_validate_checkpoint_compatibility_hash_mismatch() {
let checkpoint =
create_test_checkpoint(2, WorkflowStatus::Running, vec![0, 1], "old-hash", 5);
let result = validate_checkpoint_compatibility(&checkpoint, "new-hash", 5);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.contains("hash mismatch"));
}
#[test]
fn test_validate_checkpoint_compatibility_step_overflow() {
let checkpoint = create_test_checkpoint(10, WorkflowStatus::Running, vec![0, 1], "hash", 8);
let result = validate_checkpoint_compatibility(&checkpoint, "hash", 5);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.contains("exceeds workflow steps"));
}
#[test]
fn test_validate_checkpoint_compatibility_different_step_count() {
let checkpoint = create_test_checkpoint(2, WorkflowStatus::Running, vec![0, 1], "hash", 5);
let result = validate_checkpoint_compatibility(&checkpoint, "hash", 10);
assert!(result.is_ok());
}
}