use super::test_utils::*;
use serde_json::json;
use std::fs;
use std::path::{Path, PathBuf};
use tempfile::TempDir;
fn setup_test_prodigy_home() -> TempDir {
TempDir::new().expect("Failed to create temp directory for PRODIGY_HOME")
}
fn create_test_checkpoint(
prodigy_home: &Path,
workflow_id: &str,
commands_executed: usize,
total_commands: usize,
variables: serde_json::Value,
) {
let session_dir = prodigy_home
.join("state")
.join(workflow_id)
.join("checkpoints");
fs::create_dir_all(&session_dir).expect("Failed to create checkpoint directory");
let worktree_dir = prodigy_home
.join("worktrees")
.join("prodigy") .join(workflow_id);
fs::create_dir_all(&worktree_dir).expect("Failed to create worktree directory");
std::process::Command::new("git")
.arg("init")
.current_dir(&worktree_dir)
.output()
.expect("Failed to initialize git repository in worktree");
let now = chrono::Utc::now();
let checkpoint = json!({
"workflow_id": workflow_id,
"execution_state": {
"current_step_index": commands_executed,
"total_steps": total_commands,
"status": "Interrupted",
"start_time": now.to_rfc3339(),
"last_checkpoint": now.to_rfc3339(),
"current_iteration": null,
"total_iterations": null
},
"completed_steps": (0..commands_executed).map(|i| {
json!({
"step_index": i,
"command": format!("shell: echo 'Command {}'", i + 1),
"success": true,
"output": format!("Command {} output", i + 1),
"captured_variables": {},
"duration": {
"secs": 1,
"nanos": 0
},
"completed_at": now.to_rfc3339(),
"retry_state": null
})
}).collect::<Vec<_>>(),
"variable_state": variables,
"mapreduce_state": null,
"timestamp": now.to_rfc3339(),
"version": 1,
"workflow_hash": "test-hash-12345",
"total_steps": total_commands,
"workflow_name": "test-resume-workflow",
"workflow_path": "test-resume-workflow.yaml"
});
let checkpoint_file = session_dir.join(format!("{}.checkpoint.json", workflow_id));
fs::write(
&checkpoint_file,
serde_json::to_string_pretty(&checkpoint).unwrap(),
)
.expect("Failed to write checkpoint file");
let unified_session = json!({
"id": workflow_id,
"session_type": "Workflow",
"status": "Paused", "started_at": now.to_rfc3339(),
"updated_at": now.to_rfc3339(),
"completed_at": null,
"metadata": {},
"checkpoints": [],
"timings": {},
"error": null,
"workflow_data": {
"workflow_id": workflow_id,
"workflow_name": "test-resume-workflow",
"current_step": commands_executed,
"total_steps": total_commands,
"completed_steps": (0..commands_executed).collect::<Vec<_>>(),
"variables": {},
"iterations_completed": 0,
"files_changed": 0,
"worktree_name": workflow_id
},
"mapreduce_data": null
});
let sessions_dir = prodigy_home.join("sessions");
fs::create_dir_all(&sessions_dir).expect("Failed to create sessions directory");
fs::write(
sessions_dir.join(format!("{}.json", workflow_id)),
serde_json::to_string_pretty(&unified_session).unwrap(),
)
.expect("Failed to write session file");
}
fn create_test_workflow(workflow_dir: &Path, filename: &str) -> PathBuf {
let workflow_content = r#"
name: test-resume-workflow
description: Test workflow for resume functionality
commands:
- shell: "echo 'Command 1 executed'"
id: cmd1
- shell: "echo 'Command 2 executed'"
id: cmd2
- shell: "echo 'Command 3 executed'"
id: cmd3
- shell: "echo 'Command 4 executed'"
id: cmd4
- shell: "echo 'Final command executed'"
id: cmd5
"#;
fs::create_dir_all(workflow_dir).unwrap();
let workflow_path = workflow_dir.join(filename);
fs::write(&workflow_path, workflow_content).unwrap();
workflow_path
}
#[test]
fn test_resume_from_early_interruption() {
let prodigy_home = setup_test_prodigy_home();
let mut test = CliTest::new();
let test_dir = test.temp_path().to_path_buf();
let _workflow_path = create_test_workflow(&test_dir, "test-resume-workflow.yaml");
let workflow_id = "session-resume-early-12345";
let variables = json!({
"variable1": "test-value",
"shell": {
"output": "Command 1 output"
}
});
let _worktree_path = create_test_checkpoint_with_worktree(
prodigy_home.path(),
&test_dir,
workflow_id,
1, 5, variables,
)
.expect("Failed to create test checkpoint with worktree");
let checkpoint_dir = prodigy_home
.path()
.join("state")
.join(workflow_id)
.join("checkpoints");
let checkpoint_file = checkpoint_dir.join(format!("{}.checkpoint.json", workflow_id));
assert!(
checkpoint_file.exists(),
"Checkpoint file should exist at {:?}",
checkpoint_file
);
test = test
.env("PRODIGY_HOME", prodigy_home.path().to_str().unwrap())
.arg("resume")
.arg(workflow_id)
.arg("--path")
.arg(test_dir.to_str().unwrap());
let output = test.run();
if output.exit_code != exit_codes::SUCCESS {
eprintln!("Resume failed!");
eprintln!("Exit code: {}", output.exit_code);
eprintln!("STDOUT:\n{}", output.stdout);
eprintln!("STDERR:\n{}", output.stderr);
}
assert_eq!(
output.exit_code,
exit_codes::SUCCESS,
"Resume failed with stderr: {}",
output.stderr
);
assert!(
output.stdout_contains("Resuming session:")
|| output.stdout_contains("Resuming workflow from checkpoint")
|| output.stdout_contains("Resuming from iteration"),
"Expected resume message not found in stdout: {}",
output.stdout
);
assert!(
output.stdout_contains("Resumed session completed successfully")
|| output.stdout_contains("Session complete"),
"Expected completion message not found in stdout: {}",
output.stdout
);
}
#[test]
fn test_resume_from_middle_interruption() {
let prodigy_home = setup_test_prodigy_home();
let mut test = CliTest::new();
let test_dir = test.temp_path().to_path_buf();
let _workflow_path = create_test_workflow(&test_dir, "test-resume-workflow.yaml");
let workflow_id = "session-resume-middle-67890";
let variables = json!({
"variable1": "test-value",
"shell": {
"output": "Command 3 output"
},
"cmd1_output": "Command 1 completed",
"cmd2_output": "Command 2 completed"
});
let _worktree_path = create_test_checkpoint_with_worktree(
prodigy_home.path(),
&test_dir,
workflow_id,
3, 5, variables,
)
.expect("Failed to create test checkpoint with worktree");
test = test
.env("PRODIGY_HOME", prodigy_home.path().to_str().unwrap())
.arg("resume")
.arg(workflow_id)
.arg("--path")
.arg(test_dir.to_str().unwrap());
let output = test.run();
assert_eq!(
output.exit_code,
exit_codes::SUCCESS,
"Resume failed with stderr: {}",
output.stderr
);
assert!(
output.stdout_contains("Resuming session:")
|| output.stdout_contains("Resuming workflow from checkpoint")
|| output.stdout_contains("Resuming from iteration"),
"Expected resume message not found in stdout: {}",
output.stdout
);
assert!(
output.stdout_contains("Resumed session completed successfully")
|| output.stdout_contains("Session complete"),
"Expected completion message not found in stdout: {}",
output.stdout
);
}
#[test]
fn test_resume_with_variable_preservation() {
let prodigy_home_dir = setup_test_prodigy_home();
let mut test = CliTest::new();
let test_dir = test.temp_path().to_path_buf();
let workflow_content = r#"
name: test-variable-workflow
description: Test workflow with variables
commands:
- shell: "echo 'Setting up variables'"
capture_output: var1
- shell: "echo 'Using ${var1}'"
capture_output: var2
- shell: "echo 'Final: ${var1} and ${var2}'"
"#;
let workflow_path = test_dir.join("test-resume-workflow.yaml");
fs::write(&workflow_path, workflow_content).unwrap();
let workflow_id = "session-resume-vars-11111";
let variables = json!({
"var1": "First variable value",
"var2": "Second variable value",
"shell": {
"output": "Previous command output"
}
});
let prodigy_home = prodigy_home_dir.path().to_path_buf();
let _worktree_path = create_test_checkpoint_with_worktree(
&prodigy_home,
&test_dir,
workflow_id,
2, 3, variables,
)
.expect("Failed to create test checkpoint with worktree");
test = test
.env("PRODIGY_HOME", prodigy_home.to_str().unwrap())
.arg("resume")
.arg(workflow_id)
.arg("--path")
.arg(test_dir.to_str().unwrap());
let output = test.run();
assert_eq!(
output.exit_code,
exit_codes::SUCCESS,
"Resume should succeed. Stdout: {}\nStderr: {}",
output.stdout,
output.stderr
);
assert!(
output.stdout_contains("Resumed session completed successfully")
|| output.stdout_contains("Session complete")
|| output.stdout_contains("completed"),
"Expected completion message. Stdout: {}",
output.stdout
);
}
#[test]
fn test_resume_with_retry_state() {
let prodigy_home_dir = setup_test_prodigy_home();
let mut test = CliTest::new();
let test_dir = test.temp_path().to_path_buf();
let workflow_content = r#"
name: test-retry-workflow
description: Test workflow with retries
commands:
- shell: "echo 'Command 1'"
- shell: "test -f /tmp/retry-test-marker || exit 1"
retry: 3
id: retry_command
- shell: "echo 'Success after retry'"
"#;
let workflow_path = test_dir.join("test-resume-workflow.yaml");
fs::write(&workflow_path, workflow_content).unwrap();
let workflow_id = "session-resume-retry-22222";
let prodigy_home = prodigy_home_dir.path().to_path_buf();
let _worktree_path = create_test_checkpoint_with_worktree(
&prodigy_home,
&test_dir,
workflow_id,
1, 3, json!({}),
)
.expect("Failed to create test checkpoint with worktree");
fs::write("/tmp/retry-test-marker", "test").ok();
test = test
.env("PRODIGY_HOME", prodigy_home.to_str().unwrap())
.arg("resume")
.arg(workflow_id)
.arg("--path")
.arg(test_dir.to_str().unwrap());
let output = test.run();
assert_eq!(output.exit_code, exit_codes::SUCCESS);
fs::remove_file("/tmp/retry-test-marker").ok();
}
#[test]
fn test_resume_completed_workflow() {
let prodigy_home = setup_test_prodigy_home();
let mut test = CliTest::new();
let test_dir = test.temp_path().to_path_buf();
let workflow_id = "session-resume-complete-33333";
let now = chrono::Utc::now();
let worktree_dir = prodigy_home
.path()
.to_path_buf()
.join("worktrees")
.join("prodigy")
.join(workflow_id);
fs::create_dir_all(&worktree_dir).expect("Failed to create worktree directory");
std::process::Command::new("git")
.arg("init")
.current_dir(&worktree_dir)
.output()
.expect("Failed to initialize git repository in worktree");
let unified_session = json!({
"id": workflow_id,
"session_type": "Workflow",
"status": "Completed", "started_at": now.to_rfc3339(),
"updated_at": now.to_rfc3339(),
"completed_at": now.to_rfc3339(),
"metadata": {},
"checkpoints": [],
"timings": {},
"error": null,
"workflow_data": {
"workflow_id": workflow_id,
"workflow_name": "test-workflow",
"current_step": 5,
"total_steps": 5,
"completed_steps": [0, 1, 2, 3, 4],
"variables": {},
"iterations_completed": 1,
"files_changed": 0,
"worktree_name": workflow_id
},
"mapreduce_data": null
});
let sessions_dir = prodigy_home.path().to_path_buf().join("sessions");
fs::create_dir_all(&sessions_dir).unwrap();
fs::write(
sessions_dir.join(format!("{}.json", workflow_id)),
serde_json::to_string_pretty(&unified_session).unwrap(),
)
.unwrap();
test = test
.arg("resume")
.arg(workflow_id)
.arg("--path")
.arg(test_dir.to_str().unwrap());
let output = test.run();
assert_ne!(
output.exit_code,
exit_codes::SUCCESS,
"Resume should fail for completed workflow"
);
assert!(
output.stderr.contains("already completed")
|| output.stderr.contains("nothing to resume")
|| output.stderr.contains("No checkpoints found")
|| output.stdout.contains("already completed"),
"Expected appropriate error message, got stdout: {}\nstderr: {}",
output.stdout,
output.stderr
);
}
#[test]
fn test_resume_with_force_restart() {
let prodigy_home_dir = setup_test_prodigy_home();
let mut test = CliTest::new();
let test_dir = test.temp_path().to_path_buf();
let _workflow_path = create_test_workflow(&test_dir, "test-resume-workflow.yaml");
let workflow_id = "session-resume-force-44444";
let prodigy_home = prodigy_home_dir.path().to_path_buf();
let _worktree_path = create_test_checkpoint_with_worktree(
&prodigy_home,
&test_dir,
workflow_id,
3, 5, json!({}),
)
.expect("Failed to create test checkpoint with worktree");
test = test
.env("PRODIGY_HOME", prodigy_home.to_str().unwrap())
.arg("resume")
.arg(workflow_id)
.arg("--force")
.arg("--path")
.arg(test_dir.to_str().unwrap());
let output = test.run();
assert_eq!(
output.exit_code,
exit_codes::SUCCESS,
"Force restart should succeed. Stdout: {}\nStderr: {}",
output.stdout,
output.stderr
);
assert!(
output.stdout_contains("completed")
|| output.stdout_contains("Session complete")
|| output.stdout_contains("Resumed"),
"Expected completion or resume message. Stdout: {}",
output.stdout
);
}
#[test]
fn test_resume_parallel_workflow() {
let prodigy_home = setup_test_prodigy_home();
let mut test = CliTest::new();
let test_dir = test.temp_path().to_path_buf();
let workflow_content = r#"
name: test-parallel-workflow
description: Test parallel execution resume
parallel:
max_workers: 3
commands:
- shell: "echo 'Parallel 1'"
id: p1
- shell: "echo 'Parallel 2'"
id: p2
- shell: "echo 'Parallel 3'"
id: p3
- shell: "echo 'Parallel 4'"
id: p4
commands:
- shell: "echo 'After parallel'"
"#;
let workflow_path = test_dir.join("test-resume-workflow.yaml");
fs::write(&workflow_path, workflow_content).unwrap();
let workflow_id = "session-resume-parallel-55555";
let prodigy_home = prodigy_home.path().to_path_buf();
let _worktree_path = create_test_checkpoint_with_worktree(
&prodigy_home,
&test_dir,
workflow_id,
0, 5, json!({}),
)
.expect("Failed to create test checkpoint with worktree");
test = test
.env("PRODIGY_HOME", prodigy_home.to_str().unwrap())
.arg("resume")
.arg(workflow_id)
.arg("--path")
.arg(test_dir.to_str().unwrap());
let output = test.run();
if output.exit_code != exit_codes::SUCCESS {
eprintln!("Resume failed with exit code: {}", output.exit_code);
eprintln!("STDOUT:\n{}", output.stdout);
eprintln!("STDERR:\n{}", output.stderr);
}
assert_eq!(
output.exit_code,
exit_codes::SUCCESS,
"Resume failed with exit code: {}, stderr: {}, stdout: {}",
output.exit_code,
output.stderr,
output.stdout
);
assert!(
output.stdout_contains("Resuming") || output.stdout_contains("Found checkpoint"),
"Expected resume message not found"
);
}
#[test]
fn test_resume_with_checkpoint_cleanup() {
let prodigy_home = setup_test_prodigy_home();
let mut test = CliTest::new();
let test_dir = test.temp_path().to_path_buf();
let _workflow_path = create_test_workflow(&test_dir, "test-resume-workflow.yaml");
let workflow_id = "session-resume-cleanup-66666";
let prodigy_home = prodigy_home.path().to_path_buf();
let _worktree_path = create_test_checkpoint_with_worktree(
&prodigy_home,
&test_dir,
workflow_id,
4, 5, json!({}),
)
.expect("Failed to create test checkpoint with worktree");
let checkpoint_file = prodigy_home
.join("state")
.join(workflow_id)
.join("checkpoints")
.join(format!("{}.checkpoint.json", workflow_id));
assert!(
checkpoint_file.exists(),
"Checkpoint should exist before resume"
);
test = test
.env("PRODIGY_HOME", prodigy_home.to_str().unwrap())
.arg("resume")
.arg(workflow_id)
.arg("--path")
.arg(test_dir.to_str().unwrap());
let output = test.run();
assert_eq!(
output.exit_code,
exit_codes::SUCCESS,
"Resume failed with stderr: {}",
output.stderr
);
assert!(
output.stdout_contains(
"[TEST MODE] Would execute Shell command: echo 'Final command executed'"
) || output.stdout_contains("Final command executed")
|| output.stdout_contains("completed"),
"Expected completion message not found in stdout: {}",
output.stdout
);
}
#[test]
#[ignore = "Error recovery during resume not fully implemented"]
fn test_resume_with_error_recovery() {
let prodigy_home = setup_test_prodigy_home();
let mut test = CliTest::new();
let test_dir = test.temp_path().to_path_buf();
let _checkpoint_dir = test_dir.join(".prodigy").join("checkpoints");
let workflow_dir = test_dir.clone();
let workflow_content = r#"
name: test-error-workflow
description: Test error recovery during resume
commands:
- shell: "echo 'Command 1'"
- shell: "echo 'Command 2'"
- shell: "exit 1"
id: failing_command
on_failure: "echo 'Error handled'"
- shell: "echo 'Continue after error'"
"#;
let workflow_path = workflow_dir.join("test-resume-workflow.yaml");
fs::write(&workflow_path, workflow_content).unwrap();
let workflow_id = "resume-error-77777";
create_test_checkpoint(prodigy_home.path(), workflow_id, 2, 4, json!({}));
test = test
.env("PRODIGY_HOME", prodigy_home.path().to_str().unwrap())
.arg("resume")
.arg(workflow_id)
.arg("--path")
.arg(test_dir.to_str().unwrap());
let output = test.run();
assert_eq!(output.exit_code, exit_codes::SUCCESS);
}
#[test]
fn test_resume_multiple_checkpoints() {
let prodigy_home = setup_test_prodigy_home();
let test = CliTest::new();
let test_dir = test.temp_path().to_path_buf();
let _checkpoint_dir = test_dir.join(".prodigy").join("checkpoints");
for i in 1..=3 {
let workflow_id = format!("workflow-{}", i);
create_test_checkpoint(prodigy_home.path(), &workflow_id, i, 5, json!({}));
}
for i in 1..=3 {
let workflow_id = format!("workflow-{}", i);
let checkpoint_file = prodigy_home
.path()
.join("state")
.join(&workflow_id)
.join("checkpoints")
.join(format!("{}.checkpoint.json", workflow_id));
assert!(
checkpoint_file.exists(),
"Checkpoint should exist at {:?}",
checkpoint_file
);
}
}
#[test]
#[ignore = "MapReduce resume not fully implemented"]
fn test_resume_with_mapreduce_state() {
let _prodigy_home = setup_test_prodigy_home();
let mut test = CliTest::new();
let test_dir = test.temp_path().to_path_buf();
let checkpoint_dir = test_dir.join(".prodigy").join("checkpoints");
let workflow_dir = test_dir.clone();
let workflow_content = r#"
name: test-mapreduce-workflow
mode: mapreduce
map:
input: "items.json"
agent_template:
- shell: "echo 'Processing ${item}'"
max_parallel: 2
reduce:
- shell: "echo 'Reducing results'"
"#;
let workflow_path = workflow_dir.join("test-resume-workflow.yaml");
fs::write(&workflow_path, workflow_content).unwrap();
let items = json!(["item1", "item2", "item3", "item4"]);
fs::write(workflow_dir.join("items.json"), items.to_string()).unwrap();
let workflow_id = "resume-mapreduce-88888";
let now = chrono::Utc::now();
let checkpoint = json!({
"workflow_id": workflow_id,
"execution_state": {
"current_step_index": 0,
"total_steps": 2,
"status": "Interrupted",
"start_time": now.to_rfc3339(),
"last_checkpoint": now.to_rfc3339(),
"current_iteration": null,
"total_iterations": null
},
"completed_steps": [],
"variable_state": {},
"mapreduce_state": {
"phase": "map",
"completed_items": ["item1", "item2"],
"pending_items": ["item3", "item4"],
"map_results": {
"item1": {"status": "success", "output": "Processed item1"},
"item2": {"status": "success", "output": "Processed item2"}
}
},
"timestamp": now.to_rfc3339(),
"version": 1,
"workflow_hash": "test-hash-88888",
"total_steps": 2,
"workflow_name": "test-resume-workflow",
"workflow_path": null
});
fs::create_dir_all(&checkpoint_dir).unwrap();
fs::write(
checkpoint_dir.join(format!("{}.checkpoint.json", workflow_id)),
serde_json::to_string_pretty(&checkpoint).unwrap(),
)
.unwrap();
test = test
.arg("resume")
.arg(workflow_id)
.arg("--path")
.arg(test_dir.to_str().unwrap());
let output = test.run();
assert_eq!(output.exit_code, exit_codes::SUCCESS);
}
#[test]
fn test_resume_workflow_with_on_failure_handlers() {
let prodigy_home_dir = setup_test_prodigy_home();
let mut test = CliTest::new();
let test_dir = test.temp_path().to_path_buf();
let workflow_content = r#"name: test-resume-workflow
description: Test resuming workflow with on_failure handlers
commands:
- shell: "echo 'Step 1 completed' > step1.txt"
id: step1
- shell: "echo 'Step 2 completed' > step2.txt"
id: step2
- shell: "test -f trigger-failure.txt && exit 1 || echo 'Step 3 completed' > step3.txt"
id: step3
on_failure:
claude: "/fix-error --message 'Step 3 failed, cleaning up'"
- shell: "echo 'Step 4 completed' > step4.txt"
id: step4
- shell: "echo 'Final step completed' > final.txt"
id: final
"#;
let workflow_path = test_dir.join("test-resume-workflow.yaml");
fs::write(&workflow_path, workflow_content).unwrap();
let workflow_id = "session-on-failure-resume-test";
let prodigy_home = prodigy_home_dir.path().to_path_buf();
let _worktree_path = create_test_checkpoint_with_worktree(
&prodigy_home,
&test_dir,
workflow_id,
2, 5, json!({}),
)
.expect("Failed to create test checkpoint with worktree");
fs::write(test_dir.join("trigger-failure.txt"), "trigger").unwrap();
test = test
.env("PRODIGY_HOME", prodigy_home.to_str().unwrap())
.arg("resume")
.arg(workflow_id)
.arg("--path")
.arg(test_dir.to_str().unwrap());
let output = test.run();
assert_eq!(
output.exit_code,
exit_codes::SUCCESS,
"Workflow should complete successfully. Output: {}\nStderr: {}",
output.stdout,
output.stderr
);
assert!(
output.stdout_contains("completed")
|| output.stdout_contains("Session complete")
|| output.stdout_contains("successfully"),
"Workflow should show completion. Output: {}",
output.stdout
);
}
#[test]
fn test_checkpoint_with_error_recovery_state_serialization() {
let now = chrono::Utc::now();
let workflow_id = "test-error-state-serialization";
let checkpoint_with_error_state = json!({
"workflow_id": workflow_id,
"execution_state": {
"current_step_index": 2,
"total_steps": 4,
"status": "Interrupted",
"start_time": now.to_rfc3339(),
"last_checkpoint": now.to_rfc3339(),
"current_iteration": null,
"total_iterations": null
},
"completed_steps": [
{
"step_index": 0,
"command": "shell: echo 'Step 1'",
"success": true,
"output": "Step 1 output",
"captured_variables": {},
"duration": { "secs": 1, "nanos": 0 },
"completed_at": now.to_rfc3339(),
"retry_state": null
}
],
"variable_state": {
"__error_recovery_state": json!({
"active_handlers": [{
"id": "handler-1",
"commands": [
{"shell": "echo 'Handling error'"},
{"shell": "rm -f error.txt"}
],
"strategy": "retry",
"scope": "step",
"timeout": { "secs": 30, "nanos": 0 }
}],
"error_context": {
"message": "Command failed",
"exit_code": "1"
},
"handler_execution_history": [],
"correlation_id": "test-123",
"recovery_attempts": 1,
"max_recovery_attempts": 3
}),
"other_var": "some_value"
},
"mapreduce_state": null,
"timestamp": now.to_rfc3339(),
"version": 1,
"workflow_hash": "test-hash",
"total_steps": 4,
"workflow_name": "test-workflow",
"workflow_path": null
});
let checkpoint_str = serde_json::to_string(&checkpoint_with_error_state).unwrap();
let parsed_checkpoint: serde_json::Value = serde_json::from_str(&checkpoint_str).unwrap();
assert!(parsed_checkpoint["variable_state"]["__error_recovery_state"].is_object());
let error_state = &parsed_checkpoint["variable_state"]["__error_recovery_state"];
assert_eq!(error_state["correlation_id"], "test-123");
assert_eq!(error_state["recovery_attempts"], 1);
assert_eq!(error_state["max_recovery_attempts"], 3);
let handlers = error_state["active_handlers"].as_array().unwrap();
assert_eq!(handlers.len(), 1);
assert_eq!(handlers[0]["id"], "handler-1");
assert_eq!(handlers[0]["strategy"], "retry");
}
#[test]
fn test_end_to_end_error_handler_execution_after_resume() {
let prodigy_home = setup_test_prodigy_home();
let mut test = CliTest::new();
let test_dir = test.temp_path().to_path_buf();
let workflow_content = r#"
name: test-resume-workflow
description: Test error handler execution during resume
commands:
- shell: "echo 'Step 1: Initialize'"
id: step1
- shell: "echo 'Step 2: Pre-error setup'"
id: step2
- shell: "exit 1"
id: step3_with_error
on_failure:
claude: "/fix-error --output 'Error handler executed'"
- shell: "echo 'Step 4: Post-recovery'"
id: step4
- shell: "echo 'Step 5: Completion'"
id: final_step
"#;
let workflow_path = test_dir.join("test-resume-workflow.yaml");
fs::write(&workflow_path, workflow_content).unwrap();
let workflow_id = "session-end-to-end-error-handler-test";
let prodigy_home = prodigy_home.path().to_path_buf();
let variables = json!({
"__error_recovery_state": {
"active_handlers": [{
"id": "step3-error-handler",
"command": {
"claude": "/fix-error --output 'Error handler executed'"
},
"strategy": "retry"
}],
"correlation_id": "test-correlation-123",
"recovery_attempts": 1,
"max_recovery_attempts": 3
}
});
let _worktree_path = create_test_checkpoint_with_worktree(
&prodigy_home,
&test_dir,
workflow_id,
2, 5, variables,
)
.expect("Failed to create test checkpoint with worktree");
test = test
.env("PRODIGY_HOME", prodigy_home.to_str().unwrap())
.arg("resume")
.arg(workflow_id)
.arg("--path")
.arg(test_dir.to_str().unwrap());
let resume_output = test.run();
assert_eq!(
resume_output.exit_code,
exit_codes::SUCCESS,
"Resume should complete successfully. Stdout: {}\nStderr: {}",
resume_output.stdout,
resume_output.stderr
);
assert!(
resume_output.stdout_contains("Resumed session completed successfully")
|| resume_output.stdout_contains("Session complete")
|| resume_output.stdout_contains("completed"),
"Expected completion message not found in output: {}",
resume_output.stdout
);
}
#[test]
fn test_resume_session_not_found_error() {
let prodigy_home = setup_test_prodigy_home();
let mut test = CliTest::new();
let test_dir = test.temp_path().to_path_buf();
let _workflow_path = create_test_workflow(&test_dir, "test-resume-workflow.yaml");
let nonexistent_session_id = "session-does-not-exist-12345";
test = test
.env("PRODIGY_HOME", prodigy_home.path().to_str().unwrap())
.arg("resume")
.arg(nonexistent_session_id)
.arg("--path")
.arg(test_dir.to_str().unwrap());
let output = test.run();
assert_ne!(
output.exit_code,
exit_codes::SUCCESS,
"Resume should fail when session doesn't exist"
);
assert!(
output.stderr.contains("Session not found")
|| output.stderr.contains("not found")
|| output.stderr.contains("No checkpoints found"),
"Expected 'Session not found' or 'No checkpoints found' error. Stderr: {}",
output.stderr
);
}
#[test]
fn test_resume_non_resumable_session_status() {
let prodigy_home = setup_test_prodigy_home();
let mut test = CliTest::new();
let test_dir = test.temp_path().to_path_buf();
let _workflow_path = create_test_workflow(&test_dir, "test-resume-workflow.yaml");
let workflow_id = "session-failed-status-12345";
let now = chrono::Utc::now();
let worktree_dir = prodigy_home
.path()
.join("worktrees")
.join("prodigy")
.join(workflow_id);
fs::create_dir_all(&worktree_dir).unwrap();
std::process::Command::new("git")
.arg("init")
.current_dir(&worktree_dir)
.output()
.unwrap();
let unified_session = json!({
"id": workflow_id,
"session_type": "Workflow",
"status": "Failed", "started_at": now.to_rfc3339(),
"updated_at": now.to_rfc3339(),
"completed_at": null,
"metadata": {},
"checkpoints": [],
"timings": {},
"error": "Previous error",
"workflow_data": {
"workflow_id": workflow_id,
"workflow_name": "test-resume-workflow",
"current_step": 2,
"total_steps": 5,
"completed_steps": [0, 1],
"variables": {},
"iterations_completed": 0,
"files_changed": 0,
"worktree_name": workflow_id
},
"mapreduce_data": null
});
let sessions_dir = prodigy_home.path().join("sessions");
fs::create_dir_all(&sessions_dir).unwrap();
fs::write(
sessions_dir.join(format!("{}.json", workflow_id)),
serde_json::to_string_pretty(&unified_session).unwrap(),
)
.unwrap();
test = test
.env("PRODIGY_HOME", prodigy_home.path().to_str().unwrap())
.arg("resume")
.arg(workflow_id)
.arg("--path")
.arg(test_dir.to_str().unwrap());
let output = test.run();
assert_ne!(
output.exit_code,
exit_codes::SUCCESS,
"Resume should fail for non-resumable session status"
);
assert!(
output.stderr.contains("not resumable") || output.stderr.contains("Failed"),
"Expected 'not resumable' error. Stderr: {}",
output.stderr
);
}
#[test]
#[ignore = "Workflow hash validation not currently enforced in resume flow"]
fn test_resume_workflow_hash_mismatch() {
let prodigy_home = setup_test_prodigy_home();
let mut test = CliTest::new();
let test_dir = test.temp_path().to_path_buf();
let _workflow_path = create_test_workflow(&test_dir, "test-resume-workflow.yaml");
let workflow_id = "session-hash-mismatch-12345";
let variables = json!({});
let _worktree_path = create_test_checkpoint_with_worktree(
prodigy_home.path(),
&test_dir,
workflow_id,
2,
5,
variables,
)
.unwrap();
let modified_workflow_content = r#"
name: test-resume-workflow
description: Modified workflow - different hash
commands:
- shell: "echo 'Modified Command 1'"
id: cmd1
- shell: "echo 'Modified Command 2'"
id: cmd2
- shell: "echo 'Modified Command 3'"
id: cmd3
- shell: "echo 'Modified Command 4'"
id: cmd4
- shell: "echo 'Modified Final command'"
id: cmd5
"#;
let workflow_path = test_dir.join("test-resume-workflow.yaml");
fs::write(&workflow_path, modified_workflow_content).unwrap();
test = test
.env("PRODIGY_HOME", prodigy_home.path().to_str().unwrap())
.arg("resume")
.arg(workflow_id)
.arg("--path")
.arg(test_dir.to_str().unwrap());
let output = test.run();
assert_ne!(
output.exit_code,
exit_codes::SUCCESS,
"Resume should fail when workflow hash doesn't match. Stdout: {}\nStderr: {}",
output.stdout,
output.stderr
);
assert!(
output.stderr.contains("Workflow has been modified")
|| output.stderr.contains("hash")
|| output.stderr.contains("changed"),
"Expected workflow modification error. Stderr: {}",
output.stderr
);
}
#[test]
fn test_resume_missing_workflow_state() {
let prodigy_home = setup_test_prodigy_home();
let mut test = CliTest::new();
let test_dir = test.temp_path().to_path_buf();
let _workflow_path = create_test_workflow(&test_dir, "test-resume-workflow.yaml");
let workflow_id = "session-no-workflow-state-12345";
let now = chrono::Utc::now();
let worktree_dir = prodigy_home
.path()
.join("worktrees")
.join("prodigy")
.join(workflow_id);
fs::create_dir_all(&worktree_dir).unwrap();
std::process::Command::new("git")
.arg("init")
.current_dir(&worktree_dir)
.output()
.unwrap();
let unified_session = json!({
"id": workflow_id,
"session_type": "Workflow",
"status": "Paused",
"started_at": now.to_rfc3339(),
"updated_at": now.to_rfc3339(),
"completed_at": null,
"metadata": {},
"checkpoints": [],
"timings": {},
"error": null,
"workflow_data": null, "mapreduce_data": null
});
let sessions_dir = prodigy_home.path().join("sessions");
fs::create_dir_all(&sessions_dir).unwrap();
fs::write(
sessions_dir.join(format!("{}.json", workflow_id)),
serde_json::to_string_pretty(&unified_session).unwrap(),
)
.unwrap();
test = test
.env("PRODIGY_HOME", prodigy_home.path().to_str().unwrap())
.arg("resume")
.arg(workflow_id)
.arg("--path")
.arg(test_dir.to_str().unwrap());
let output = test.run();
assert_ne!(
output.exit_code,
exit_codes::SUCCESS,
"Resume should fail when workflow state is missing"
);
assert!(
output.stderr.contains("no workflow state")
|| output.stderr.contains("workflow state")
|| output.stderr.contains("No checkpoints found"),
"Expected missing workflow state or no checkpoints error. Stderr: {}",
output.stderr
);
}
#[test]
fn test_resume_interrupted_again_during_resume() {
let prodigy_home = setup_test_prodigy_home();
let mut test = CliTest::new();
let test_dir = test.temp_path().to_path_buf();
let workflow_content = r#"
name: test-resume-workflow
description: Test workflow that simulates interruption
commands:
- shell: "echo 'Command 1 executed'"
id: cmd1
- shell: "echo 'Command 2 executed'"
id: cmd2
- shell: "echo 'Command 3 - simulating interruption' && exit 130"
id: cmd3
- shell: "echo 'Command 4 executed'"
id: cmd4
- shell: "echo 'Final command executed'"
id: cmd5
"#;
let workflow_path = test_dir.join("test-resume-workflow.yaml");
fs::write(&workflow_path, workflow_content).unwrap();
let workflow_id = "session-interrupted-again-12345";
let variables = json!({});
let _worktree_path = create_test_checkpoint_with_worktree(
prodigy_home.path(),
&test_dir,
workflow_id,
2,
5,
variables,
)
.unwrap();
test = test
.env("PRODIGY_HOME", prodigy_home.path().to_str().unwrap())
.arg("resume")
.arg(workflow_id)
.arg("--path")
.arg(test_dir.to_str().unwrap());
let output = test.run();
if output.exit_code == exit_codes::SUCCESS {
assert!(output.stdout_contains("completed") || output.stdout_contains("Resumed"));
} else {
assert!(
output.stderr.contains("interrupted")
|| output.stdout.contains("interrupted")
|| output.stderr.contains("Resume with:"),
"Expected interruption message. Stdout: {}\nStderr: {}",
output.stdout,
output.stderr
);
}
}
#[test]
fn test_resume_failure_during_resume() {
let prodigy_home = setup_test_prodigy_home();
let mut test = CliTest::new();
let test_dir = test.temp_path().to_path_buf();
let workflow_content = r#"
name: test-resume-workflow
description: Test workflow that fails during resume
commands:
- shell: "echo 'Command 1 executed'"
id: cmd1
- shell: "echo 'Command 2 executed'"
id: cmd2
- shell: "echo 'Command 3 - failing' && exit 1"
id: cmd3
- shell: "echo 'Command 4 executed'"
id: cmd4
- shell: "echo 'Final command executed'"
id: cmd5
"#;
let workflow_path = test_dir.join("test-resume-workflow.yaml");
fs::write(&workflow_path, workflow_content).unwrap();
let workflow_id = "session-failed-during-resume-12345";
let variables = json!({});
let _worktree_path = create_test_checkpoint_with_worktree(
prodigy_home.path(),
&test_dir,
workflow_id,
2,
5,
variables,
)
.unwrap();
test = test
.env("PRODIGY_HOME", prodigy_home.path().to_str().unwrap())
.arg("resume")
.arg(workflow_id)
.arg("--path")
.arg(test_dir.to_str().unwrap());
let output = test.run();
assert_ne!(
output.exit_code,
exit_codes::SUCCESS,
"Resume should fail when workflow command fails"
);
assert!(
output.stderr.contains("failed") || output.stderr.contains("error"),
"Expected failure message. Stderr: {}",
output.stderr
);
}