use anyhow::Result;
use prodigy::subprocess::{ProcessCommandBuilder, SubprocessManager};
use std::fs;
use std::path::PathBuf;
use tempfile::TempDir;
use tokio::process::Command;
async fn setup_git_repo(path: &PathBuf) -> Result<()> {
Command::new("git")
.args(["init"])
.current_dir(path)
.output()
.await?;
Command::new("git")
.args(["config", "user.name", "Test User"])
.current_dir(path)
.output()
.await?;
Command::new("git")
.args(["config", "user.email", "test@example.com"])
.current_dir(path)
.output()
.await?;
fs::write(path.join("README.md"), "# Test Project")?;
Command::new("git")
.args(["add", "."])
.current_dir(path)
.output()
.await?;
Command::new("git")
.args(["commit", "-m", "Initial commit"])
.current_dir(path)
.output()
.await?;
Ok(())
}
#[tokio::test]
#[ignore] async fn test_mapreduce_executes_in_parent_worktree_only() -> Result<()> {
let temp_dir = TempDir::new()?;
let repo_path = temp_dir.path().to_path_buf();
setup_git_repo(&repo_path).await?;
let workflow_yaml = r#"
name: test-mapreduce-architecture
mode: mapreduce
setup:
- shell: "echo 'setup phase' > setup-output.txt"
map:
input: '[{"id": 1, "name": "item1"}, {"id": 2, "name": "item2"}]'
json_path: "$[*]"
agent_template:
- shell: "echo 'Processing ${item.name}' > ${item.name}.txt"
max_parallel: 2
reduce:
- shell: "echo 'reduce phase' > reduce-output.txt"
"#;
let workflow_path = repo_path.join("test-workflow.yml");
fs::write(&workflow_path, workflow_yaml)?;
let subprocess = SubprocessManager::production();
let prodigy_check = Command::new("prodigy").arg("--version").output().await;
if prodigy_check.is_err() || !prodigy_check.unwrap().status.success() {
eprintln!("⚠️ Prodigy CLI not available, skipping integration test");
return Ok(());
}
let run_cmd = ProcessCommandBuilder::new("prodigy")
.args(["run", workflow_path.to_str().unwrap(), "-y"])
.current_dir(&repo_path)
.env("PRODIGY_AUTOMATION", "true")
.build();
let result = subprocess.runner().run(run_cmd).await?;
if !result.status.success() {
eprintln!("Prodigy run output:");
eprintln!("{}", result.stdout);
eprintln!("{}", result.stderr);
anyhow::bail!("Prodigy run failed");
}
let worktree_list_output = Command::new("git")
.args(["worktree", "list"])
.current_dir(&repo_path)
.output()
.await?;
let worktree_list = String::from_utf8_lossy(&worktree_list_output.stdout);
assert!(
!worktree_list.contains("session-mapreduce-"),
"MapReduce should NOT create intermediate session-mapreduce-xxx worktree. Found worktrees:\n{}",
worktree_list
);
Ok(())
}
#[tokio::test]
async fn test_setup_and_reduce_execute_in_parent_worktree() -> Result<()> {
let temp_dir = TempDir::new()?;
let repo_path = temp_dir.path().to_path_buf();
setup_git_repo(&repo_path).await?;
let subprocess = SubprocessManager::production();
let worktree_name = "prodigy-session-test";
let worktree_path = repo_path.join(".prodigy-worktrees").join(worktree_name);
fs::create_dir_all(worktree_path.parent().unwrap())?;
let worktree_cmd = ProcessCommandBuilder::new("git")
.current_dir(&repo_path)
.args([
"worktree",
"add",
"-b",
&format!("prodigy-{}", worktree_name),
worktree_path.to_str().unwrap(),
])
.build();
subprocess.runner().run(worktree_cmd).await?;
let setup_cmd = ProcessCommandBuilder::new("sh")
.current_dir(&worktree_path)
.args(["-c", "echo 'setup' > setup-output.txt"])
.build();
let setup_result = subprocess.runner().run(setup_cmd).await?;
assert!(setup_result.status.success(), "Setup phase should succeed");
assert!(
worktree_path.join("setup-output.txt").exists(),
"Setup phase output should exist in parent worktree"
);
let reduce_cmd = ProcessCommandBuilder::new("sh")
.current_dir(&worktree_path)
.args(["-c", "echo 'reduce' > reduce-output.txt"])
.build();
let reduce_result = subprocess.runner().run(reduce_cmd).await?;
assert!(
reduce_result.status.success(),
"Reduce phase should succeed"
);
assert!(
worktree_path.join("reduce-output.txt").exists(),
"Reduce phase output should exist in parent worktree"
);
let worktree_list_output = Command::new("git")
.args(["worktree", "list"])
.current_dir(&repo_path)
.output()
.await?;
let worktree_list = String::from_utf8_lossy(&worktree_list_output.stdout);
let worktree_count = worktree_list.lines().count();
assert_eq!(
worktree_count, 2,
"Should have exactly 2 worktrees (main + parent), found {}:\n{}",
worktree_count, worktree_list
);
Ok(())
}
#[tokio::test]
async fn test_agent_worktrees_branch_from_parent() -> Result<()> {
let temp_dir = TempDir::new()?;
let repo_path = temp_dir.path().to_path_buf();
setup_git_repo(&repo_path).await?;
let subprocess = SubprocessManager::production();
let parent_worktree_name = "prodigy-session-parent";
let parent_path = repo_path
.join(".prodigy-worktrees")
.join(parent_worktree_name);
fs::create_dir_all(parent_path.parent().unwrap())?;
let parent_cmd = ProcessCommandBuilder::new("git")
.current_dir(&repo_path)
.args([
"worktree",
"add",
"-b",
&format!("prodigy-{}", parent_worktree_name),
parent_path.to_str().unwrap(),
])
.build();
subprocess.runner().run(parent_cmd).await?;
let agent_worktree_name = "agent-1";
let agent_path = parent_path.join(agent_worktree_name);
let agent_cmd = ProcessCommandBuilder::new("git")
.current_dir(&parent_path) .args([
"worktree",
"add",
"-b",
&format!("prodigy-{}", agent_worktree_name),
agent_path.to_str().unwrap(),
])
.build();
subprocess.runner().run(agent_cmd).await?;
assert!(
agent_path.exists(),
"Agent worktree should exist as child of parent worktree"
);
let worktree_list_output = Command::new("git")
.args(["worktree", "list", "--porcelain"])
.current_dir(&repo_path)
.output()
.await?;
let worktree_list = String::from_utf8_lossy(&worktree_list_output.stdout);
let worktree_count = worktree_list.matches("worktree ").count();
assert!(
worktree_count <= 3,
"Should have at most 3 worktrees (main + parent + agent), found {}:\n{}",
worktree_count,
worktree_list
);
Ok(())
}
#[test]
fn test_user_prompted_for_merge_not_automatic() {
}
#[tokio::test]
async fn test_original_branch_tracking() -> Result<()> {
let temp_dir = TempDir::new()?;
let repo_path = temp_dir.path().to_path_buf();
setup_git_repo(&repo_path).await?;
Command::new("git")
.args(["checkout", "-b", "feature/my-feature"])
.current_dir(&repo_path)
.output()
.await?;
let subprocess = SubprocessManager::production();
let worktree_name = "prodigy-session-feature";
let worktree_path = repo_path.join(".prodigy-worktrees").join(worktree_name);
fs::create_dir_all(worktree_path.parent().unwrap())?;
let worktree_cmd = ProcessCommandBuilder::new("git")
.current_dir(&repo_path)
.args([
"worktree",
"add",
"-b",
&format!("prodigy-{}", worktree_name),
worktree_path.to_str().unwrap(),
])
.build();
subprocess.runner().run(worktree_cmd).await?;
let branch_output = Command::new("git")
.args(["rev-parse", "--abbrev-ref", "HEAD"])
.current_dir(&repo_path)
.output()
.await?;
let current_branch = String::from_utf8_lossy(&branch_output.stdout)
.trim()
.to_string();
assert_eq!(
current_branch, "feature/my-feature",
"Original branch should be tracked as feature/my-feature"
);
Ok(())
}
#[tokio::test]
async fn test_worktree_cleanup_after_completion() -> Result<()> {
let temp_dir = TempDir::new()?;
let repo_path = temp_dir.path().to_path_buf();
setup_git_repo(&repo_path).await?;
let subprocess = SubprocessManager::production();
let worktree_name = "prodigy-session-cleanup-test";
let worktree_path = repo_path.join(".prodigy-worktrees").join(worktree_name);
fs::create_dir_all(worktree_path.parent().unwrap())?;
let worktree_cmd = ProcessCommandBuilder::new("git")
.current_dir(&repo_path)
.args([
"worktree",
"add",
"-b",
&format!("prodigy-{}", worktree_name),
worktree_path.to_str().unwrap(),
])
.build();
subprocess.runner().run(worktree_cmd).await?;
assert!(worktree_path.exists(), "Worktree should exist");
let cleanup_cmd = ProcessCommandBuilder::new("git")
.current_dir(&repo_path)
.args(["worktree", "remove", worktree_path.to_str().unwrap()])
.build();
let cleanup_result = subprocess.runner().run(cleanup_cmd).await?;
assert!(
cleanup_result.status.success(),
"Worktree cleanup should succeed"
);
assert!(
!worktree_path.exists(),
"Worktree should be removed after cleanup"
);
Ok(())
}