use prodigy::config::mapreduce::{AgentTemplate, MapPhaseYaml, MapReduceWorkflowConfig};
use prodigy::config::WorkflowConfig;
use prodigy::cook::command::CookCommand;
use prodigy::cook::orchestrator::CookConfig;
use prodigy::core::orchestration::{
calculate_resources, detect_execution_mode, plan_execution, ExecutionMode, Phase, PhaseType,
};
use prodigy::core::session::updates::{
apply_session_update, apply_updates, ProgressUpdate, SessionUpdate, StepRecord,
};
use prodigy::core::session::validation::{is_terminal_status, valid_transitions_from};
use prodigy::unified_session::{SessionStatus, UnifiedSession};
use serde_json::json;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
fn create_workflow_config() -> WorkflowConfig {
WorkflowConfig {
name: Some("test-workflow".to_string()),
commands: vec![],
env: None,
secrets: None,
env_files: None,
profiles: None,
merge: None,
}
}
fn create_mapreduce_config(max_parallel: usize) -> MapReduceWorkflowConfig {
MapReduceWorkflowConfig {
name: "test-mapreduce".to_string(),
mode: "mapreduce".to_string(),
env: None,
secrets: None,
env_files: None,
profiles: None,
setup: None,
map: MapPhaseYaml {
input: "items.json".to_string(),
json_path: "$.items[*]".to_string(),
agent_template: AgentTemplate { commands: vec![] },
max_parallel: max_parallel.to_string(),
filter: None,
sort_by: None,
max_items: None,
offset: None,
distinct: None,
agent_timeout_secs: None,
timeout_config: None,
},
reduce: None,
error_policy: Default::default(),
on_item_failure: None,
continue_on_failure: None,
max_failures: None,
failure_threshold: None,
error_collection: None,
merge: None,
}
}
fn create_cook_config(mapreduce: bool, max_parallel: usize, dry_run: bool) -> CookConfig {
let mapreduce_config = if mapreduce {
Some(Arc::new(create_mapreduce_config(max_parallel)))
} else {
None
};
CookConfig {
command: CookCommand {
playbook: PathBuf::from("test-workflow.yml"),
path: None,
max_iterations: 1,
map: vec![],
args: vec![],
fail_fast: false,
auto_accept: false,
resume: None,
verbosity: 0,
quiet: false,
dry_run,
params: Default::default(),
},
project_path: Arc::new(PathBuf::from(".")),
workflow: Arc::new(create_workflow_config()),
mapreduce_config,
}
}
fn create_test_session() -> UnifiedSession {
let mut session = UnifiedSession::new_workflow("test-workflow".to_string(), "test".to_string());
if let Some(ref mut wd) = session.workflow_data {
wd.total_steps = 10;
}
session
}
#[test]
fn test_orchestrator_flow_mode_to_plan_integration() {
let config = create_cook_config(false, 0, false);
let mode = detect_execution_mode(&config);
let resources = calculate_resources(&config, &mode);
let plan = plan_execution(&config);
assert_eq!(mode, ExecutionMode::Standard);
assert_eq!(resources.worktrees, 0);
assert_eq!(plan.mode, ExecutionMode::Standard);
assert!(plan.has_phase(PhaseType::Commands));
}
#[test]
fn test_orchestrator_flow_mapreduce_integration() {
let config = create_cook_config(true, 10, false);
let mode = detect_execution_mode(&config);
let resources = calculate_resources(&config, &mode);
let plan = plan_execution(&config);
assert_eq!(mode, ExecutionMode::MapReduce);
assert_eq!(resources.worktrees, 11); assert_eq!(resources.max_concurrent_commands, 10);
assert_eq!(plan.mode, ExecutionMode::MapReduce);
assert!(plan.has_phase(PhaseType::Map));
assert_eq!(plan.parallel_budget, 10);
}
#[test]
fn test_orchestrator_flow_dryrun_integration() {
let config = create_cook_config(true, 10, true);
let mode = detect_execution_mode(&config);
let resources = calculate_resources(&config, &mode);
let plan = plan_execution(&config);
assert_eq!(mode, ExecutionMode::DryRun);
assert_eq!(resources.worktrees, 0);
assert_eq!(plan.mode, ExecutionMode::DryRun);
assert!(plan.has_phase(PhaseType::DryRunAnalysis));
assert_eq!(plan.phases.len(), 1);
}
#[test]
fn test_orchestrator_plan_is_deterministic() {
let config = create_cook_config(true, 20, false);
let plan1 = plan_execution(&config);
let plan2 = plan_execution(&config);
let plan3 = plan_execution(&config);
assert_eq!(plan1, plan2);
assert_eq!(plan2, plan3);
assert_eq!(plan1.parallel_budget, 20);
assert_eq!(plan1.resource_needs.worktrees, 21);
}
#[test]
fn test_session_update_composition_sequential() {
let session = create_test_session();
let original_id = session.id.clone();
let updates = vec![
SessionUpdate::Status(SessionStatus::Running),
SessionUpdate::Progress(ProgressUpdate {
completed_steps: 3,
failed_steps: 0,
current_step: Some("step-1".to_string()),
}),
SessionUpdate::Variables({
let mut m = HashMap::new();
m.insert("result".to_string(), json!("success"));
m
}),
SessionUpdate::AddStep(StepRecord::started("echo hello")),
];
let result = apply_updates(session, updates);
assert!(result.is_ok());
let updated = result.unwrap();
assert_eq!(updated.id, original_id);
assert_eq!(updated.status, SessionStatus::Running);
assert!(updated.metadata.contains_key("result"));
assert!(updated.metadata.contains_key("current_step"));
assert!(updated.metadata.contains_key("execution_steps"));
}
#[test]
fn test_session_update_error_stops_chain() {
let session = create_test_session();
let updates = vec![
SessionUpdate::Status(SessionStatus::Running),
SessionUpdate::Status(SessionStatus::Initializing),
SessionUpdate::Progress(ProgressUpdate {
completed_steps: 100,
failed_steps: 0,
current_step: None,
}),
];
let result = apply_updates(session, updates);
assert!(result.is_err());
}
#[test]
fn test_session_status_transition_chain() {
let mut session = create_test_session();
session = apply_session_update(session, SessionUpdate::Status(SessionStatus::Running)).unwrap();
assert_eq!(session.status, SessionStatus::Running);
session = apply_session_update(session, SessionUpdate::Status(SessionStatus::Paused)).unwrap();
assert_eq!(session.status, SessionStatus::Paused);
session = apply_session_update(session, SessionUpdate::Status(SessionStatus::Running)).unwrap();
assert_eq!(session.status, SessionStatus::Running);
session =
apply_session_update(session, SessionUpdate::Status(SessionStatus::Completed)).unwrap();
assert_eq!(session.status, SessionStatus::Completed);
assert!(session.completed_at.is_some());
assert!(is_terminal_status(&session.status));
}
#[test]
fn test_session_update_preserves_immutability() {
let original = create_test_session();
let original_id = original.id.clone();
let original_status = original.status.clone();
let updated = apply_session_update(
original.clone(),
SessionUpdate::Status(SessionStatus::Running),
)
.unwrap();
assert_eq!(original.id, original_id);
assert_eq!(original.status, original_status);
assert_eq!(original.status, SessionStatus::Initializing);
assert_eq!(updated.id, original_id);
assert_eq!(updated.status, SessionStatus::Running);
}
#[test]
fn test_variable_update_merge_semantics() {
let mut session = create_test_session();
let mut vars1 = HashMap::new();
vars1.insert("key1".to_string(), json!("value1"));
vars1.insert("key2".to_string(), json!(42));
session = apply_session_update(session, SessionUpdate::Variables(vars1)).unwrap();
let mut vars2 = HashMap::new();
vars2.insert("key3".to_string(), json!(true));
vars2.insert("key2".to_string(), json!(100)); session = apply_session_update(session, SessionUpdate::Variables(vars2)).unwrap();
assert_eq!(session.metadata.get("key1"), Some(&json!("value1")));
assert_eq!(session.metadata.get("key2"), Some(&json!(100))); assert_eq!(session.metadata.get("key3"), Some(&json!(true)));
}
#[test]
fn test_step_record_composition() {
let mut session = create_test_session();
let steps = vec![
StepRecord::started("step 1").complete(Some("output 1".to_string())),
StepRecord::started("step 2").complete(Some("output 2".to_string())),
StepRecord::started("step 3").fail("error on step 3"),
];
for step in steps {
session = apply_session_update(session, SessionUpdate::AddStep(step)).unwrap();
}
let execution_steps = session
.metadata
.get("execution_steps")
.and_then(|v| v.as_array())
.unwrap();
assert_eq!(execution_steps.len(), 3);
assert_eq!(
execution_steps[0].get("status").and_then(|v| v.as_str()),
Some("completed")
);
assert_eq!(
execution_steps[2].get("status").and_then(|v| v.as_str()),
Some("failed")
);
}
#[test]
fn test_planning_pure_to_execution_boundary() {
let config = create_cook_config(true, 5, false);
let plan = plan_execution(&config);
assert!(plan.requires_worktrees());
assert_eq!(plan.parallel_budget, 5);
assert!(plan.phase_count() > 0);
for phase in &plan.phases {
if let Phase::Map {
max_parallel,
has_filter,
has_sort,
} = phase
{
assert_eq!(*max_parallel, 5);
assert!(!has_filter); assert!(!has_sort);
}
}
}
#[test]
fn test_validation_pure_functions_used_in_updates() {
let session = create_test_session();
let valid_next = valid_transitions_from(&session.status);
assert_eq!(valid_next, vec![SessionStatus::Running]);
let result = apply_session_update(
session.clone(),
SessionUpdate::Status(SessionStatus::Running),
);
assert!(result.is_ok());
let result = apply_session_update(session, SessionUpdate::Status(SessionStatus::Completed));
assert!(result.is_err());
}
mod work_planning_integration {
use prodigy::cook::execution::mapreduce::pure::work_planning::{
plan_work_assignments, FilterExpression, WorkPlanConfig,
};
use serde_json::json;
#[test]
fn test_work_planning_full_pipeline() {
let items = vec![
json!({"id": 1, "type": "a", "priority": 10}),
json!({"id": 2, "type": "b", "priority": 5}),
json!({"id": 3, "type": "a", "priority": 15}),
json!({"id": 4, "type": "a", "priority": 3}),
json!({"id": 5, "type": "b", "priority": 20}),
];
let config = WorkPlanConfig {
filter: Some(FilterExpression::Equals {
field: "type".to_string(),
value: json!("a"),
}),
offset: 0,
max_items: Some(2),
};
let assignments = plan_work_assignments(items, &config);
assert_eq!(assignments.len(), 2);
for assignment in &assignments {
assert_eq!(assignment.item["type"], "a");
}
assert_eq!(assignments[0].id, 0);
assert_eq!(assignments[1].id, 1);
assert_eq!(assignments[0].worktree_name, "agent-0");
assert_eq!(assignments[1].worktree_name, "agent-1");
}
#[test]
fn test_work_planning_with_offset() {
let items = vec![
json!({"id": 1}),
json!({"id": 2}),
json!({"id": 3}),
json!({"id": 4}),
json!({"id": 5}),
];
let config = WorkPlanConfig {
filter: None,
offset: 2,
max_items: Some(2),
};
let assignments = plan_work_assignments(items, &config);
assert_eq!(assignments.len(), 2);
assert_eq!(assignments[0].item["id"], 3);
assert_eq!(assignments[1].item["id"], 4);
}
#[test]
fn test_work_planning_complex_filter() {
let items = vec![
json!({"status": "active", "priority": 5}),
json!({"status": "active", "priority": 15}),
json!({"status": "inactive", "priority": 20}),
json!({"status": "active", "priority": 25}),
];
let config = WorkPlanConfig {
filter: Some(FilterExpression::And(vec![
FilterExpression::Equals {
field: "status".to_string(),
value: json!("active"),
},
FilterExpression::GreaterThan {
field: "priority".to_string(),
value: json!(10),
},
])),
offset: 0,
max_items: None,
};
let assignments = plan_work_assignments(items, &config);
assert_eq!(assignments.len(), 2);
for assignment in &assignments {
assert_eq!(assignment.item["status"], "active");
assert!(assignment.item["priority"].as_i64().unwrap() > 10);
}
}
}
mod dependency_analysis_integration {
use prodigy::cook::execution::mapreduce::pure::dependency_analysis::{
analyze_dependencies, extract_variable_reads, extract_variable_writes, Command,
};
use std::collections::HashSet;
#[test]
fn test_dependency_analysis_real_commands() {
let cmd1 = "export RESULT=$(process_item $INPUT)";
let cmd2 = "echo $RESULT > output.txt";
let cmd3 = "validate $RESULT && notify";
let commands = vec![
Command {
reads: extract_variable_reads(cmd1),
writes: extract_variable_writes(cmd1),
},
Command {
reads: extract_variable_reads(cmd2),
writes: extract_variable_writes(cmd2),
},
Command {
reads: extract_variable_reads(cmd3),
writes: extract_variable_writes(cmd3),
},
];
let graph = analyze_dependencies(&commands);
let batches = graph.parallel_batches();
assert!(!batches.is_empty());
assert!(batches[0].contains(&0)); }
#[test]
fn test_dependency_analysis_parallel_independent() {
let commands = vec![
Command {
reads: HashSet::new(),
writes: ["A".to_string()].into_iter().collect(),
},
Command {
reads: HashSet::new(),
writes: ["B".to_string()].into_iter().collect(),
},
Command {
reads: HashSet::new(),
writes: ["C".to_string()].into_iter().collect(),
},
];
let graph = analyze_dependencies(&commands);
let batches = graph.parallel_batches();
assert_eq!(batches.len(), 1);
assert_eq!(batches[0].len(), 3);
}
#[test]
fn test_dependency_analysis_diamond_pattern() {
let commands = vec![
Command {
reads: HashSet::new(),
writes: ["A".to_string()].into_iter().collect(),
},
Command {
reads: ["A".to_string()].into_iter().collect(),
writes: ["B".to_string()].into_iter().collect(),
},
Command {
reads: ["A".to_string()].into_iter().collect(),
writes: ["C".to_string()].into_iter().collect(),
},
Command {
reads: ["B".to_string(), "C".to_string()].into_iter().collect(),
writes: ["D".to_string()].into_iter().collect(),
},
];
let graph = analyze_dependencies(&commands);
let batches = graph.parallel_batches();
assert_eq!(batches.len(), 3);
assert_eq!(batches[0], vec![0]);
assert_eq!(batches[1].len(), 2);
assert!(batches[1].contains(&1));
assert!(batches[1].contains(&2));
assert_eq!(batches[2], vec![3]);
}
}
#[test]
fn test_end_to_end_workflow_session_lifecycle() {
let config = create_cook_config(true, 5, false);
let plan = plan_execution(&config);
let mut session = UnifiedSession::new_workflow("test-job".to_string(), "test".to_string());
if let Some(ref mut wd) = session.workflow_data {
wd.total_steps = plan.phase_count();
}
session = apply_session_update(session, SessionUpdate::Status(SessionStatus::Running)).unwrap();
for (i, phase) in plan.phases.iter().enumerate() {
let step = StepRecord::started(format!("{}", phase));
session = apply_session_update(session, SessionUpdate::AddStep(step)).unwrap();
session = apply_session_update(
session,
SessionUpdate::Progress(ProgressUpdate {
completed_steps: 1,
failed_steps: 0,
current_step: Some(format!("phase-{}", i)),
}),
)
.unwrap();
}
session =
apply_session_update(session, SessionUpdate::Status(SessionStatus::Completed)).unwrap();
assert_eq!(session.status, SessionStatus::Completed);
assert!(session.completed_at.is_some());
assert!(is_terminal_status(&session.status));
let steps = session
.metadata
.get("execution_steps")
.and_then(|v| v.as_array())
.unwrap();
assert_eq!(steps.len(), plan.phase_count());
}