use super::*;
use crate::abstractions::{ClaudeClient, GitOperations, MockClaudeClient, MockGitOperations};
use anyhow::Result;
use tempfile::TempDir;
#[derive(Debug, Clone)]
#[allow(dead_code)]
struct TestSessionState {
session_id: String,
started_at: i64,
completed_at: Option<i64>,
iterations_completed: usize,
max_iterations: usize,
worktree_mode: bool,
focus: Option<String>,
changes_made: bool,
files_changed: Vec<String>,
commands_executed: Vec<String>,
errors: Vec<String>,
summary: Option<String>,
}
#[cfg(test)]
mod core_tests {
use super::*;
use crate::worktree::WorktreeManager;
async fn run_analysis(_project_path: &Path, _run_coverage: bool) -> Result<()> {
Ok(())
}
async fn extract_spec_from_git(git_ops: &dyn GitOperations) -> Result<Option<String>> {
let msg = git_ops.get_last_commit_message().await?;
if let Some(spec_part) = msg.strip_prefix("add: spec ") {
Ok(Some(spec_part.to_string()))
} else {
Ok(None)
}
}
async fn git_command_exists() -> bool {
tokio::process::Command::new("git")
.arg("--version")
.output()
.await
.map(|output| output.status.success())
.unwrap_or(false)
}
fn validate_arguments(args: &[String]) -> Vec<String> {
args.to_vec()
}
async fn ensure_prodigy_directory(project_path: &Path) -> Result<()> {
let prodigy_dir = project_path.join(".prodigy");
tokio::fs::create_dir_all(prodigy_dir).await?;
Ok(())
}
fn create_session_state(
session_id: &str,
focus: Option<&str>,
max_iterations: usize,
worktree_mode: bool,
) -> TestSessionState {
TestSessionState {
session_id: session_id.to_string(),
started_at: chrono::Utc::now().timestamp(),
completed_at: None,
iterations_completed: 0,
max_iterations,
worktree_mode,
focus: focus.map(|f| f.to_string()),
changes_made: true,
files_changed: Vec::new(),
commands_executed: Vec::new(),
errors: Vec::new(),
summary: None,
}
}
fn generate_session_id() -> String {
format!("session-{}", uuid::Uuid::new_v4())
}
async fn check_for_changes(git_ops: &dyn GitOperations) -> Result<bool> {
let status = git_ops.check_git_status().await?;
Ok(!status.trim().is_empty())
}
async fn update_session_metrics(_project_path: &Path, _state: &TestSessionState) {
}
async fn handle_worktree_merge(
_worktree_mgr: &WorktreeManager,
_worktree_name: &str,
_auto_accept: bool,
) -> Result<()> {
Ok(())
}
fn format_session_summary(state: &TestSessionState) -> String {
let mode = if state.worktree_mode {
"worktree"
} else {
"direct"
};
let focus = state.focus.as_deref().unwrap_or("none");
format!(
"Session: {}\nIterations: {}/{}\nFocus: {}\nFiles changed: {}\nMode: {}",
state.session_id,
state.iterations_completed,
state.max_iterations,
focus,
state.files_changed.len(),
mode
)
}
fn should_continue_iteration(state: &TestSessionState, fail_fast: bool) -> bool {
if state.iterations_completed >= state.max_iterations {
return false;
}
if !state.changes_made && fail_fast {
return false;
}
state.changes_made
}
async fn save_checkpoint(
_project_path: &Path,
_state: &TestSessionState,
_spec_id: Option<&str>,
) {
}
#[tokio::test]
async fn test_load_playbook_yaml() {
let temp_dir = TempDir::new().unwrap();
let playbook_path = temp_dir.path().join("test.yml");
let yaml_content = r#"
commands:
- prodigy-code-review
- prodigy-implement-spec
- prodigy-lint
"#;
tokio::fs::write(&playbook_path, yaml_content)
.await
.unwrap();
let result = load_playbook(&playbook_path).await;
assert!(result.is_ok());
let workflow = result.unwrap();
assert_eq!(workflow.commands.len(), 3);
}
#[tokio::test]
async fn test_load_playbook_json() {
let temp_dir = TempDir::new().unwrap();
let playbook_path = temp_dir.path().join("test.json");
let json_content = r#"{
"commands": ["prodigy-code-review", "prodigy-implement-spec"]
}"#;
tokio::fs::write(&playbook_path, json_content)
.await
.unwrap();
let result = load_playbook(&playbook_path).await;
assert!(result.is_ok());
let workflow = result.unwrap();
assert_eq!(workflow.commands.len(), 2);
}
#[tokio::test]
async fn test_load_playbook_invalid_format() {
let temp_dir = TempDir::new().unwrap();
let playbook_path = temp_dir.path().join("test.yml");
tokio::fs::write(&playbook_path, "invalid: [yaml content")
.await
.unwrap();
let result = load_playbook(&playbook_path).await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Failed to parse"));
}
#[tokio::test]
async fn test_load_playbook_nonexistent() {
let result = load_playbook(Path::new("/nonexistent/playbook.yml")).await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Failed to read"));
}
#[tokio::test]
async fn test_load_playbook_without_commands_wrapper() {
let temp_dir = TempDir::new().unwrap();
let playbook_path = temp_dir.path().join("test_new_format.yml");
let yaml_content = r#"
- name: prodigy-code-review
id: review
commit_required: false
- claude: /prodigy-implement-spec ${review.spec}
name: implement
commit_required: true
- name: prodigy-lint
commit_required: false
"#;
tokio::fs::write(&playbook_path, yaml_content)
.await
.unwrap();
let result = load_playbook(&playbook_path).await;
assert!(result.is_ok(), "Failed to parse new format: {result:?}");
let workflow = result.unwrap();
assert_eq!(workflow.commands.len(), 3);
assert_eq!(
workflow.commands[0].to_command().name,
"prodigy-code-review"
);
assert_eq!(workflow.commands[2].to_command().name, "prodigy-lint");
}
#[tokio::test]
async fn test_run_analysis() {
let temp_dir = TempDir::new().unwrap();
let project_path = temp_dir.path();
std::fs::create_dir_all(project_path.join("src")).unwrap();
std::fs::write(
project_path.join("Cargo.toml"),
r#"[package]
name = "test"
version = "0.1.0"
"#,
)
.unwrap();
std::fs::write(
project_path.join("src/main.rs"),
"fn main() { println!(\"Hello\"); }",
)
.unwrap();
let result = run_analysis(project_path, false).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_run_analysis_with_coverage() {
let temp_dir = TempDir::new().unwrap();
let project_path = temp_dir.path();
std::fs::create_dir_all(project_path.join("src")).unwrap();
std::fs::write(
project_path.join("Cargo.toml"),
"[package]\nname = \"test\"\nversion = \"0.1.0\"",
)
.unwrap();
let result = run_analysis(project_path, true).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_extract_spec_from_git() {
let git_ops = MockGitOperations::new();
git_ops
.add_success_response("add: spec iteration-12345-improvements")
.await;
let result = extract_spec_from_git(&git_ops).await;
assert!(result.is_ok());
assert_eq!(
result.unwrap(),
Some("iteration-12345-improvements".to_string())
);
}
#[tokio::test]
async fn test_extract_spec_from_git_no_spec() {
let git_ops = MockGitOperations::new();
git_ops.add_success_response("regular commit message").await;
let result = extract_spec_from_git(&git_ops).await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), None);
}
#[tokio::test]
async fn test_extract_spec_from_git_error() {
let git_ops = MockGitOperations::new();
git_ops
.add_error_response("fatal: not a git repository")
.await;
let result = extract_spec_from_git(&git_ops).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_git_command_exists() {
assert!(git_command_exists().await);
}
#[test]
fn test_validate_arguments() {
let safe_args = vec![
("--flag", true),
("value", true),
("file.txt", true),
("./path/to/file", true),
("spec-12345", true),
];
for (arg, expected) in safe_args {
let args = vec![arg.to_string()];
let validated = validate_arguments(&args);
assert_eq!(validated.len(), if expected { 1 } else { 0 });
}
let dangerous_args = vec![
";rm -rf /",
"&&malicious",
"`evil`",
"$(bad)",
"\ncommand",
"../../../etc/passwd",
];
for arg in dangerous_args {
let args = vec![arg.to_string()];
let validated = validate_arguments(&args);
assert_eq!(validated.len(), 1); }
}
#[tokio::test]
async fn test_ensure_prodigy_directory() {
let temp_dir = TempDir::new().unwrap();
let project_path = temp_dir.path();
assert!(!project_path.join(".prodigy").exists());
ensure_prodigy_directory(project_path).await.unwrap();
assert!(project_path.join(".prodigy").exists());
ensure_prodigy_directory(project_path).await.unwrap();
assert!(project_path.join(".prodigy").exists());
}
#[tokio::test]
async fn test_create_session_state() {
let state = create_session_state("test-session", None, 5, false);
assert_eq!(state.session_id, "test-session");
assert!(state.started_at > 0);
assert_eq!(state.iterations_completed, 0);
assert_eq!(state.max_iterations, 5);
assert!(!state.worktree_mode);
assert!(state.focus.is_none());
assert!(state.changes_made);
assert!(state.files_changed.is_empty());
}
#[tokio::test]
async fn test_create_session_state_with_focus() {
let state = create_session_state("test-session", Some("performance"), 3, true);
assert_eq!(state.focus, Some("performance".to_string()));
assert!(state.worktree_mode);
assert_eq!(state.max_iterations, 3);
}
#[test]
fn test_generate_session_id() {
let id1 = generate_session_id();
let id2 = generate_session_id();
assert_ne!(id1, id2);
assert!(id1.starts_with("session-"));
assert!(id1.len() > 8); }
#[tokio::test]
async fn test_check_for_changes() {
let git_ops = MockGitOperations::new();
git_ops
.add_success_response("M src/main.rs\nA src/lib.rs")
.await;
let result = check_for_changes(&git_ops).await;
assert!(result.is_ok());
assert!(result.unwrap());
git_ops.add_success_response("").await;
let result = check_for_changes(&git_ops).await;
assert!(result.is_ok());
assert!(!result.unwrap());
}
#[tokio::test]
async fn test_update_session_metrics() {
let temp_dir = TempDir::new().unwrap();
let project_path = temp_dir.path();
std::fs::create_dir_all(project_path.join(".prodigy")).unwrap();
let mut state = create_session_state("test", None, 5, false);
state.iterations_completed = 2;
update_session_metrics(project_path, &state).await;
}
#[tokio::test]
async fn test_handle_worktree_merge() {
let temp_dir = TempDir::new().unwrap();
let project_path = temp_dir.path();
let subprocess = crate::subprocess::SubprocessManager::production();
match WorktreeManager::new(project_path.to_path_buf(), subprocess) {
Ok(worktree_mgr) => {
let result = handle_worktree_merge(&worktree_mgr, "test-worktree", false).await;
assert!(result.is_ok());
}
Err(_) => {
}
}
}
#[test]
fn test_format_session_summary() {
let mut state = create_session_state("test", Some("security"), 3, true);
state.iterations_completed = 2;
state.files_changed = vec!["src/main.rs".to_string(), "src/lib.rs".to_string()];
let summary = format_session_summary(&state);
assert!(summary.contains("Session: test"));
assert!(summary.contains("Iterations: 2/3"));
assert!(summary.contains("Focus: security"));
assert!(summary.contains("Files changed: 2"));
assert!(summary.contains("Mode: worktree"));
}
#[tokio::test]
async fn test_run_single_iteration() {
let git_ops: Box<dyn GitOperations> = Box::new(MockGitOperations::new());
let claude: Box<dyn ClaudeClient> = Box::new(MockClaudeClient::new());
assert!(git_ops.is_git_repo().await);
assert!(claude.check_availability().await.is_ok());
}
#[test]
fn test_should_continue_iteration() {
let mut state = create_session_state("test", None, 3, false);
state.changes_made = true;
assert!(should_continue_iteration(&state, false));
state.iterations_completed = 3;
assert!(!should_continue_iteration(&state, false));
state.iterations_completed = 1;
state.changes_made = false;
assert!(!should_continue_iteration(&state, false));
assert!(!should_continue_iteration(&state, true));
}
#[tokio::test]
async fn test_save_checkpoint() {
let temp_dir = TempDir::new().unwrap();
let project_path = temp_dir.path();
std::fs::create_dir_all(project_path.join(".prodigy/state")).unwrap();
let state = create_session_state("test", None, 5, false);
save_checkpoint(project_path, &state, Some("test-spec")).await;
let _checkpoint_path = project_path.join(".prodigy/state/checkpoint.json");
}
#[tokio::test]
async fn test_run_command_with_timeout() {
let cmd = tokio::process::Command::new("echo")
.arg("Hello, World!")
.output()
.await;
assert!(cmd.is_ok());
let output = cmd.unwrap();
assert!(output.status.success());
assert!(String::from_utf8_lossy(&output.stdout).contains("Hello, World!"));
}
}