use anyhow::Result;
use std::collections::HashMap;
fn dry_run_config() -> xchecker::orchestrator::OrchestratorConfig {
xchecker::orchestrator::OrchestratorConfig {
dry_run: true,
config: HashMap::new(),
full_config: None,
selectors: None,
strict_validation: false,
redactor: Default::default(),
hooks: None,
}
}
fn unique_spec_id(test_name: &str) -> String {
format!("engine-invariants-{}-{}", test_name, std::process::id())
}
#[tokio::test]
async fn test_core_output_has_packet_evidence() -> Result<()> {
let _temp = xchecker::paths::with_isolated_home();
let spec_id = unique_spec_id("packet-evidence");
let config = dry_run_config();
let mut handle =
xchecker::orchestrator::OrchestratorHandle::with_config_and_force(&spec_id, config, false)?;
let result = handle
.run_phase(xchecker::types::PhaseId::Requirements)
.await?;
assert!(result.success, "Phase should succeed");
let receipt_path = result.receipt_path.expect("Should have receipt path");
let receipt_content = std::fs::read_to_string(&receipt_path)?;
let receipt: serde_json::Value = serde_json::from_str(&receipt_content)?;
assert!(
receipt["packet"].is_object(),
"Receipt should have packet object"
);
let packet = &receipt["packet"];
assert!(
packet["max_bytes"].is_number(),
"Packet should have max_bytes"
);
assert!(
packet["max_lines"].is_number(),
"Packet should have max_lines"
);
assert!(packet["files"].is_array(), "Packet should have files array");
assert!(
packet["max_bytes"].as_u64().unwrap_or(0) > 0,
"Packet max_bytes should be positive"
);
assert!(
packet["max_lines"].as_u64().unwrap_or(0) > 0,
"Packet max_lines should be positive"
);
Ok(())
}
#[tokio::test]
async fn test_core_output_success_has_artifacts() -> Result<()> {
let _temp = xchecker::paths::with_isolated_home();
let spec_id = unique_spec_id("success-artifacts");
let config = dry_run_config();
let mut handle =
xchecker::orchestrator::OrchestratorHandle::with_config_and_force(&spec_id, config, false)?;
let result = handle
.run_phase(xchecker::types::PhaseId::Requirements)
.await?;
assert!(result.success, "Phase should succeed");
assert_eq!(result.exit_code, 0, "Exit code should be 0");
assert!(
!result.artifact_paths.is_empty(),
"Successful phase should produce artifacts"
);
for artifact_path in &result.artifact_paths {
assert!(
artifact_path.exists(),
"Artifact should exist on disk: {:?}",
artifact_path
);
}
let receipt_path = result.receipt_path.expect("Should have receipt path");
let receipt_content = std::fs::read_to_string(&receipt_path)?;
let receipt: serde_json::Value = serde_json::from_str(&receipt_content)?;
assert!(
receipt["outputs"].is_array(),
"Receipt should have outputs array"
);
let outputs = receipt["outputs"].as_array().unwrap();
if !result.artifact_paths.is_empty() {
assert!(
!outputs.is_empty(),
"Receipt outputs should not be empty when artifacts exist"
);
}
Ok(())
}
#[tokio::test]
async fn test_core_output_has_hashes() -> Result<()> {
let _temp = xchecker::paths::with_isolated_home();
let spec_id = unique_spec_id("output-hashes");
let config = dry_run_config();
let mut handle =
xchecker::orchestrator::OrchestratorHandle::with_config_and_force(&spec_id, config, false)?;
let result = handle
.run_phase(xchecker::types::PhaseId::Requirements)
.await?;
assert!(result.success, "Phase should succeed");
let receipt_path = result.receipt_path.expect("Should have receipt path");
let receipt_content = std::fs::read_to_string(&receipt_path)?;
let receipt: serde_json::Value = serde_json::from_str(&receipt_content)?;
let outputs = receipt["outputs"]
.as_array()
.expect("Receipt should have outputs array");
let artifact_count = result.artifact_paths.len();
assert_eq!(
outputs.len(),
artifact_count,
"Number of output hashes should match number of artifacts"
);
for (idx, output) in outputs.iter().enumerate() {
assert!(output.is_object(), "Output {} should be an object", idx);
assert!(
output["path"].is_string(),
"Output {} should have path field",
idx
);
assert!(
output["blake3_canonicalized"].is_string(),
"Output {} should have blake3_canonicalized field",
idx
);
let hash = output["blake3_canonicalized"]
.as_str()
.expect("Hash should be string");
assert!(!hash.is_empty(), "Output {} hash should not be empty", idx);
assert!(
hash.len() >= 32, "Output {} hash should be reasonable length, got {}",
idx,
hash.len()
);
}
Ok(())
}
#[tokio::test]
async fn test_phase_execution_deterministic() -> Result<()> {
let _temp = xchecker::paths::with_isolated_home();
let spec_id = unique_spec_id("deterministic");
let config = dry_run_config();
let mut handle =
xchecker::orchestrator::OrchestratorHandle::with_config_and_force(&spec_id, config, false)?;
let result1 = handle
.run_phase(xchecker::types::PhaseId::Requirements)
.await?;
assert!(result1.success, "First run should succeed");
let receipt1_path = result1.receipt_path.expect("Should have receipt path");
let receipt1_content = std::fs::read_to_string(&receipt1_path)?;
let receipt1: serde_json::Value = serde_json::from_str(&receipt1_content)?;
let result2 = handle
.run_phase(xchecker::types::PhaseId::Requirements)
.await?;
assert!(result2.success, "Second run should succeed");
let receipt2_path = result2.receipt_path.expect("Should have receipt path");
let receipt2_content = std::fs::read_to_string(&receipt2_path)?;
let receipt2: serde_json::Value = serde_json::from_str(&receipt2_content)?;
assert_eq!(
receipt1["exit_code"], receipt2["exit_code"],
"Exit codes should match across runs"
);
assert_eq!(
result1.artifact_paths.len(),
result2.artifact_paths.len(),
"Artifact count should be consistent"
);
let outputs1 = receipt1["outputs"].as_array().unwrap();
let outputs2 = receipt2["outputs"].as_array().unwrap();
assert_eq!(
outputs1.len(),
outputs2.len(),
"Output count should be consistent"
);
Ok(())
}
#[tokio::test]
async fn test_receipts_have_required_metadata() -> Result<()> {
let _temp = xchecker::paths::with_isolated_home();
let spec_id = unique_spec_id("required-metadata");
let config = dry_run_config();
let mut handle =
xchecker::orchestrator::OrchestratorHandle::with_config_and_force(&spec_id, config, false)?;
let result = handle
.run_phase(xchecker::types::PhaseId::Requirements)
.await?;
let receipt_path = result.receipt_path.expect("Should have receipt path");
let receipt_content = std::fs::read_to_string(&receipt_path)?;
let receipt: serde_json::Value = serde_json::from_str(&receipt_content)?;
assert!(
receipt["schema_version"].is_string(),
"Receipt should have schema_version"
);
assert!(
receipt["emitted_at"].is_string(),
"Receipt should have emitted_at timestamp"
);
assert!(
receipt["spec_id"].is_string(),
"Receipt should have spec_id"
);
assert!(receipt["phase"].is_string(), "Receipt should have phase");
assert!(
receipt["xchecker_version"].is_string(),
"Receipt should have xchecker_version"
);
assert!(
receipt["exit_code"].is_number(),
"Receipt should have exit_code"
);
assert!(
receipt["packet"].is_object(),
"Receipt should have packet object"
);
assert!(
receipt["outputs"].is_array(),
"Receipt should have outputs array"
);
assert!(
receipt["flags"].is_object(),
"Receipt should have flags object"
);
assert!(
receipt["pipeline"].is_object(),
"Receipt should have pipeline object"
);
assert_eq!(
receipt["pipeline"]["execution_strategy"].as_str(),
Some("controlled"),
"Pipeline should have execution_strategy = controlled"
);
assert!(
receipt["llm"].is_object() || receipt["llm"].is_null(),
"Receipt should have llm field (object or null)"
);
Ok(())
}
#[tokio::test]
async fn test_artifacts_follow_naming_convention() -> Result<()> {
let _temp = xchecker::paths::with_isolated_home();
let spec_id = unique_spec_id("naming-convention");
let config = dry_run_config();
let mut handle =
xchecker::orchestrator::OrchestratorHandle::with_config_and_force(&spec_id, config, false)?;
let result = handle
.run_phase(xchecker::types::PhaseId::Requirements)
.await?;
assert!(result.success, "Phase should succeed");
assert!(!result.artifact_paths.is_empty(), "Should have artifacts");
let req_artifact = &result.artifact_paths[0];
let req_filename = req_artifact.file_name().unwrap().to_str().unwrap();
assert!(
req_filename.starts_with("00-") || req_filename.contains("requirements"),
"Requirements artifact should follow naming convention, got: {}",
req_filename
);
let design_result = handle.run_phase(xchecker::types::PhaseId::Design).await?;
assert!(design_result.success, "Design phase should succeed");
if !design_result.artifact_paths.is_empty() {
let design_artifact = &design_result.artifact_paths[0];
let design_filename = design_artifact.file_name().unwrap().to_str().unwrap();
assert!(
design_filename.starts_with("01-") || design_filename.contains("design"),
"Design artifact should follow naming convention, got: {}",
design_filename
);
}
Ok(())
}
#[test]
fn test_externaltool_execution_strategy_rejected() -> Result<()> {
let _temp = xchecker::paths::with_isolated_home();
let cli_args_externaltool = xchecker::config::CliArgs {
execution_strategy: Some("externaltool".to_string()),
..Default::default()
};
let result_externaltool = xchecker::config::Config::discover(&cli_args_externaltool);
assert!(
result_externaltool.is_err(),
"Config with execution_strategy='externaltool' should be rejected"
);
let error_msg = result_externaltool.unwrap_err().to_string();
assert!(
error_msg.contains("externaltool")
|| error_msg.contains("ExternalTool")
|| error_msg.contains("not supported"),
"Error should mention that externaltool is not supported, got: {}",
error_msg
);
let cli_args_external_tool = xchecker::config::CliArgs {
execution_strategy: Some("external_tool".to_string()),
..Default::default()
};
let result_external_tool = xchecker::config::Config::discover(&cli_args_external_tool);
assert!(
result_external_tool.is_err(),
"Config with execution_strategy='external_tool' should be rejected"
);
let error_msg2 = result_external_tool.unwrap_err().to_string();
assert!(
error_msg2.contains("external_tool") || error_msg2.contains("not supported"),
"Error should mention that external_tool is not supported, got: {}",
error_msg2
);
let cli_args_controlled = xchecker::config::CliArgs {
execution_strategy: Some("controlled".to_string()),
..Default::default()
};
let result_controlled = xchecker::config::Config::discover(&cli_args_controlled);
assert!(
result_controlled.is_ok(),
"Config with execution_strategy='controlled' should succeed"
);
let config_controlled = result_controlled.unwrap();
assert_eq!(
config_controlled.llm.execution_strategy,
Some("controlled".to_string()),
"Execution strategy should be 'controlled'"
);
Ok(())
}
#[tokio::test]
async fn test_packet_construction_in_execute_phase_core() -> Result<()> {
let _temp = xchecker::paths::with_isolated_home();
let spec_id = unique_spec_id("packet-construction");
let mut config_map = HashMap::new();
config_map.insert("test_mode".to_string(), "true".to_string());
let config = xchecker::orchestrator::OrchestratorConfig {
dry_run: true,
config: config_map,
full_config: None,
selectors: None,
strict_validation: false,
redactor: Default::default(),
hooks: None,
};
let mut handle =
xchecker::orchestrator::OrchestratorHandle::with_config_and_force(&spec_id, config, false)?;
let result = handle
.run_phase(xchecker::types::PhaseId::Requirements)
.await?;
assert!(result.success, "Phase should succeed");
let receipt_path = result.receipt_path.expect("Should have receipt path");
let receipt_content = std::fs::read_to_string(&receipt_path)?;
let receipt: serde_json::Value = serde_json::from_str(&receipt_content)?;
let packet = &receipt["packet"];
assert!(
packet.is_object(),
"Receipt should have packet object with evidence"
);
let max_bytes = packet["max_bytes"].as_u64().expect("Should have max_bytes");
let max_lines = packet["max_lines"].as_u64().expect("Should have max_lines");
assert_eq!(
max_bytes, 65536,
"Packet max_bytes should match default limit (64KB)"
);
assert_eq!(
max_lines, 1200,
"Packet max_lines should match default limit"
);
let files = packet["files"].as_array().expect("Should have files array");
for (idx, file) in files.iter().enumerate() {
assert!(file.is_object(), "File entry {} should be an object", idx);
assert!(
file["path"].is_string(),
"File {} should have path field",
idx
);
assert!(
file["blake3_pre_redaction"].is_string(),
"File {} should have blake3_pre_redaction field",
idx
);
let hash = file["blake3_pre_redaction"]
.as_str()
.expect("Hash should be string");
assert!(!hash.is_empty(), "File {} hash should not be empty", idx);
if !file["range"].is_null() {
assert!(
file["range"].is_string(),
"File {} range should be string if present",
idx
);
}
assert!(
file["priority"].is_string(),
"File {} should have priority field",
idx
);
let priority = file["priority"]
.as_str()
.expect("Priority should be string");
assert!(
["high", "medium", "low"].contains(&priority),
"File {} priority should be valid enum value, got: {}",
idx,
priority
);
}
let spec_dir = xchecker::paths::spec_root(&spec_id);
let context_dir = spec_dir.join(".context");
if context_dir.exists() {
let packet_preview_path = context_dir.join("requirements-packet");
if packet_preview_path.exists() {
let packet_content = std::fs::read_to_string(&packet_preview_path)?;
assert!(
!packet_content.is_empty(),
"Packet preview content should not be empty"
);
}
} else {
println!("Note: Context directory not found (acceptable for handle-based API)");
}
let design_result = handle.run_phase(xchecker::types::PhaseId::Design).await?;
assert!(design_result.success, "Design phase should succeed");
let design_receipt_path = design_result
.receipt_path
.expect("Should have receipt path");
let design_receipt_content = std::fs::read_to_string(&design_receipt_path)?;
let design_receipt: serde_json::Value = serde_json::from_str(&design_receipt_content)?;
let design_packet = &design_receipt["packet"];
let design_files = design_packet["files"]
.as_array()
.expect("Design packet should have files array");
assert!(
!design_files.is_empty(),
"Design packet should include files from Requirements phase"
);
let mut found_requirements_artifact = false;
for file in design_files.iter() {
let path = file["path"].as_str().expect("File should have path");
if path.contains("requirements") || path.contains("00-") {
found_requirements_artifact = true;
break;
}
}
assert!(
found_requirements_artifact,
"Design packet should include Requirements artifacts in evidence"
);
if context_dir.exists() {
let design_packet_path = context_dir.join("design-packet");
if design_packet_path.exists() {
let design_packet_content = std::fs::read_to_string(&design_packet_path)?;
assert!(
!design_packet_content.is_empty(),
"Design packet content should not be empty"
);
assert!(
design_packet_content.contains("Requirements"),
"Design packet should reference Requirements artifacts"
);
}
}
Ok(())
}
#[tokio::test]
async fn test_prompt_packet_consistency() -> Result<()> {
let _temp = xchecker::paths::with_isolated_home();
let spec_id = unique_spec_id("prompt-packet-consistency");
let config = dry_run_config();
let mut handle =
xchecker::orchestrator::OrchestratorHandle::with_config_and_force(&spec_id, config, false)?;
let result = handle
.run_phase(xchecker::types::PhaseId::Requirements)
.await?;
assert!(result.success, "Phase should succeed");
let receipt_path = result.receipt_path.expect("Should have receipt path");
let receipt_content = std::fs::read_to_string(&receipt_path)?;
let receipt: serde_json::Value = serde_json::from_str(&receipt_content)?;
let packet = &receipt["packet"];
assert!(
packet.is_object(),
"Receipt should have packet object - packet was built"
);
assert!(
packet["max_bytes"].is_number(),
"Packet evidence should have max_bytes"
);
assert!(
packet["max_lines"].is_number(),
"Packet evidence should have max_lines"
);
assert!(
packet["files"].is_array(),
"Packet evidence should have files array"
);
let max_bytes = packet["max_bytes"].as_u64().unwrap();
let max_lines = packet["max_lines"].as_u64().unwrap();
assert!(
max_bytes > 0,
"Packet evidence max_bytes should be positive (packet construction ran)"
);
assert!(
max_lines > 0,
"Packet evidence max_lines should be positive (packet construction ran)"
);
let spec_dir = xchecker::paths::spec_root(&spec_id);
let context_dir = spec_dir.join(".context");
let packet_preview_path = context_dir.join("requirements-packet");
if context_dir.exists() && packet_preview_path.exists() {
let packet_content = std::fs::read_to_string(&packet_preview_path)?;
assert!(
!packet_content.is_empty(),
"Packet preview should not be empty - packet was constructed"
);
assert!(
packet_content.len() > 50,
"Packet should have substantial content, got {} bytes",
packet_content.len()
);
println!("✓ Packet preview file exists and has content");
} else {
println!("Note: Context directory or packet preview not found");
println!(" This may occur with high-level API, but receipt has evidence");
}
let design_result = handle.run_phase(xchecker::types::PhaseId::Design).await?;
assert!(design_result.success, "Design phase should succeed");
let design_receipt_path = design_result
.receipt_path
.expect("Should have receipt path");
let design_receipt_content = std::fs::read_to_string(&design_receipt_path)?;
let design_receipt: serde_json::Value = serde_json::from_str(&design_receipt_content)?;
let design_packet = &design_receipt["packet"];
let design_files = design_packet["files"]
.as_array()
.expect("Design packet should have files array");
assert!(
!design_files.is_empty(),
"Design packet should include files from Requirements phase"
);
let mut found_requirements_artifact = false;
for file in design_files.iter() {
let path = file["path"].as_str().expect("File should have path");
if path.contains("requirements") || path.contains("00-") {
found_requirements_artifact = true;
break;
}
}
assert!(
found_requirements_artifact,
"Design packet should include Requirements artifacts - packet includes prior outputs"
);
let design_packet_path = context_dir.join("design-packet");
if design_packet_path.exists() {
let design_packet_content = std::fs::read_to_string(&design_packet_path)?;
assert!(
!design_packet_content.is_empty(),
"Design packet should not be empty"
);
assert!(
design_packet_content.contains("requirements")
|| design_packet_content.contains("Requirements")
|| design_packet_content.contains("00-"),
"Design packet should reference Requirements artifacts"
);
println!("✓ Design packet includes Requirements artifacts");
}
println!("✓ Packet evidence properly populated in receipts");
println!("✓ Packet building includes prior phase artifacts");
println!("✓ Both prompt and packet building paths executed successfully");
Ok(())
}
#[tokio::test]
async fn test_packet_evidence_round_trip_validation() -> Result<()> {
let _temp = xchecker::paths::with_isolated_home();
let spec_id = unique_spec_id("packet-evidence-roundtrip");
let mut config_map = HashMap::new();
config_map.insert("packet_max_bytes".to_string(), "65536".to_string());
config_map.insert("packet_max_lines".to_string(), "1200".to_string());
let config = xchecker::orchestrator::OrchestratorConfig {
dry_run: true,
config: config_map,
full_config: None,
selectors: None,
strict_validation: false,
redactor: Default::default(),
hooks: None,
};
let mut handle =
xchecker::orchestrator::OrchestratorHandle::with_config_and_force(&spec_id, config, false)?;
let result = handle
.run_phase(xchecker::types::PhaseId::Requirements)
.await?;
assert!(result.success, "Phase should succeed");
let receipt_path = result.receipt_path.expect("Should have receipt path");
let receipt_content = std::fs::read_to_string(&receipt_path)?;
let receipt: serde_json::Value = serde_json::from_str(&receipt_content)?;
let packet = &receipt["packet"];
assert!(packet.is_object(), "Receipt should have packet object");
let max_bytes = packet["max_bytes"].as_u64().expect("Should have max_bytes");
assert_eq!(
max_bytes, 65536,
"Packet max_bytes should match configured value"
);
let max_lines = packet["max_lines"].as_u64().expect("Should have max_lines");
assert_eq!(
max_lines, 1200,
"Packet max_lines should match configured value"
);
let files = packet["files"].as_array().expect("Should have files array");
let evidence_file_count = files.len();
for (idx, file) in files.iter().enumerate() {
assert!(
file["path"].is_string(),
"File {} should have path field",
idx
);
let path = file["path"].as_str().unwrap();
assert!(!path.is_empty(), "File {} path should not be empty", idx);
assert!(
file["blake3_pre_redaction"].is_string(),
"File {} should have blake3_pre_redaction hash",
idx
);
let hash = file["blake3_pre_redaction"].as_str().unwrap();
assert!(!hash.is_empty(), "File {} hash should not be empty", idx);
assert!(
hash.len() >= 32 && hash.chars().all(|c| c.is_ascii_hexdigit()),
"File {} hash should be valid hex string, got: {}",
idx,
hash
);
assert!(
file["priority"].is_string(),
"File {} should have priority field",
idx
);
let priority = file["priority"].as_str().unwrap();
assert!(
["high", "medium", "low", "upstream"].contains(&priority),
"File {} priority should be valid enum value, got: {}",
idx,
priority
);
if !file["range"].is_null() {
assert!(
file["range"].is_string(),
"File {} range should be string if present",
idx
);
}
}
println!("Packet evidence contains {} files", evidence_file_count);
let design_result = handle.run_phase(xchecker::types::PhaseId::Design).await?;
assert!(design_result.success, "Design phase should succeed");
let design_receipt_path = design_result
.receipt_path
.expect("Should have receipt path");
let design_receipt_content = std::fs::read_to_string(&design_receipt_path)?;
let design_receipt: serde_json::Value = serde_json::from_str(&design_receipt_content)?;
let design_packet = &design_receipt["packet"];
let design_files = design_packet["files"]
.as_array()
.expect("Design packet should have files array");
assert!(
!design_files.is_empty(),
"Design packet should include files from Requirements phase"
);
let mut found_requirements_artifact = false;
for file in design_files.iter() {
let path = file["path"].as_str().expect("File should have path");
if path.contains("requirements") || path.contains("00-") {
found_requirements_artifact = true;
assert!(
file["blake3_pre_redaction"].is_string(),
"Requirements artifact should have hash"
);
assert!(
file["priority"].is_string(),
"Requirements artifact should have priority"
);
break;
}
}
assert!(
found_requirements_artifact,
"Design packet evidence should reference Requirements artifacts"
);
let design_max_bytes = design_packet["max_bytes"].as_u64().unwrap();
let design_max_lines = design_packet["max_lines"].as_u64().unwrap();
assert_eq!(
design_max_bytes, max_bytes,
"Packet max_bytes should be consistent across phases"
);
assert_eq!(
design_max_lines, max_lines,
"Packet max_lines should be consistent across phases"
);
Ok(())
}
#[tokio::test]
async fn test_pipeline_execution_strategy_consistency() -> Result<()> {
let _temp = xchecker::paths::with_isolated_home();
let spec_id = unique_spec_id("execution-strategy-consistency");
let config = dry_run_config();
let mut handle =
xchecker::orchestrator::OrchestratorHandle::with_config_and_force(&spec_id, config, false)?;
let phases = vec![
xchecker::types::PhaseId::Requirements,
xchecker::types::PhaseId::Design,
xchecker::types::PhaseId::Tasks,
];
for phase in phases {
let result = handle.run_phase(phase).await?;
assert!(result.success, "Phase {:?} should succeed", phase);
let receipt_path = result.receipt_path.expect("Should have receipt path");
let receipt_content = std::fs::read_to_string(&receipt_path)?;
let receipt: serde_json::Value = serde_json::from_str(&receipt_content)?;
assert!(
receipt["pipeline"].is_object(),
"Receipt for {:?} should have pipeline object",
phase
);
let execution_strategy = receipt["pipeline"]["execution_strategy"]
.as_str()
.expect("Pipeline should have execution_strategy field");
assert_eq!(
execution_strategy, "controlled",
"Phase {:?}: pipeline.execution_strategy should always be 'controlled', got: {}",
phase, execution_strategy
);
}
Ok(())
}
#[tokio::test]
async fn test_receipt_required_fields_populated() -> Result<()> {
let _temp = xchecker::paths::with_isolated_home();
let spec_id = unique_spec_id("receipt-fields-populated");
let config = dry_run_config();
let mut handle =
xchecker::orchestrator::OrchestratorHandle::with_config_and_force(&spec_id, config, false)?;
let result = handle
.run_phase(xchecker::types::PhaseId::Requirements)
.await?;
assert!(result.success, "Phase should succeed");
let receipt_path = result.receipt_path.expect("Should have receipt path");
let receipt_content = std::fs::read_to_string(&receipt_path)?;
let receipt: serde_json::Value = serde_json::from_str(&receipt_content)?;
assert!(
receipt["schema_version"].is_string(),
"Receipt should have schema_version"
);
let schema_version = receipt["schema_version"].as_str().unwrap();
assert!(
!schema_version.is_empty(),
"schema_version should not be empty"
);
assert!(
receipt["emitted_at"].is_string(),
"Receipt should have emitted_at timestamp"
);
let emitted_at = receipt["emitted_at"].as_str().unwrap();
assert!(!emitted_at.is_empty(), "emitted_at should not be empty");
assert!(
emitted_at.contains('T') && (emitted_at.contains('Z') || emitted_at.contains('+')),
"emitted_at should be valid RFC3339 timestamp, got: {}",
emitted_at
);
assert!(
receipt["spec_id"].is_string(),
"Receipt should have spec_id"
);
let spec_id_val = receipt["spec_id"].as_str().unwrap();
assert!(!spec_id_val.is_empty(), "spec_id should not be empty");
assert_eq!(
spec_id_val, spec_id,
"Receipt spec_id should match expected value"
);
assert!(receipt["phase"].is_string(), "Receipt should have phase");
let phase = receipt["phase"].as_str().unwrap();
assert!(!phase.is_empty(), "phase should not be empty");
assert_eq!(phase, "requirements", "Phase should be 'requirements'");
assert!(
receipt["xchecker_version"].is_string(),
"Receipt should have xchecker_version"
);
let xchecker_version = receipt["xchecker_version"].as_str().unwrap();
assert!(
!xchecker_version.is_empty(),
"xchecker_version should not be empty"
);
assert!(
receipt["exit_code"].is_number(),
"Receipt should have exit_code"
);
let exit_code = receipt["exit_code"].as_i64().unwrap();
assert_eq!(exit_code, 0, "Successful phase should have exit_code 0");
assert!(
receipt["packet"].is_object(),
"Receipt should have packet object"
);
assert!(
receipt["packet"]["max_bytes"].is_number(),
"Packet should have max_bytes"
);
assert!(
receipt["packet"]["max_lines"].is_number(),
"Packet should have max_lines"
);
assert!(
receipt["packet"]["files"].is_array(),
"Packet should have files array"
);
assert!(
receipt["outputs"].is_array(),
"Receipt should have outputs array"
);
assert!(
receipt["flags"].is_object(),
"Receipt should have flags object"
);
assert!(
receipt["pipeline"].is_object(),
"Receipt should have pipeline object"
);
assert!(
receipt["pipeline"]["execution_strategy"].is_string(),
"Pipeline should have execution_strategy"
);
assert!(
receipt["llm"].is_object() || receipt["llm"].is_null(),
"Receipt should have llm field (object or null)"
);
assert!(
receipt["runner"].is_string(),
"Receipt should have runner field"
);
let runner = receipt["runner"].as_str().unwrap();
assert!(
["native", "wsl", "simulated"].contains(&runner),
"Runner should be 'native', 'wsl', or 'simulated' (dry-run), got: {}",
runner
);
assert!(
receipt["canonicalization_version"].is_string(),
"Receipt should have canonicalization_version"
);
assert!(
receipt["canonicalization_backend"].is_string(),
"Receipt should have canonicalization_backend"
);
assert!(
receipt["claude_cli_version"].is_string(),
"Receipt should have claude_cli_version"
);
assert!(
receipt["model_full_name"].is_string(),
"Receipt should have model_full_name"
);
Ok(())
}
#[tokio::test]
async fn test_packet_file_count_matches_actual_files() -> Result<()> {
let _temp = xchecker::paths::with_isolated_home();
let spec_id = unique_spec_id("packet-file-count");
let config = dry_run_config();
let mut handle =
xchecker::orchestrator::OrchestratorHandle::with_config_and_force(&spec_id, config, false)?;
let req_result = handle
.run_phase(xchecker::types::PhaseId::Requirements)
.await?;
assert!(req_result.success, "Requirements phase should succeed");
let req_artifact_count = req_result.artifact_paths.len();
assert!(
req_artifact_count > 0,
"Requirements should produce at least one artifact"
);
let design_result = handle.run_phase(xchecker::types::PhaseId::Design).await?;
assert!(design_result.success, "Design phase should succeed");
let receipt_path = design_result
.receipt_path
.expect("Should have receipt path");
let receipt_content = std::fs::read_to_string(&receipt_path)?;
let receipt: serde_json::Value = serde_json::from_str(&receipt_content)?;
let packet = &receipt["packet"];
let files = packet["files"].as_array().expect("Should have files array");
assert!(
!files.is_empty(),
"Design packet should contain files from Requirements phase"
);
let mut req_artifact_count_in_packet = 0;
for file in files.iter() {
let path = file["path"].as_str().expect("File should have path");
if path.contains("requirements") || path.contains("00-") {
req_artifact_count_in_packet += 1;
}
}
assert!(
req_artifact_count_in_packet > 0,
"Design packet should include at least one Requirements artifact, \
found {} Requirements artifacts but {} in packet",
req_artifact_count,
req_artifact_count_in_packet
);
for (idx, file) in files.iter().enumerate() {
let path = file["path"].as_str().expect("File should have path");
let hash = file["blake3_pre_redaction"]
.as_str()
.expect("File should have hash");
let priority = file["priority"]
.as_str()
.expect("File should have priority");
assert!(!path.is_empty(), "File {} path should not be empty", idx);
assert!(!hash.is_empty(), "File {} hash should not be empty", idx);
assert!(
!priority.is_empty(),
"File {} priority should not be empty",
idx
);
}
Ok(())
}
#[tokio::test]
async fn test_receipt_consistency_across_executions() -> Result<()> {
let _temp = xchecker::paths::with_isolated_home();
let spec_id = unique_spec_id("receipt-consistency");
let config = dry_run_config();
let mut handle =
xchecker::orchestrator::OrchestratorHandle::with_config_and_force(&spec_id, config, false)?;
let result1 = handle
.run_phase(xchecker::types::PhaseId::Requirements)
.await?;
let result2 = handle
.run_phase(xchecker::types::PhaseId::Requirements)
.await?;
assert!(
result1.success && result2.success,
"Both runs should succeed"
);
let receipt1_content =
std::fs::read_to_string(result1.receipt_path.expect("Should have receipt path"))?;
let receipt1: serde_json::Value = serde_json::from_str(&receipt1_content)?;
let receipt2_content =
std::fs::read_to_string(result2.receipt_path.expect("Should have receipt path"))?;
let receipt2: serde_json::Value = serde_json::from_str(&receipt2_content)?;
assert_eq!(
receipt1["schema_version"], receipt2["schema_version"],
"Schema version should be consistent"
);
assert_eq!(
receipt1["exit_code"], receipt2["exit_code"],
"Exit code should be consistent"
);
assert_eq!(
receipt1["phase"], receipt2["phase"],
"Phase should be consistent"
);
assert_eq!(
receipt1["packet"]["max_bytes"], receipt2["packet"]["max_bytes"],
"Packet max_bytes should be consistent"
);
assert_eq!(
receipt1["packet"]["max_lines"], receipt2["packet"]["max_lines"],
"Packet max_lines should be consistent"
);
assert_eq!(
receipt1["pipeline"]["execution_strategy"], receipt2["pipeline"]["execution_strategy"],
"Execution strategy should be consistent"
);
assert_eq!(
receipt1["pipeline"]["execution_strategy"].as_str(),
Some("controlled"),
"First run should have controlled strategy"
);
assert_eq!(
receipt2["pipeline"]["execution_strategy"].as_str(),
Some("controlled"),
"Second run should have controlled strategy"
);
Ok(())
}