#[cfg(test)]
mod tests {
use crate::cook::workflow::validation::*;
use crate::cook::workflow::{WorkflowContext, WorkflowStep, CaptureOutput};
use crate::cook::interaction::MockUserInteraction;
use crate::cook::execution::ClaudeExecutor;
use crate::cook::orchestrator::ExecutionEnvironment;
use crate::cook::session::SessionManager;
use crate::cook::workflow::WorkflowExecutor as WorkflowExecutorImpl;
use anyhow::Result;
use async_trait::async_trait;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use tempfile::TempDir;
struct MockClaudeExecutor;
#[async_trait]
impl ClaudeExecutor for MockClaudeExecutor {
async fn execute_claude_command(
&self,
_command: &str,
_working_dir: &PathBuf,
_env_vars: HashMap<String, String>,
) -> Result<crate::cook::execution::ExecutionResult> {
Ok(crate::cook::execution::ExecutionResult {
success: true,
stdout: "Command executed".to_string(),
stderr: String::new(),
exit_code: Some(0),
metadata: HashMap::new(),
})
}
}
struct MockSessionManager;
#[async_trait]
impl SessionManager for MockSessionManager {
async fn update_session(&self, _update: crate::cook::session::SessionUpdate) -> Result<()> {
Ok(())
}
async fn get_session_state(&self) -> Result<crate::cook::session::SessionState> {
Ok(crate::cook::session::SessionState::default())
}
async fn save_checkpoint(&self, _checkpoint: crate::cook::session::SessionCheckpoint) -> Result<()> {
Ok(())
}
}
#[test]
fn test_validation_config_creation() {
let config = ValidationConfig {
command: Some("/prodigy-validate-spec 01".to_string()),
shell: None,
claude: None,
commands: None,
expected_schema: None,
threshold: 95.0,
timeout: Some(30),
result_file: None,
on_incomplete: Some(OnIncompleteConfig {
claude: Some("/prodigy-fix-gaps".to_string()),
shell: None,
commands: None,
prompt: None,
max_attempts: 2,
fail_workflow: true,
commit_required: false,
}),
};
assert!(config.validate().is_ok());
assert_eq!(config.threshold, 95.0);
}
#[test]
fn test_validation_result_helpers() {
let result = ValidationResult::complete();
assert_eq!(result.status, ValidationStatus::Complete);
assert_eq!(result.completion_percentage, 100.0);
let mut gaps = HashMap::new();
gaps.insert(
"auth".to_string(),
GapDetail {
description: "Missing authentication".to_string(),
location: Some("src/auth.rs".to_string()),
severity: Severity::Critical,
suggested_fix: None,
},
);
let incomplete = ValidationResult::incomplete(
75.0,
vec!["Authentication".to_string()],
gaps,
);
assert_eq!(incomplete.status, ValidationStatus::Incomplete);
assert_eq!(incomplete.completion_percentage, 75.0);
assert_eq!(incomplete.missing.len(), 1);
let failed = ValidationResult::failed("Error message".to_string());
assert_eq!(failed.status, ValidationStatus::Failed);
assert_eq!(failed.completion_percentage, 0.0);
}
#[test]
fn test_validation_config_is_complete() {
let config = ValidationConfig {
command: Some("cargo test".to_string()),
claude: None,
expected_schema: None,
threshold: 80.0,
timeout: None,
on_incomplete: None,
result_file: None,
};
let passing_result = ValidationResult {
completion_percentage: 85.0,
status: ValidationStatus::Complete,
implemented: vec![],
missing: vec![],
gaps: HashMap::new(),
raw_output: None,
};
let failing_result = ValidationResult {
completion_percentage: 75.0,
status: ValidationStatus::Incomplete,
implemented: vec![],
missing: vec!["Some tests".to_string()],
gaps: HashMap::new(),
raw_output: None,
};
assert!(config.is_complete(&passing_result));
assert!(!config.is_complete(&failing_result));
}
#[test]
fn test_workflow_context_interpolation_with_validation() {
let mut ctx = WorkflowContext::default();
let validation = ValidationResult {
completion_percentage: 85.5,
status: ValidationStatus::Incomplete,
implemented: vec!["Feature A".to_string()],
missing: vec!["Feature B".to_string(), "Feature C".to_string()],
gaps: {
let mut gaps = HashMap::new();
gaps.insert(
"feature_b".to_string(),
GapDetail {
description: "Feature B not implemented".to_string(),
location: None,
severity: Severity::High,
suggested_fix: None,
},
);
gaps
},
raw_output: None,
};
ctx.validation_results.insert("spec".to_string(), validation);
let template = "Completion: ${spec.completion}%, Missing: ${spec.missing}, Gaps: ${spec.gaps}";
let result = ctx.interpolate(template);
assert!(result.contains("85.5"));
assert!(result.contains("Feature B, Feature C"));
assert!(result.contains("Feature B not implemented"));
}
#[test]
fn test_on_incomplete_config_validation() {
let valid = OnIncompleteConfig {
strategy: CompletionStrategy::PatchGaps,
claude: Some("/prodigy-fix".to_string()),
shell: None,
prompt: None,
max_attempts: 3,
fail_workflow: false,
};
assert!(valid.validate().is_ok());
assert!(valid.has_command());
let invalid = OnIncompleteConfig {
strategy: CompletionStrategy::PatchGaps,
claude: None,
shell: None,
prompt: None,
max_attempts: 2,
fail_workflow: true,
};
assert!(invalid.validate().is_err());
assert!(!invalid.has_command());
let interactive = OnIncompleteConfig {
strategy: CompletionStrategy::Interactive,
claude: None,
shell: None,
prompt: Some("Continue?".to_string()),
max_attempts: 1,
fail_workflow: false,
};
assert!(interactive.validate().is_ok());
let zero_attempts = OnIncompleteConfig {
strategy: CompletionStrategy::RetryFull,
claude: Some("/prodigy-retry".to_string()),
shell: None,
prompt: None,
max_attempts: 0,
fail_workflow: true,
};
assert!(zero_attempts.validate().is_err());
}
#[test]
fn test_validation_workflow_step() {
let step = WorkflowStep {
name: None,
claude: Some("/prodigy-implement-spec 01".to_string()),
shell: None,
test: None,
command: None,
handler: None,
timeout: None,
capture_output: CaptureOutput::Disabled,
on_failure: None,
retry: None,
on_success: None,
on_exit_code: Default::default(),
commit_required: true,
working_dir: None,
env: Default::default(),
validate: Some(ValidationConfig {
command: Some("/prodigy-validate-spec 01".to_string()),
claude: None,
expected_schema: None,
threshold: 100.0,
timeout: None,
result_file: None,
on_incomplete: Some(OnIncompleteConfig {
claude: Some("/prodigy-complete-spec 01".to_string()),
shell: None,
prompt: None,
max_attempts: 2,
fail_workflow: true,
commit_required: false,
}),
}),
};
assert!(step.validate.is_some());
let validation = step.validate.unwrap();
assert_eq!(validation.threshold, 100.0);
}
#[tokio::test]
async fn test_validation_execution_flow() {
let temp_dir = TempDir::new().unwrap();
let env = ExecutionEnvironment {
working_dir: temp_dir.path().to_path_buf(),
project_dir: temp_dir.path().to_path_buf(),
claude_exe: PathBuf::from("claude"),
environment: Default::default(),
};
let step = WorkflowStep {
name: None,
claude: Some("/test-command".to_string()),
shell: None,
test: None,
command: None,
handler: None,
timeout: None,
capture_output: CaptureOutput::Disabled,
on_failure: None,
retry: None,
on_success: None,
on_exit_code: Default::default(),
commit_required: false,
working_dir: None,
env: Default::default(),
validate: Some(ValidationConfig {
command: Some("echo '{\"completion_percentage\": 100, \"status\": \"complete\"}'".to_string()),
claude: None,
expected_schema: None,
threshold: 100.0,
timeout: None,
on_incomplete: None,
result_file: None,
}),
};
assert!(step.validate.is_some());
}
#[test]
fn test_gaps_summary() {
let mut gaps = HashMap::new();
gaps.insert(
"rbac".to_string(),
GapDetail {
description: "Role-based access control missing".to_string(),
location: Some("src/auth/rbac.rs".to_string()),
severity: Severity::Critical,
suggested_fix: Some("Implement RBAC middleware".to_string()),
},
);
gaps.insert(
"logging".to_string(),
GapDetail {
description: "Audit logging not implemented".to_string(),
location: None,
severity: Severity::Medium,
suggested_fix: None,
},
);
let result = ValidationResult {
completion_percentage: 60.0,
status: ValidationStatus::Incomplete,
implemented: vec![],
missing: vec![],
gaps,
raw_output: None,
};
let summary = result.gaps_summary();
assert!(summary.contains("Role-based access control missing"));
assert!(summary.contains("Audit logging not implemented"));
assert!(summary.contains("critical"));
assert!(summary.contains("medium"));
}
#[test]
fn test_validation_config_array_format() {
let yaml = r#"
- shell: "debtmap analyze . --lcov target/coverage/lcov.info --output .prodigy/debtmap-after.json --format json"
- shell: "debtmap compare --before .prodigy/debtmap-before.json --after .prodigy/debtmap-after.json --output .prodigy/comparison.json --format json"
- claude: "/prodigy-validate-debtmap-improvement --comparison .prodigy/comparison.json --output .prodigy/debtmap-validation.json"
result_file: ".prodigy/debtmap-validation.json"
threshold: 75
"#;
let config: ValidationConfig = serde_yaml::from_str(yaml).expect("Failed to parse validation config array");
assert!(config.commands.is_some());
let commands = config.commands.unwrap();
assert_eq!(commands.len(), 3);
assert_eq!(config.threshold, 75.0);
}
#[test]
fn test_validation_config_object_with_commands() {
let yaml = r#"
commands:
- shell: "prep-command-1"
- shell: "prep-command-2"
- claude: "/validate-command"
result_file: "results.json"
threshold: 80
"#;
let config: ValidationConfig = serde_yaml::from_str(yaml).expect("Failed to parse validation config with commands field");
assert!(config.commands.is_some());
let commands = config.commands.unwrap();
assert_eq!(commands.len(), 3);
assert_eq!(config.threshold, 80.0);
assert_eq!(config.result_file, Some("results.json".to_string()));
}
#[test]
fn test_validation_config_single_command() {
let yaml = r#"
claude: "/validate-command"
threshold: 90
result_file: "validation.json"
"#;
let config: ValidationConfig = serde_yaml::from_str(yaml).expect("Failed to parse validation config single command");
assert!(config.commands.is_none());
assert_eq!(config.claude, Some("/validate-command".to_string()));
assert_eq!(config.threshold, 90.0);
assert_eq!(config.result_file, Some("validation.json".to_string()));
}
#[test]
fn test_on_incomplete_array_format() {
let yaml = r#"
- claude: "/prodigy-complete-debtmap-fix --gaps ${validation.gaps}"
commit_required: true
- shell: "just coverage-lcov"
- shell: "debtmap analyze . --output .prodigy/debtmap-after.json"
"#;
let config: OnIncompleteConfig = serde_yaml::from_str(yaml).expect("Failed to parse on_incomplete config array");
assert!(config.commands.is_some());
let commands = config.commands.unwrap();
assert_eq!(commands.len(), 3);
}
#[test]
fn test_on_incomplete_object_format() {
let yaml = r#"
claude: "/prodigy-fix-gaps"
max_attempts: 3
fail_workflow: false
commit_required: true
"#;
let config: OnIncompleteConfig = serde_yaml::from_str(yaml).expect("Failed to parse on_incomplete config object");
assert!(config.commands.is_none());
assert_eq!(config.claude, Some("/prodigy-fix-gaps".to_string()));
assert_eq!(config.max_attempts, 3);
assert_eq!(config.fail_workflow, false);
assert_eq!(config.commit_required, true);
}
#[test]
fn test_nested_validation_with_arrays() {
let yaml = r#"
validate:
- shell: "debtmap analyze . --lcov target/coverage/lcov.info --output .prodigy/debtmap-after.json --format json"
- shell: "debtmap compare --before .prodigy/debtmap-before.json --after .prodigy/debtmap-after.json --output .prodigy/comparison.json --format json"
- claude: "/prodigy-validate-debtmap-improvement --comparison .prodigy/comparison.json --output .prodigy/debtmap-validation.json"
result_file: ".prodigy/debtmap-validation.json"
threshold: 75
on_incomplete:
- claude: "/prodigy-complete-debtmap-fix --gaps ${validation.gaps}"
commit_required: true
- shell: "just coverage-lcov"
- shell: "debtmap analyze . --output .prodigy/debtmap-after.json"
"#;
#[derive(serde::Deserialize)]
struct TestStruct {
validate: ValidationConfig,
}
let result: TestStruct = serde_yaml::from_str(yaml).expect("Failed to parse nested validation config");
assert!(result.validate.commands.is_some());
let commands = result.validate.commands.unwrap();
assert_eq!(commands.len(), 3);
assert_eq!(result.validate.threshold, 75.0);
assert!(result.validate.on_incomplete.is_some());
let on_incomplete = result.validate.on_incomplete.unwrap();
assert!(on_incomplete.commands.is_some());
let on_incomplete_cmds = on_incomplete.commands.unwrap();
assert_eq!(on_incomplete_cmds.len(), 3);
}
}