use anyhow::Result;
use tempfile::TempDir;
use xchecker::orchestrator::{OrchestratorConfig, PhaseOrchestrator};
use xchecker::types::PhaseId;
fn setup_test_environment(test_name: &str) -> (PhaseOrchestrator, TempDir) {
let temp_dir = xchecker::paths::with_isolated_home();
let spec_id = format!("test-transition-{}", test_name);
let orchestrator = PhaseOrchestrator::new(&spec_id).unwrap();
(orchestrator, temp_dir)
}
#[test]
fn test_fresh_spec_can_only_start_with_requirements() -> Result<()> {
let (orchestrator, _temp_dir) = setup_test_environment("fresh-requirements");
let result = orchestrator.validate_transition(PhaseId::Requirements);
assert!(result.is_ok(), "Fresh spec should allow Requirements phase");
let result = orchestrator.validate_transition(PhaseId::Design);
assert!(
result.is_err(),
"Fresh spec should not allow Design phase without Requirements"
);
let result = orchestrator.validate_transition(PhaseId::Tasks);
assert!(
result.is_err(),
"Fresh spec should not allow Tasks phase without Design"
);
let result = orchestrator.validate_transition(PhaseId::Review);
assert!(
result.is_err(),
"Fresh spec should not allow Review phase without Tasks"
);
let result = orchestrator.validate_transition(PhaseId::Fixup);
assert!(
result.is_err(),
"Fresh spec should not allow Fixup phase without Review"
);
let result = orchestrator.validate_transition(PhaseId::Final);
assert!(
result.is_err(),
"Fresh spec should not allow Final phase without Tasks"
);
Ok(())
}
#[tokio::test]
async fn test_legal_transition_requirements_to_design() -> Result<()> {
let (orchestrator, _temp_dir) = setup_test_environment("req-to-design");
let config = OrchestratorConfig {
dry_run: true,
..Default::default()
};
let result = orchestrator.execute_requirements_phase(&config).await;
assert!(result.is_ok(), "Requirements phase should succeed");
let validation = orchestrator.validate_transition(PhaseId::Design);
assert!(
validation.is_ok(),
"Design phase should be allowed after Requirements"
);
let validation = orchestrator.validate_transition(PhaseId::Requirements);
assert!(validation.is_ok(), "Requirements phase can be re-run");
let validation = orchestrator.validate_transition(PhaseId::Tasks);
assert!(
validation.is_err(),
"Tasks phase should not be allowed without Design"
);
Ok(())
}
#[tokio::test]
async fn test_legal_transition_design_to_tasks() -> Result<()> {
let (orchestrator, _temp_dir) = setup_test_environment("design-to-tasks");
let config = OrchestratorConfig {
dry_run: true,
..Default::default()
};
orchestrator.execute_requirements_phase(&config).await?;
orchestrator.execute_design_phase(&config).await?;
let validation = orchestrator.validate_transition(PhaseId::Tasks);
assert!(
validation.is_ok(),
"Tasks phase should be allowed after Design"
);
let validation = orchestrator.validate_transition(PhaseId::Design);
assert!(validation.is_ok(), "Design phase can be re-run");
let validation = orchestrator.validate_transition(PhaseId::Review);
assert!(
validation.is_err(),
"Review phase should not be allowed without Tasks"
);
Ok(())
}
#[tokio::test]
async fn test_legal_transition_tasks_to_review_or_final() -> Result<()> {
let (orchestrator, _temp_dir) = setup_test_environment("tasks-to-review-final");
let config = OrchestratorConfig {
dry_run: true,
..Default::default()
};
orchestrator.execute_requirements_phase(&config).await?;
orchestrator.execute_design_phase(&config).await?;
orchestrator.execute_tasks_phase(&config).await?;
let validation = orchestrator.validate_transition(PhaseId::Review);
assert!(
validation.is_ok(),
"Review phase should be allowed after Tasks"
);
let validation = orchestrator.validate_transition(PhaseId::Final);
assert!(
validation.is_ok(),
"Final phase should be allowed after Tasks (can skip Review/Fixup)"
);
let validation = orchestrator.validate_transition(PhaseId::Tasks);
assert!(validation.is_ok(), "Tasks phase can be re-run");
let validation = orchestrator.validate_transition(PhaseId::Fixup);
assert!(
validation.is_err(),
"Fixup phase should not be allowed without Review"
);
Ok(())
}
#[test]
fn test_illegal_transition_provides_guidance() -> Result<()> {
let (orchestrator, _temp_dir) = setup_test_environment("illegal-guidance");
let result = orchestrator.validate_transition(PhaseId::Design);
assert!(result.is_err(), "Design without Requirements should fail");
let err = result.unwrap_err();
let err_msg = err.to_string();
assert!(
err_msg.contains("Design")
|| err_msg.contains("design")
|| err_msg.contains("Requirements")
|| err_msg.contains("requirements")
|| err_msg.contains("dependency")
|| err_msg.contains("transition"),
"Error should mention Design, Requirements, dependency, or transition: {}",
err_msg
);
Ok(())
}
#[tokio::test]
async fn test_dependency_not_satisfied_error() -> Result<()> {
let (orchestrator, _temp_dir) = setup_test_environment("dep-not-satisfied");
let result = orchestrator.validate_transition(PhaseId::Design);
assert!(
result.is_err(),
"Design should fail without Requirements dependency"
);
let err = result.unwrap_err();
let err_msg = err.to_string();
assert!(
err_msg.contains("dependency")
|| err_msg.contains("Requirements")
|| err_msg.contains("requirements")
|| err_msg.contains("transition")
|| err_msg.contains("Design")
|| err_msg.contains("design"),
"Error should mention dependency, Requirements, or transition: {}",
err_msg
);
Ok(())
}
#[tokio::test]
async fn test_failed_dependency_prevents_transition() -> Result<()> {
let (orchestrator, _temp_dir) = setup_test_environment("failed-dep");
let receipt_manager =
xchecker::receipt::ReceiptManager::new(orchestrator.artifact_manager().base_path());
let failed_receipt = receipt_manager.create_receipt(
"test-transition-failed-dep",
PhaseId::Requirements,
1, vec![],
"0.1.0",
"0.8.1",
"haiku",
None,
std::collections::HashMap::new(),
xchecker::types::PacketEvidence {
files: vec![],
max_bytes: 65536,
max_lines: 1200,
},
None, None, vec![], None, "native", None, Some(xchecker::types::ErrorKind::Unknown), Some("Test failure".to_string()), None, None, );
receipt_manager.write_receipt(&failed_receipt)?;
let result = orchestrator.validate_transition(PhaseId::Design);
assert!(
result.is_err(),
"Design should fail when Requirements dependency failed"
);
Ok(())
}
#[tokio::test]
async fn test_complete_legal_workflow() -> Result<()> {
let (orchestrator, _temp_dir) = setup_test_environment("complete-workflow");
let config = OrchestratorConfig {
dry_run: true,
..Default::default()
};
assert!(
orchestrator
.validate_transition(PhaseId::Requirements)
.is_ok(),
"Requirements should be allowed on fresh spec"
);
orchestrator.execute_requirements_phase(&config).await?;
assert!(
orchestrator.validate_transition(PhaseId::Design).is_ok(),
"Design should be allowed after Requirements"
);
orchestrator.execute_design_phase(&config).await?;
assert!(
orchestrator.validate_transition(PhaseId::Tasks).is_ok(),
"Tasks should be allowed after Design"
);
orchestrator.execute_tasks_phase(&config).await?;
assert!(
orchestrator.validate_transition(PhaseId::Review).is_ok(),
"Review should be allowed after Tasks"
);
assert!(
orchestrator.validate_transition(PhaseId::Final).is_ok(),
"Final should be allowed after Tasks"
);
Ok(())
}
#[tokio::test]
async fn test_phases_can_be_rerun() -> Result<()> {
let (orchestrator, _temp_dir) = setup_test_environment("rerun-phases");
let config = OrchestratorConfig {
dry_run: true,
..Default::default()
};
orchestrator.execute_requirements_phase(&config).await?;
assert!(
orchestrator
.validate_transition(PhaseId::Requirements)
.is_ok(),
"Requirements can be re-run"
);
orchestrator.execute_requirements_phase(&config).await?;
orchestrator.execute_design_phase(&config).await?;
assert!(
orchestrator.validate_transition(PhaseId::Design).is_ok(),
"Design can be re-run"
);
orchestrator.execute_design_phase(&config).await?;
orchestrator.execute_tasks_phase(&config).await?;
assert!(
orchestrator.validate_transition(PhaseId::Tasks).is_ok(),
"Tasks can be re-run"
);
Ok(())
}
#[test]
fn test_invalid_transition_exit_code() -> Result<()> {
let (orchestrator, _temp_dir) = setup_test_environment("exit-code");
let result = orchestrator.validate_transition(PhaseId::Design);
assert!(result.is_err());
let err = result.unwrap_err();
let (exit_code, error_kind) = (&err).into();
assert_eq!(exit_code, 2, "Invalid transition should map to exit code 2");
assert_eq!(
error_kind,
xchecker::types::ErrorKind::CliArgs,
"Invalid transition should map to CliArgs error kind"
);
Ok(())
}
#[tokio::test]
async fn test_design_on_fresh_spec_error_specificity() -> Result<()> {
let (orchestrator, _temp_dir) = setup_test_environment("design-fresh-error");
let config = OrchestratorConfig {
dry_run: true,
..Default::default()
};
let result = orchestrator.execute_design_phase(&config).await;
assert!(result.is_err(), "Should return Err for invalid transition");
let err = result.unwrap_err();
let xchecker_err = err
.downcast_ref::<xchecker::error::XCheckerError>()
.expect("Error should be XCheckerError");
let (exit_code, error_kind) = xchecker_err.into();
assert_eq!(
exit_code, 2,
"Invalid transition should map to exit code 2 (CLI_ARGS)"
);
assert_eq!(
error_kind,
xchecker::types::ErrorKind::CliArgs,
"error_kind should be CliArgs"
);
let error_msg = err.to_string();
assert!(
error_msg.contains("transition")
|| error_msg.contains("Requirements")
|| error_msg.contains("requirements"),
"Error should mention transition or Requirements: {}",
error_msg
);
match xchecker_err {
xchecker::error::XCheckerError::Phase(phase_err) => match phase_err {
xchecker::error::PhaseError::InvalidTransition { from, to } => {
assert_eq!(to, "design", "Target phase should be design");
assert!(
from.contains("none") || from.contains("fresh"),
"From phase should indicate fresh spec: {}",
from
);
}
_ => panic!("Expected InvalidTransition error, got: {:?}", phase_err),
},
_ => panic!("Expected Phase error, got: {:?}", xchecker_err),
}
Ok(())
}
#[tokio::test]
async fn test_tasks_without_design_dependency_error() -> Result<()> {
let (orchestrator, _temp_dir) = setup_test_environment("tasks-no-design");
let config = OrchestratorConfig {
dry_run: true,
..Default::default()
};
orchestrator.execute_requirements_phase(&config).await?;
let result = orchestrator.execute_tasks_phase(&config).await;
assert!(
result.is_err(),
"Should return Err for dependency not satisfied"
);
let err = result.unwrap_err();
let xchecker_err = err
.downcast_ref::<xchecker::error::XCheckerError>()
.expect("Error should be XCheckerError");
let (exit_code, error_kind) = xchecker_err.into();
assert_eq!(
exit_code, 2,
"Dependency not satisfied should map to exit code 2 (CLI_ARGS)"
);
assert_eq!(
error_kind,
xchecker::types::ErrorKind::CliArgs,
"error_kind should be CliArgs"
);
let error_msg = err.to_string();
assert!(
error_msg.contains("dependency")
|| error_msg.contains("Design")
|| error_msg.contains("design")
|| error_msg.contains("transition"),
"Error should mention dependency, Design, or transition: {}",
error_msg
);
match xchecker_err {
xchecker::error::XCheckerError::Phase(phase_err) => match phase_err {
xchecker::error::PhaseError::DependencyNotSatisfied { phase, dependency } => {
assert_eq!(phase, "tasks", "Phase should be tasks");
assert_eq!(dependency, "design", "Dependency should be design");
}
xchecker::error::PhaseError::InvalidTransition { from, to } => {
assert_eq!(to, "tasks", "Target phase should be tasks");
assert!(
from.contains("requirements"),
"From phase should be requirements: {}",
from
);
}
_ => panic!(
"Expected DependencyNotSatisfied or InvalidTransition error, got: {:?}",
phase_err
),
},
_ => panic!("Expected Phase error, got: {:?}", xchecker_err),
}
Ok(())
}