use anyhow::Result;
use async_trait::async_trait;
use prodigy::cook::execution::{ClaudeExecutor, ExecutionResult};
use prodigy::cook::interaction::{SpinnerHandle, UserInteraction, VerbosityLevel};
use prodigy::cook::session::state::SessionState;
use prodigy::cook::session::summary::SessionSummary;
use prodigy::cook::session::{SessionInfo, SessionManager, SessionUpdate};
use prodigy::cook::workflow::checkpoint::{
CheckpointManager, CompletedStep, ExecutionState, ResumeOptions, WorkflowCheckpoint,
WorkflowStatus, CHECKPOINT_VERSION,
};
use prodigy::cook::workflow::resume::ResumeExecutor;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use tempfile::TempDir;
struct TestClaudeExecutor {
responses: Arc<Mutex<Vec<ExecutionResult>>>,
}
impl TestClaudeExecutor {
fn new() -> Self {
Self {
responses: Arc::new(Mutex::new(Vec::new())),
}
}
#[allow(dead_code)]
fn add_response(&self, response: ExecutionResult) {
self.responses.lock().unwrap().push(response);
}
}
#[async_trait]
impl ClaudeExecutor for TestClaudeExecutor {
async fn execute_claude_command(
&self,
_command: &str,
_working_dir: &Path,
_env_vars: HashMap<String, String>,
) -> Result<ExecutionResult> {
self.responses
.lock()
.unwrap()
.pop()
.ok_or_else(|| anyhow::anyhow!("No mock response configured"))
}
async fn check_claude_cli(&self) -> Result<bool> {
Ok(true)
}
async fn get_claude_version(&self) -> Result<String> {
Ok("mock-1.0.0".to_string())
}
}
struct TestSessionManager {
updates: Arc<Mutex<Vec<SessionUpdate>>>,
}
impl TestSessionManager {
fn new() -> Self {
Self {
updates: Arc::new(Mutex::new(Vec::new())),
}
}
}
#[async_trait]
impl SessionManager for TestSessionManager {
async fn update_session(&self, update: SessionUpdate) -> Result<()> {
self.updates.lock().unwrap().push(update);
Ok(())
}
async fn start_session(&self, _session_id: &str) -> Result<()> {
Ok(())
}
async fn complete_session(&self) -> Result<SessionSummary> {
Ok(SessionSummary {
iterations: 1,
files_changed: 0,
})
}
fn get_state(&self) -> Result<SessionState> {
Ok(SessionState::new(
"test-session".to_string(),
PathBuf::from("/tmp"),
))
}
async fn save_state(&self, _path: &Path) -> Result<()> {
Ok(())
}
async fn load_state(&self, _path: &Path) -> Result<()> {
Ok(())
}
async fn load_session(&self, _session_id: &str) -> Result<SessionState> {
Ok(SessionState::new(
"test-session".to_string(),
PathBuf::from("/tmp"),
))
}
async fn save_checkpoint(&self, _state: &SessionState) -> Result<()> {
Ok(())
}
async fn list_resumable(&self) -> Result<Vec<SessionInfo>> {
Ok(vec![])
}
async fn get_last_interrupted(&self) -> Result<Option<String>> {
Ok(None)
}
}
struct TestSpinnerHandle;
impl SpinnerHandle for TestSpinnerHandle {
fn update_message(&mut self, _message: &str) {}
fn success(&mut self, _message: &str) {}
fn fail(&mut self, _message: &str) {}
}
struct TestUserInteraction;
#[async_trait]
impl UserInteraction for TestUserInteraction {
async fn prompt_yes_no(&self, _message: &str) -> Result<bool> {
Ok(true)
}
async fn prompt_text(&self, _message: &str, _default: Option<&str>) -> Result<String> {
Ok(String::new())
}
fn display_info(&self, _message: &str) {}
fn display_warning(&self, _message: &str) {}
fn display_error(&self, _message: &str) {}
fn display_progress(&self, _message: &str) {}
fn start_spinner(&self, _message: &str) -> Box<dyn SpinnerHandle> {
Box::new(TestSpinnerHandle)
}
fn display_success(&self, _message: &str) {}
fn display_action(&self, _message: &str) {}
fn display_metric(&self, _label: &str, _value: &str) {}
fn display_status(&self, _message: &str) {}
fn iteration_start(&self, _current: u32, _total: u32) {}
fn iteration_end(&self, _current: u32, _duration: Duration, _success: bool) {}
fn step_start(&self, _step: u32, _total: u32, _description: &str) {}
fn step_end(&self, _step: u32, _success: bool) {}
fn command_output(&self, _output: &str, _verbosity: VerbosityLevel) {}
fn debug_output(&self, _message: &str, _min_verbosity: VerbosityLevel) {}
fn verbosity(&self) -> VerbosityLevel {
VerbosityLevel::Normal
}
}
fn compute_workflow_hash(content: &str) -> String {
use sha2::{Digest, Sha256};
format!("{:x}", Sha256::digest(content.as_bytes()))
}
fn create_test_checkpoint(workflow_id: &str, workflow_path: PathBuf) -> WorkflowCheckpoint {
let workflow_hash = if workflow_path.exists() {
std::fs::read_to_string(&workflow_path)
.map(|content| compute_workflow_hash(&content))
.unwrap_or_else(|_| "test-hash".to_string())
} else {
"test-hash".to_string()
};
WorkflowCheckpoint {
workflow_id: workflow_id.to_string(),
workflow_path: Some(workflow_path),
execution_state: ExecutionState {
current_step_index: 0,
total_steps: 2,
status: WorkflowStatus::Running,
start_time: chrono::Utc::now(),
last_checkpoint: chrono::Utc::now(),
current_iteration: Some(1),
total_iterations: Some(1),
},
completed_steps: vec![],
variable_state: HashMap::new(),
mapreduce_state: None,
timestamp: chrono::Utc::now(),
variable_checkpoint_state: None,
version: CHECKPOINT_VERSION,
workflow_hash,
total_steps: 2,
workflow_name: Some("test-workflow".to_string()),
error_recovery_state: None,
retry_checkpoint_state: None,
}
}
async fn setup_test_environment() -> Result<(TempDir, PathBuf, PathBuf)> {
let temp_dir = TempDir::new()?;
let checkpoint_dir = temp_dir.path().join("checkpoints");
let workflow_path = temp_dir.path().join("test.yml");
tokio::fs::create_dir_all(&checkpoint_dir).await?;
Ok((temp_dir, checkpoint_dir, workflow_path))
}
async fn create_yaml_workflow(path: &Path) -> Result<()> {
let workflow_content = r#"
name: test-workflow
commands:
- shell: echo "step1"
- shell: echo "step2"
"#;
tokio::fs::write(path, workflow_content).await?;
Ok(())
}
async fn create_json_workflow(path: &Path) -> Result<()> {
let workflow_content = r#"{
"name": "test-workflow",
"commands": [
{"shell": "echo step1"},
{"shell": "echo step2"}
]
}"#;
tokio::fs::write(path, workflow_content).await?;
Ok(())
}
#[tokio::test]
async fn test_execute_from_checkpoint_missing_claude_executor() -> Result<()> {
let (_temp_dir, checkpoint_dir, workflow_path) = setup_test_environment().await?;
create_yaml_workflow(&workflow_path).await?;
let checkpoint = create_test_checkpoint("test-workflow", workflow_path.clone());
#[allow(deprecated)]
let checkpoint_manager = Arc::new(CheckpointManager::new(checkpoint_dir));
checkpoint_manager.save_checkpoint(&checkpoint).await?;
let mut executor = ResumeExecutor::new(checkpoint_manager);
let options = ResumeOptions {
skip_validation: false,
force: false,
from_step: None,
reset_failures: false,
};
let result = executor
.execute_from_checkpoint("test-workflow", &workflow_path, options)
.await;
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("Claude executor not configured"),
"Expected error about missing Claude executor, got: {}",
err_msg
);
Ok(())
}
#[tokio::test]
async fn test_execute_from_checkpoint_already_completed() -> Result<()> {
let (_temp_dir, checkpoint_dir, workflow_path) = setup_test_environment().await?;
create_yaml_workflow(&workflow_path).await?;
let mut checkpoint = create_test_checkpoint("test-workflow", workflow_path.clone());
checkpoint.execution_state.status = WorkflowStatus::Completed;
checkpoint.execution_state.current_step_index = 2;
#[allow(deprecated)]
let checkpoint_manager = Arc::new(CheckpointManager::new(checkpoint_dir));
checkpoint_manager.save_checkpoint(&checkpoint).await?;
let claude_executor = Arc::new(TestClaudeExecutor::new());
let session_manager = Arc::new(TestSessionManager::new());
let user_interaction = Arc::new(TestUserInteraction);
let mut executor = ResumeExecutor::new(checkpoint_manager).with_executors(
claude_executor,
session_manager,
user_interaction,
);
let options = ResumeOptions {
skip_validation: false,
force: false,
from_step: None,
reset_failures: false,
};
let result = executor
.execute_from_checkpoint("test-workflow", &workflow_path, options)
.await?;
assert!(result.success);
assert_eq!(result.new_steps_executed, 0);
assert_eq!(result.total_steps_executed, 2);
assert_eq!(result.skipped_steps, 2);
Ok(())
}
#[tokio::test]
async fn test_execute_from_checkpoint_workflow_file_yaml_parsing() -> Result<()> {
let (_temp_dir, checkpoint_dir, workflow_path) = setup_test_environment().await?;
create_yaml_workflow(&workflow_path).await?;
let checkpoint = create_test_checkpoint("test-workflow", workflow_path.clone());
#[allow(deprecated)]
let checkpoint_manager = Arc::new(CheckpointManager::new(checkpoint_dir));
checkpoint_manager.save_checkpoint(&checkpoint).await?;
let claude_executor = Arc::new(TestClaudeExecutor::new());
let session_manager = Arc::new(TestSessionManager::new());
let user_interaction = Arc::new(TestUserInteraction);
claude_executor.add_response(ExecutionResult {
success: true,
stdout: "step2".to_string(),
stderr: String::new(),
exit_code: Some(0),
metadata: HashMap::new(),
});
claude_executor.add_response(ExecutionResult {
success: true,
stdout: "step1".to_string(),
stderr: String::new(),
exit_code: Some(0),
metadata: HashMap::new(),
});
let mut executor = ResumeExecutor::new(checkpoint_manager).with_executors(
claude_executor,
session_manager,
user_interaction,
);
let options = ResumeOptions {
skip_validation: false,
force: false,
from_step: None,
reset_failures: false,
};
let result = executor
.execute_from_checkpoint("test-workflow", &workflow_path, options)
.await;
assert!(
result.is_ok() || result.is_err(),
"Execution should complete (success or controlled failure)"
);
Ok(())
}
#[tokio::test]
async fn test_execute_from_checkpoint_workflow_file_json_parsing() -> Result<()> {
let temp_dir = TempDir::new()?;
let checkpoint_dir = temp_dir.path().join("checkpoints");
let workflow_path = temp_dir.path().join("test.json");
tokio::fs::create_dir_all(&checkpoint_dir).await?;
create_json_workflow(&workflow_path).await?;
let checkpoint = create_test_checkpoint("test-workflow", workflow_path.clone());
#[allow(deprecated)]
let checkpoint_manager = Arc::new(CheckpointManager::new(checkpoint_dir));
checkpoint_manager.save_checkpoint(&checkpoint).await?;
let claude_executor = Arc::new(TestClaudeExecutor::new());
let session_manager = Arc::new(TestSessionManager::new());
let user_interaction = Arc::new(TestUserInteraction);
claude_executor.add_response(ExecutionResult {
success: true,
stdout: "step2".to_string(),
stderr: String::new(),
exit_code: Some(0),
metadata: HashMap::new(),
});
claude_executor.add_response(ExecutionResult {
success: true,
stdout: "step1".to_string(),
stderr: String::new(),
exit_code: Some(0),
metadata: HashMap::new(),
});
let mut executor = ResumeExecutor::new(checkpoint_manager).with_executors(
claude_executor,
session_manager,
user_interaction,
);
let options = ResumeOptions {
skip_validation: false,
force: false,
from_step: None,
reset_failures: false,
};
let result = executor
.execute_from_checkpoint("test-workflow", &workflow_path, options)
.await;
assert!(
result.is_ok() || result.is_err(),
"Execution should complete (success or controlled failure)"
);
Ok(())
}
#[tokio::test]
async fn test_execute_from_checkpoint_invalid_workflow_format() -> Result<()> {
let temp_dir = TempDir::new()?;
let checkpoint_dir = temp_dir.path().join("checkpoints");
let workflow_path = temp_dir.path().join("test.txt");
tokio::fs::create_dir_all(&checkpoint_dir).await?;
tokio::fs::write(&workflow_path, "invalid format").await?;
let checkpoint = create_test_checkpoint("test-workflow", workflow_path.clone());
#[allow(deprecated)]
let checkpoint_manager = Arc::new(CheckpointManager::new(checkpoint_dir));
checkpoint_manager.save_checkpoint(&checkpoint).await?;
let claude_executor = Arc::new(TestClaudeExecutor::new());
let session_manager = Arc::new(TestSessionManager::new());
let user_interaction = Arc::new(TestUserInteraction);
let mut executor = ResumeExecutor::new(checkpoint_manager).with_executors(
claude_executor,
session_manager,
user_interaction,
);
let options = ResumeOptions {
skip_validation: false,
force: false,
from_step: None,
reset_failures: false,
};
let result = executor
.execute_from_checkpoint("test-workflow", &workflow_path, options)
.await;
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("Unsupported workflow file format"),
"Expected unsupported format error, got: {}",
err_msg
);
Ok(())
}
#[tokio::test]
async fn test_execute_from_checkpoint_restore_context() -> Result<()> {
let (_temp_dir, checkpoint_dir, workflow_path) = setup_test_environment().await?;
create_yaml_workflow(&workflow_path).await?;
let mut checkpoint = create_test_checkpoint("test-workflow", workflow_path.clone());
checkpoint
.variable_state
.insert("test_var".to_string(), serde_json::json!("test_value"));
checkpoint.completed_steps.push(CompletedStep {
step_index: 0,
command: "shell: echo step1".to_string(),
success: true,
output: Some("step1".to_string()),
captured_variables: HashMap::new(),
duration: std::time::Duration::from_secs(1),
completed_at: chrono::Utc::now(),
retry_state: None,
});
checkpoint.execution_state.current_step_index = 1;
#[allow(deprecated)]
let checkpoint_manager = Arc::new(CheckpointManager::new(checkpoint_dir));
checkpoint_manager.save_checkpoint(&checkpoint).await?;
let claude_executor = Arc::new(TestClaudeExecutor::new());
let session_manager = Arc::new(TestSessionManager::new());
let user_interaction = Arc::new(TestUserInteraction);
claude_executor.add_response(ExecutionResult {
success: true,
stdout: "step2".to_string(),
stderr: String::new(),
exit_code: Some(0),
metadata: HashMap::new(),
});
let mut executor = ResumeExecutor::new(checkpoint_manager).with_executors(
claude_executor,
session_manager,
user_interaction,
);
let options = ResumeOptions {
skip_validation: false,
force: false,
from_step: None,
reset_failures: false,
};
let result = executor
.execute_from_checkpoint("test-workflow", &workflow_path, options)
.await;
assert!(
result.is_ok() || result.is_err(),
"Execution should handle context restoration"
);
if let Ok(resume_result) = result {
assert_eq!(resume_result.skipped_steps, 1);
}
Ok(())
}