use anyhow::Result;
use std::collections::HashMap;
use tempfile::TempDir;
use xchecker::orchestrator::{OrchestratorConfig, PhaseOrchestrator};
use xchecker::types::{ErrorKind, PhaseId};
fn setup_test_environment(test_name: &str) -> (PhaseOrchestrator, TempDir) {
let temp_dir = xchecker::paths::with_isolated_home();
let spec_id = format!("test-resume-{test_name}");
let orchestrator = PhaseOrchestrator::new(&spec_id).unwrap();
(orchestrator, temp_dir)
}
#[tokio::test]
async fn test_resume_from_requirements() -> Result<()> {
let (orchestrator, _temp_dir) = setup_test_environment("from-requirements");
let config = OrchestratorConfig {
dry_run: true,
..Default::default()
};
let result = orchestrator.execute_requirements_phase(&config).await?;
assert!(result.success, "Requirements phase should succeed");
let resume_result = orchestrator
.resume_from_phase(PhaseId::Requirements, &config)
.await?;
assert!(
resume_result.success,
"Resume from Requirements should succeed"
);
assert_eq!(resume_result.phase, PhaseId::Requirements);
Ok(())
}
#[tokio::test]
async fn test_resume_from_design() -> Result<()> {
let (orchestrator, _temp_dir) = setup_test_environment("from-design");
let config = OrchestratorConfig {
dry_run: true,
..Default::default()
};
orchestrator.execute_requirements_phase(&config).await?;
let result = orchestrator.execute_design_phase(&config).await?;
assert!(result.success, "Design phase should succeed");
let resume_result = orchestrator
.resume_from_phase(PhaseId::Design, &config)
.await?;
assert!(resume_result.success, "Resume from Design should succeed");
assert_eq!(resume_result.phase, PhaseId::Design);
Ok(())
}
#[tokio::test]
async fn test_resume_from_tasks() -> Result<()> {
let (orchestrator, _temp_dir) = setup_test_environment("from-tasks");
let config = OrchestratorConfig {
dry_run: true,
..Default::default()
};
orchestrator.execute_requirements_phase(&config).await?;
orchestrator.execute_design_phase(&config).await?;
let result = orchestrator.execute_tasks_phase(&config).await?;
assert!(result.success, "Tasks phase should succeed");
let resume_result = orchestrator
.resume_from_phase(PhaseId::Tasks, &config)
.await?;
assert!(resume_result.success, "Resume from Tasks should succeed");
assert_eq!(resume_result.phase, PhaseId::Tasks);
Ok(())
}
#[tokio::test]
async fn test_resume_with_missing_dependency_fails() -> Result<()> {
let (orchestrator, _temp_dir) = setup_test_environment("missing-dep");
let config = OrchestratorConfig {
dry_run: true,
..Default::default()
};
let result = orchestrator
.resume_from_phase(PhaseId::Design, &config)
.await;
assert!(
result.is_err(),
"Resume should fail with missing 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"),
"Error should mention dependency or Requirements: {err_msg}"
);
Ok(())
}
#[tokio::test]
async fn test_resume_with_failed_dependency_fails() -> 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-resume-failed-dep",
PhaseId::Requirements,
1, vec![],
"0.1.0",
"0.8.1",
"haiku",
None,
HashMap::new(),
xchecker::types::PacketEvidence {
files: vec![],
max_bytes: 65536,
max_lines: 1200,
},
None, None, vec![], None, "native", None, Some(ErrorKind::Unknown), Some("Test failure".to_string()), None, None, );
receipt_manager.write_receipt(&failed_receipt)?;
let config = OrchestratorConfig {
dry_run: true,
..Default::default()
};
let result = orchestrator
.resume_from_phase(PhaseId::Design, &config)
.await;
assert!(result.is_err(), "Resume should fail when dependency failed");
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("failed")
|| 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_resume_with_partial_artifact_deletes_and_restarts() -> Result<()> {
let (orchestrator, _temp_dir) = setup_test_environment("partial-artifact");
let config = OrchestratorConfig {
dry_run: true,
..Default::default()
};
orchestrator.execute_requirements_phase(&config).await?;
let partial_artifact = xchecker::artifact::Artifact {
name: "10-design.partial.md".to_string(),
content: "# Partial Design\n\nThis is a partial artifact from a failed run.".to_string(),
artifact_type: xchecker::artifact::ArtifactType::Partial,
blake3_hash: blake3::hash(b"partial content").to_hex().to_string(),
};
orchestrator
.artifact_manager()
.store_artifact(&partial_artifact)?;
assert!(
orchestrator
.artifact_manager()
.has_partial_artifact(PhaseId::Design),
"Partial artifact should exist before resume"
);
let resume_result = orchestrator
.resume_from_phase(PhaseId::Design, &config)
.await?;
assert!(
resume_result.success,
"Resume should succeed after deleting partial"
);
assert_eq!(resume_result.phase, PhaseId::Design);
Ok(())
}
#[tokio::test]
async fn test_current_state_detection() -> Result<()> {
let (orchestrator, _temp_dir) = setup_test_environment("state-detection");
let config = OrchestratorConfig {
dry_run: true,
..Default::default()
};
let current = orchestrator.get_current_phase_state()?;
assert!(current.is_none(), "Fresh spec should have no current phase");
orchestrator.execute_requirements_phase(&config).await?;
let current = orchestrator.get_current_phase_state()?;
assert_eq!(
current,
Some(PhaseId::Requirements),
"Current phase should be Requirements"
);
orchestrator.execute_design_phase(&config).await?;
let current = orchestrator.get_current_phase_state()?;
assert_eq!(
current,
Some(PhaseId::Design),
"Current phase should be Design"
);
orchestrator.execute_tasks_phase(&config).await?;
let current = orchestrator.get_current_phase_state()?;
assert_eq!(
current,
Some(PhaseId::Tasks),
"Current phase should be Tasks"
);
Ok(())
}
#[tokio::test]
async fn test_resume_validates_transition() -> Result<()> {
let (orchestrator, _temp_dir) = setup_test_environment("validate-transition");
let config = OrchestratorConfig {
dry_run: true,
..Default::default()
};
let result = orchestrator
.resume_from_phase(PhaseId::Tasks, &config)
.await;
assert!(
result.is_err(),
"Resume should validate transition and fail"
);
orchestrator.execute_requirements_phase(&config).await?;
let result = orchestrator
.resume_from_phase(PhaseId::Tasks, &config)
.await;
assert!(
result.is_err(),
"Resume should validate transition and fail without Design"
);
orchestrator.execute_design_phase(&config).await?;
let result = orchestrator
.resume_from_phase(PhaseId::Tasks, &config)
.await;
assert!(
result.is_ok(),
"Resume should succeed after all dependencies satisfied"
);
Ok(())
}
#[tokio::test]
async fn test_resume_after_timeout() -> Result<()> {
let (orchestrator, _temp_dir) = setup_test_environment("after-timeout");
let receipt_manager =
xchecker::receipt::ReceiptManager::new(orchestrator.artifact_manager().base_path());
let timeout_receipt = receipt_manager.create_receipt(
"test-resume-after-timeout",
PhaseId::Requirements,
10, vec![],
"0.1.0",
"0.8.1",
"haiku",
None,
HashMap::new(),
xchecker::types::PacketEvidence {
files: vec![],
max_bytes: 65536,
max_lines: 1200,
},
None, None, vec!["phase_timeout:600".to_string()], None, "native", None, Some(ErrorKind::PhaseTimeout), Some("Phase timed out after 600 seconds".to_string()), None, None, );
receipt_manager.write_receipt(&timeout_receipt)?;
let config = OrchestratorConfig {
dry_run: true,
..Default::default()
};
let resume_result = orchestrator
.resume_from_phase(PhaseId::Requirements, &config)
.await?;
assert!(resume_result.success, "Resume should succeed after timeout");
assert_eq!(resume_result.phase, PhaseId::Requirements);
Ok(())
}
#[tokio::test]
async fn test_resume_cleans_up_partial_on_success() -> Result<()> {
let (orchestrator, _temp_dir) = setup_test_environment("cleanup-partial");
let config = OrchestratorConfig {
dry_run: true,
..Default::default()
};
orchestrator.execute_requirements_phase(&config).await?;
let partial_artifact = xchecker::artifact::Artifact {
name: "10-design.partial.md".to_string(),
content: "# Partial Design".to_string(),
artifact_type: xchecker::artifact::ArtifactType::Partial,
blake3_hash: blake3::hash(b"partial").to_hex().to_string(),
};
orchestrator
.artifact_manager()
.store_artifact(&partial_artifact)?;
let result = orchestrator
.resume_from_phase(PhaseId::Design, &config)
.await?;
assert!(result.success, "Resume should succeed");
assert_eq!(result.phase, PhaseId::Design);
Ok(())
}
#[tokio::test]
async fn test_can_resume_from_phase() -> Result<()> {
let (orchestrator, _temp_dir) = setup_test_environment("can-resume");
let config = OrchestratorConfig {
dry_run: true,
..Default::default()
};
assert!(
orchestrator.can_resume_from_phase_public(PhaseId::Requirements)?,
"Should be able to resume Requirements on fresh spec"
);
assert!(
!orchestrator.can_resume_from_phase_public(PhaseId::Design)?,
"Should not be able to resume Design without Requirements"
);
orchestrator.execute_requirements_phase(&config).await?;
assert!(
orchestrator.can_resume_from_phase_public(PhaseId::Design)?,
"Should be able to resume Design after Requirements"
);
assert!(
!orchestrator.can_resume_from_phase_public(PhaseId::Tasks)?,
"Should not be able to resume Tasks without Design"
);
orchestrator.execute_design_phase(&config).await?;
assert!(
orchestrator.can_resume_from_phase_public(PhaseId::Tasks)?,
"Should be able to resume Tasks after Design"
);
Ok(())
}
#[tokio::test]
async fn test_resume_from_each_phase_in_sequence() -> Result<()> {
let (orchestrator, _temp_dir) = setup_test_environment("each-phase");
let config = OrchestratorConfig {
dry_run: true,
..Default::default()
};
orchestrator.execute_requirements_phase(&config).await?;
let result = orchestrator
.resume_from_phase(PhaseId::Requirements, &config)
.await?;
assert!(result.success && result.phase == PhaseId::Requirements);
orchestrator.execute_design_phase(&config).await?;
let result = orchestrator
.resume_from_phase(PhaseId::Design, &config)
.await?;
assert!(result.success && result.phase == PhaseId::Design);
orchestrator.execute_tasks_phase(&config).await?;
let result = orchestrator
.resume_from_phase(PhaseId::Tasks, &config)
.await?;
assert!(result.success && result.phase == PhaseId::Tasks);
Ok(())
}