use super::orchestrator::ReleaseOrchestrator;
use crate::stack::checker::StackChecker;
use crate::stack::graph::{DependencyEdge, DependencyGraph};
use crate::stack::releaser_types::{
format_plan_text, BumpType, ReleaseConfig, ReleaseResult, ReleasedCrate,
};
use crate::stack::types::*;
use std::collections::HashMap;
use std::path::Path;
use std::path::PathBuf;
fn create_test_graph() -> DependencyGraph {
let mut graph = DependencyGraph::new();
graph.add_crate(CrateInfo::new(
"trueno",
semver::Version::new(1, 2, 0),
PathBuf::from("trueno/Cargo.toml"),
));
graph.add_crate(CrateInfo::new(
"aprender",
semver::Version::new(0, 8, 1),
PathBuf::from("aprender/Cargo.toml"),
));
graph.add_crate(CrateInfo::new(
"entrenar",
semver::Version::new(0, 2, 2),
PathBuf::from("entrenar/Cargo.toml"),
));
graph.add_dependency(
"aprender",
"trueno",
DependencyEdge {
version_req: "^1.0".to_string(),
is_path: false,
kind: DependencyKind::Normal,
},
);
graph.add_dependency(
"entrenar",
"aprender",
DependencyEdge {
version_req: "^0.8".to_string(),
is_path: false,
kind: DependencyKind::Normal,
},
);
graph
}
#[test]
fn test_bump_type_patch() {
let version = semver::Version::new(1, 2, 3);
let bumped = BumpType::Patch.apply(&version);
assert_eq!(bumped, semver::Version::new(1, 2, 4));
}
#[test]
fn test_bump_type_minor() {
let version = semver::Version::new(1, 2, 3);
let bumped = BumpType::Minor.apply(&version);
assert_eq!(bumped, semver::Version::new(1, 3, 0));
}
#[test]
fn test_bump_type_major() {
let version = semver::Version::new(1, 2, 3);
let bumped = BumpType::Major.apply(&version);
assert_eq!(bumped, semver::Version::new(2, 0, 0));
}
#[test]
fn test_release_config_default() {
let config = ReleaseConfig::default();
assert!(!config.no_verify);
assert!(!config.dry_run);
assert!(!config.publish);
assert_eq!(config.min_coverage, 90.0);
}
#[test]
fn test_orchestrator_creation() {
let graph = create_test_graph();
let checker = StackChecker::with_graph(graph);
let config = ReleaseConfig::default();
let orchestrator = ReleaseOrchestrator::new(checker, config);
assert!(!orchestrator.config.dry_run);
}
#[test]
fn test_plan_release() {
let graph = create_test_graph();
let checker = StackChecker::with_graph(graph);
let config = ReleaseConfig { dry_run: true, ..Default::default() };
let mut orchestrator = ReleaseOrchestrator::new(checker, config);
let plan = orchestrator.plan_release("entrenar").unwrap();
assert!(!plan.releases.is_empty());
assert!(plan.dry_run);
assert_eq!(plan.releases.last().unwrap().crate_name, "entrenar");
}
#[test]
fn test_plan_all_releases() {
let graph = create_test_graph();
let checker = StackChecker::with_graph(graph);
let config = ReleaseConfig { dry_run: true, ..Default::default() };
let mut orchestrator = ReleaseOrchestrator::new(checker, config);
let plan = orchestrator.plan_all_releases().unwrap();
assert_eq!(plan.releases.len(), 3);
}
#[test]
fn test_preflight_no_verify() {
let graph = create_test_graph();
let checker = StackChecker::with_graph(graph);
let config = ReleaseConfig { no_verify: true, ..Default::default() };
let mut orchestrator = ReleaseOrchestrator::new(checker, config);
let result = orchestrator.run_preflight("trueno", Path::new(".")).unwrap();
assert!(result.passed);
assert_eq!(result.checks.len(), 1);
assert!(result.checks[0].message.contains("Skipped"));
}
#[test]
fn test_preflight_result_aggregation() {
let mut result = PreflightResult::new("test");
result.add_check(PreflightCheck::pass("check1", "ok"));
result.add_check(PreflightCheck::pass("check2", "ok"));
assert!(result.passed);
result.add_check(PreflightCheck::fail("check3", "failed"));
assert!(!result.passed);
}
#[test]
fn test_format_plan_text() {
let plan = ReleasePlan {
releases: vec![
PlannedRelease {
crate_name: "trueno".to_string(),
current_version: semver::Version::new(1, 2, 0),
new_version: semver::Version::new(1, 2, 1),
dependents: vec![],
ready: true,
},
PlannedRelease {
crate_name: "aprender".to_string(),
current_version: semver::Version::new(0, 8, 1),
new_version: semver::Version::new(0, 8, 2),
dependents: vec![],
ready: true,
},
],
dry_run: true,
preflight_results: HashMap::new(),
};
let text = format_plan_text(&plan);
assert!(text.contains("DRY RUN"));
assert!(text.contains("trueno"));
assert!(text.contains("aprender"));
assert!(text.contains("1.2.0 → 1.2.1"));
}
#[test]
fn test_released_crate() {
let released = ReleasedCrate {
name: "trueno".to_string(),
version: semver::Version::new(1, 2, 1),
published: true,
};
assert_eq!(released.name, "trueno");
assert!(released.published);
}
#[test]
fn test_release_result() {
let result = ReleaseResult {
success: true,
released_crates: vec![ReleasedCrate {
name: "trueno".to_string(),
version: semver::Version::new(1, 2, 1),
published: true,
}],
message: "Success".to_string(),
};
assert!(result.success);
assert_eq!(result.released_crates.len(), 1);
}
#[test]
#[allow(non_snake_case)]
fn test_RELEASE_001_bump_patch_from_zero() {
let version = semver::Version::new(0, 0, 0);
let bumped = BumpType::Patch.apply(&version);
assert_eq!(bumped, semver::Version::new(0, 0, 1));
}
#[test]
#[allow(non_snake_case)]
fn test_RELEASE_001_bump_minor_resets_patch() {
let version = semver::Version::new(1, 2, 99);
let bumped = BumpType::Minor.apply(&version);
assert_eq!(bumped, semver::Version::new(1, 3, 0));
}
#[test]
#[allow(non_snake_case)]
fn test_RELEASE_001_bump_major_resets_minor_patch() {
let version = semver::Version::new(5, 99, 99);
let bumped = BumpType::Major.apply(&version);
assert_eq!(bumped, semver::Version::new(6, 0, 0));
}
#[test]
#[allow(non_snake_case)]
fn test_RELEASE_001_bump_type_equality() {
assert_eq!(BumpType::Patch, BumpType::Patch);
assert_eq!(BumpType::Minor, BumpType::Minor);
assert_eq!(BumpType::Major, BumpType::Major);
assert_ne!(BumpType::Patch, BumpType::Minor);
}
#[test]
#[allow(non_snake_case)]
fn test_RELEASE_001_bump_type_clone() {
let bump = BumpType::Minor;
let cloned = bump;
assert_eq!(bump, cloned);
}
#[test]
#[allow(non_snake_case)]
fn test_RELEASE_001_bump_type_debug() {
assert!(format!("{:?}", BumpType::Patch).contains("Patch"));
assert!(format!("{:?}", BumpType::Minor).contains("Minor"));
assert!(format!("{:?}", BumpType::Major).contains("Major"));
}
#[test]
#[allow(non_snake_case)]
fn test_RELEASE_002_config_custom_values() {
let config = ReleaseConfig {
bump_type: Some(BumpType::Minor),
no_verify: true,
dry_run: true,
publish: true,
min_coverage: 95.0,
lint_command: "cargo clippy".to_string(),
coverage_command: "cargo tarpaulin".to_string(),
comply_command: "pmat comply --strict".to_string(),
fail_on_comply_violations: true,
..Default::default()
};
assert!(config.no_verify);
assert!(config.dry_run);
assert!(config.publish);
assert_eq!(config.min_coverage, 95.0);
assert_eq!(config.lint_command, "cargo clippy");
assert_eq!(config.bump_type, Some(BumpType::Minor));
assert_eq!(config.comply_command, "pmat comply --strict");
assert!(config.fail_on_comply_violations);
}
#[test]
#[allow(non_snake_case)]
fn test_RELEASE_002_config_clone() {
let config = ReleaseConfig::default();
let cloned = config.clone();
assert_eq!(cloned.min_coverage, config.min_coverage);
assert_eq!(cloned.dry_run, config.dry_run);
}
#[test]
#[allow(non_snake_case)]
fn test_RELEASE_002_config_debug() {
let config = ReleaseConfig::default();
let debug = format!("{:?}", config);
assert!(debug.contains("ReleaseConfig"));
assert!(debug.contains("min_coverage"));
}
#[test]
#[allow(non_snake_case)]
fn test_RELEASE_003_format_plan_text_live() {
let plan = ReleasePlan {
releases: vec![PlannedRelease {
crate_name: "test-crate".to_string(),
current_version: semver::Version::new(1, 0, 0),
new_version: semver::Version::new(1, 0, 1),
dependents: vec![],
ready: true,
}],
dry_run: false,
preflight_results: HashMap::new(),
};
let text = format_plan_text(&plan);
assert!(!text.contains("DRY RUN"));
assert!(text.contains("Release Plan"));
assert!(text.contains("test-crate"));
}
#[test]
#[allow(non_snake_case)]
fn test_RELEASE_003_format_plan_text_with_preflight() {
let mut preflight_results = HashMap::new();
preflight_results.insert(
"trueno".to_string(),
PreflightResult {
crate_name: "trueno".to_string(),
checks: vec![PreflightCheck::pass("git", "clean")],
passed: true,
},
);
preflight_results.insert(
"aprender".to_string(),
PreflightResult {
crate_name: "aprender".to_string(),
checks: vec![PreflightCheck::fail("lint", "errors")],
passed: false,
},
);
let plan = ReleasePlan {
releases: vec![
PlannedRelease {
crate_name: "trueno".to_string(),
current_version: semver::Version::new(1, 0, 0),
new_version: semver::Version::new(1, 0, 1),
dependents: vec![],
ready: true,
},
PlannedRelease {
crate_name: "aprender".to_string(),
current_version: semver::Version::new(0, 8, 0),
new_version: semver::Version::new(0, 8, 1),
dependents: vec![],
ready: false,
},
],
dry_run: true,
preflight_results,
};
let text = format_plan_text(&plan);
assert!(text.contains("trueno"));
assert!(text.contains("aprender"));
}
#[test]
#[allow(non_snake_case)]
fn test_RELEASE_003_format_plan_text_empty() {
let plan = ReleasePlan { releases: vec![], dry_run: false, preflight_results: HashMap::new() };
let text = format_plan_text(&plan);
assert!(text.contains("Release Plan"));
assert!(text.contains("Release order"));
}
#[test]
#[allow(non_snake_case)]
fn test_RELEASE_004_planned_release_with_dependents() {
let release = PlannedRelease {
crate_name: "trueno".to_string(),
current_version: semver::Version::new(1, 0, 0),
new_version: semver::Version::new(1, 1, 0),
dependents: vec!["aprender".to_string(), "trueno-db".to_string()],
ready: true,
};
assert_eq!(release.dependents.len(), 2);
assert!(release.dependents.contains(&"aprender".to_string()));
assert!(release.ready);
}
#[test]
#[allow(non_snake_case)]
fn test_RELEASE_004_release_plan_dry_run() {
let plan_dry =
ReleasePlan { releases: vec![], dry_run: true, preflight_results: HashMap::new() };
let plan_live =
ReleasePlan { releases: vec![], dry_run: false, preflight_results: HashMap::new() };
assert!(plan_dry.dry_run);
assert!(!plan_live.dry_run);
}
#[test]
#[allow(non_snake_case)]
fn test_RELEASE_005_release_result_failure() {
let result = ReleaseResult {
success: false,
released_crates: vec![],
message: "Pre-flight checks failed".to_string(),
};
assert!(!result.success);
assert!(result.released_crates.is_empty());
assert!(result.message.contains("failed"));
}
#[test]
#[allow(non_snake_case)]
fn test_RELEASE_005_released_crate_unpublished() {
let released = ReleasedCrate {
name: "local-only".to_string(),
version: semver::Version::new(0, 1, 0),
published: false,
};
assert!(!released.published);
assert_eq!(released.name, "local-only");
}
#[test]
#[allow(non_snake_case)]
fn test_RELEASE_005_release_result_debug() {
let result =
ReleaseResult { success: true, released_crates: vec![], message: "OK".to_string() };
let debug = format!("{:?}", result);
assert!(debug.contains("ReleaseResult"));
assert!(debug.contains("success"));
}
#[test]
#[allow(non_snake_case)]
fn test_RELEASE_005_released_crate_debug() {
let released = ReleasedCrate {
name: "test".to_string(),
version: semver::Version::new(1, 0, 0),
published: true,
};
let debug = format!("{:?}", released);
assert!(debug.contains("ReleasedCrate"));
assert!(debug.contains("test"));
}
#[test]
#[allow(non_snake_case)]
fn test_RELEASE_006_plan_with_bump_type() {
let graph = create_test_graph();
let checker = StackChecker::with_graph(graph);
let config =
ReleaseConfig { bump_type: Some(BumpType::Minor), dry_run: true, ..Default::default() };
let mut orchestrator = ReleaseOrchestrator::new(checker, config);
let plan = orchestrator.plan_release("trueno").unwrap();
for release in &plan.releases {
assert_eq!(release.new_version.patch, 0);
}
}
#[test]
#[allow(non_snake_case)]
fn test_RELEASE_006_plan_leaf_crate() {
let graph = create_test_graph();
let checker = StackChecker::with_graph(graph);
let config = ReleaseConfig::default();
let mut orchestrator = ReleaseOrchestrator::new(checker, config);
let result = orchestrator.plan_release("trueno");
assert!(result.is_ok());
let plan = result.unwrap();
assert_eq!(plan.releases.len(), 1);
assert_eq!(plan.releases[0].crate_name, "trueno");
}
#[test]
#[allow(non_snake_case)]
fn test_RELEASE_007_preflight_no_verify() {
let graph = create_test_graph();
let checker = StackChecker::with_graph(graph);
let config = ReleaseConfig { no_verify: true, ..Default::default() };
let mut orchestrator = ReleaseOrchestrator::new(checker, config);
let result = orchestrator.run_preflight("trueno", std::path::Path::new("."));
assert!(result.is_ok());
let preflight = result.unwrap();
assert!(preflight.passed);
assert_eq!(preflight.checks.len(), 1);
assert!(preflight.checks[0].message.contains("Skipped"));
}
#[test]
#[allow(non_snake_case)]
fn test_RELEASE_007_check_no_path_deps() {
let graph = create_test_graph();
let checker = StackChecker::with_graph(graph);
let config = ReleaseConfig::default();
let orchestrator = ReleaseOrchestrator::new(checker, config);
let check = orchestrator.check_no_path_deps("any-crate");
assert!(check.passed);
assert!(check.message.contains("No path dependencies"));
}
#[test]
#[allow(non_snake_case)]
fn test_RELEASE_007_check_version_bumped() {
let graph = create_test_graph();
let checker = StackChecker::with_graph(graph);
let config = ReleaseConfig::default();
let orchestrator = ReleaseOrchestrator::new(checker, config);
let check = orchestrator.check_version_bumped("any-crate");
assert!(check.passed);
assert!(check.message.contains("ahead"));
}
#[test]
#[allow(non_snake_case)]
fn test_RELEASE_008_execute_dry_run() {
let graph = create_test_graph();
let checker = StackChecker::with_graph(graph);
let config = ReleaseConfig { dry_run: true, ..Default::default() };
let orchestrator = ReleaseOrchestrator::new(checker, config);
let plan = ReleasePlan {
releases: vec![PlannedRelease {
crate_name: "test".to_string(),
current_version: semver::Version::new(1, 0, 0),
new_version: semver::Version::new(1, 0, 1),
dependents: vec![],
ready: true,
}],
dry_run: true,
preflight_results: HashMap::new(),
};
let result = orchestrator.execute(&plan);
assert!(result.is_ok());
let release_result = result.unwrap();
assert!(release_result.success);
assert!(release_result.released_crates.is_empty());
assert!(release_result.message.contains("Dry run"));
}
#[test]
#[allow(non_snake_case)]
fn test_RELEASE_008_execute_preflight_failed() {
let graph = create_test_graph();
let checker = StackChecker::with_graph(graph);
let config = ReleaseConfig::default();
let orchestrator = ReleaseOrchestrator::new(checker, config);
let mut preflight_results = HashMap::new();
preflight_results.insert(
"test".to_string(),
PreflightResult {
crate_name: "test".to_string(),
checks: vec![PreflightCheck::fail("lint", "errors")],
passed: false,
},
);
let plan = ReleasePlan {
releases: vec![PlannedRelease {
crate_name: "test".to_string(),
current_version: semver::Version::new(1, 0, 0),
new_version: semver::Version::new(1, 0, 1),
dependents: vec![],
ready: false,
}],
dry_run: false,
preflight_results,
};
let result = orchestrator.execute(&plan);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Pre-flight checks failed"));
}
#[test]
#[allow(non_snake_case)]
fn test_RELEASE_008_execute_success_no_publish() {
let graph = create_test_graph();
let checker = StackChecker::with_graph(graph);
let config = ReleaseConfig { publish: false, ..Default::default() };
let orchestrator = ReleaseOrchestrator::new(checker, config);
let mut preflight_results = HashMap::new();
preflight_results.insert(
"test".to_string(),
PreflightResult {
crate_name: "test".to_string(),
checks: vec![PreflightCheck::pass("all", "ok")],
passed: true,
},
);
let plan = ReleasePlan {
releases: vec![PlannedRelease {
crate_name: "test".to_string(),
current_version: semver::Version::new(1, 0, 0),
new_version: semver::Version::new(1, 0, 1),
dependents: vec![],
ready: true,
}],
dry_run: false,
preflight_results,
};
let result = orchestrator.execute(&plan);
assert!(result.is_ok());
let release_result = result.unwrap();
assert!(release_result.success);
assert_eq!(release_result.released_crates.len(), 1);
assert!(!release_result.released_crates[0].published);
}
#[test]
#[allow(non_snake_case)]
fn test_RELEASE_008_execute_multiple_crates() {
let graph = create_test_graph();
let checker = StackChecker::with_graph(graph);
let config = ReleaseConfig { publish: true, ..Default::default() };
let orchestrator = ReleaseOrchestrator::new(checker, config);
let mut preflight_results = HashMap::new();
for name in &["trueno", "aprender"] {
preflight_results.insert(
name.to_string(),
PreflightResult {
crate_name: name.to_string(),
checks: vec![PreflightCheck::pass("all", "ok")],
passed: true,
},
);
}
let plan = ReleasePlan {
releases: vec![
PlannedRelease {
crate_name: "trueno".to_string(),
current_version: semver::Version::new(1, 0, 0),
new_version: semver::Version::new(1, 0, 1),
dependents: vec!["aprender".to_string()],
ready: true,
},
PlannedRelease {
crate_name: "aprender".to_string(),
current_version: semver::Version::new(0, 8, 0),
new_version: semver::Version::new(0, 8, 1),
dependents: vec![],
ready: true,
},
],
dry_run: false,
preflight_results,
};
let result = orchestrator.execute(&plan);
assert!(result.is_ok());
let release_result = result.unwrap();
assert!(release_result.success);
assert_eq!(release_result.released_crates.len(), 2);
assert!(!release_result.released_crates[0].published);
assert!(!release_result.released_crates[1].published);
}
#[test]
#[allow(non_snake_case)]
fn test_RELEASE_009_plan_all_releases() {
let graph = create_test_graph();
let checker = StackChecker::with_graph(graph);
let config = ReleaseConfig::default();
let mut orchestrator = ReleaseOrchestrator::new(checker, config);
let result = orchestrator.plan_all_releases();
assert!(result.is_ok());
let plan = result.unwrap();
assert!(!plan.releases.is_empty());
}
#[test]
#[allow(non_snake_case)]
fn test_RELEASE_010_config_publish() {
let config = ReleaseConfig { publish: true, dry_run: false, ..Default::default() };
assert!(config.publish);
assert!(!config.dry_run);
}
#[test]
#[allow(non_snake_case)]
fn test_RELEASE_010_config_lint_command() {
let config = ReleaseConfig {
lint_command: "cargo clippy -- -D warnings".to_string(),
..Default::default()
};
assert_eq!(config.lint_command, "cargo clippy -- -D warnings");
}
#[test]
#[allow(non_snake_case)]
fn test_RELEASE_010_config_coverage_command() {
let config = ReleaseConfig {
coverage_command: "cargo tarpaulin".to_string(),
min_coverage: 95.0,
..Default::default()
};
assert_eq!(config.coverage_command, "cargo tarpaulin");
assert_eq!(config.min_coverage, 95.0);
}
#[test]
#[allow(non_snake_case)]
fn test_RELEASE_DOCS_config_defaults() {
let config = ReleaseConfig::default();
assert_eq!(config.book_command, "mdbook build book");
assert!(config.fail_on_book);
assert_eq!(config.examples_command, "cargo run --example");
assert!(config.fail_on_examples);
}
#[test]
#[allow(non_snake_case)]
fn test_RELEASE_DOCS_config_custom_book() {
let config = ReleaseConfig {
book_command: "mdbook build docs/book".to_string(),
fail_on_book: false,
..Default::default()
};
assert_eq!(config.book_command, "mdbook build docs/book");
assert!(!config.fail_on_book);
}
#[test]
#[allow(non_snake_case)]
fn test_RELEASE_DOCS_config_custom_examples() {
let config = ReleaseConfig {
examples_command: "cargo run --release --example".to_string(),
fail_on_examples: false,
..Default::default()
};
assert_eq!(config.examples_command, "cargo run --release --example");
assert!(!config.fail_on_examples);
}
#[test]
#[allow(non_snake_case)]
fn test_RELEASE_DOCS_config_disabled() {
let config = ReleaseConfig {
book_command: String::new(),
examples_command: String::new(),
..Default::default()
};
assert!(config.book_command.is_empty());
assert!(config.examples_command.is_empty());
}
#[test]
#[allow(non_snake_case)]
fn test_RELEASE_DOCS_check_book_no_command() {
let graph = create_test_graph();
let checker = StackChecker::with_graph(graph);
let config = ReleaseConfig { book_command: String::new(), ..Default::default() };
let orchestrator = ReleaseOrchestrator::new(checker, config);
let check = orchestrator.check_book_build(Path::new("."));
assert!(check.passed);
assert!(check.message.contains("skipped"));
}
#[test]
#[allow(non_snake_case)]
fn test_RELEASE_DOCS_check_book_no_directory() {
let graph = create_test_graph();
let checker = StackChecker::with_graph(graph);
let config = ReleaseConfig::default();
let orchestrator = ReleaseOrchestrator::new(checker, config);
let check = orchestrator.check_book_build(Path::new("/tmp"));
assert!(check.passed);
assert!(check.message.contains("No book directory"));
}
#[test]
#[allow(non_snake_case)]
fn test_RELEASE_DOCS_check_examples_no_command() {
let graph = create_test_graph();
let checker = StackChecker::with_graph(graph);
let config = ReleaseConfig { examples_command: String::new(), ..Default::default() };
let orchestrator = ReleaseOrchestrator::new(checker, config);
let check = orchestrator.check_examples_run(Path::new("."));
assert!(check.passed);
assert!(check.message.contains("skipped"));
}
#[test]
#[allow(non_snake_case)]
fn test_RELEASE_DOCS_check_examples_no_directory() {
let graph = create_test_graph();
let checker = StackChecker::with_graph(graph);
let config = ReleaseConfig::default();
let orchestrator = ReleaseOrchestrator::new(checker, config);
let check = orchestrator.check_examples_run(Path::new("/tmp"));
assert!(check.passed);
assert!(check.message.contains("No examples directory"));
}
#[test]
#[allow(non_snake_case)]
fn test_RELEASE_DOCS_discover_examples_empty() {
let graph = create_test_graph();
let checker = StackChecker::with_graph(graph);
let config = ReleaseConfig::default();
let orchestrator = ReleaseOrchestrator::new(checker, config);
let examples = orchestrator.discover_examples(Path::new("/tmp"));
assert!(examples.is_empty());
}
#[test]
#[allow(non_snake_case)]
fn test_RELEASE_011_update_cargo_toml_basic() {
let temp_dir = std::env::temp_dir().join("batuta_release_011_update_basic");
let _ = std::fs::remove_dir_all(&temp_dir);
std::fs::create_dir_all(&temp_dir).unwrap();
let manifest = temp_dir.join("Cargo.toml");
std::fs::write(
&manifest,
"[package]\nname = \"test-crate\"\nversion = \"1.0.0\"\nedition = \"2021\"\n",
)
.unwrap();
let graph = create_test_graph();
let checker = StackChecker::with_graph(graph);
let config = ReleaseConfig::default();
let orchestrator = ReleaseOrchestrator::new(checker, config);
let result = orchestrator.update_cargo_toml(&manifest, &semver::Version::new(1, 0, 1));
assert!(result.is_ok());
let content = std::fs::read_to_string(&manifest).unwrap();
assert!(
content.contains("\"1.0.1\""),
"Manifest should contain updated version, got: {}",
content
);
assert!(!content.contains("\"1.0.0\""), "Old version should be replaced");
let _ = std::fs::remove_dir_all(&temp_dir);
}
#[test]
#[allow(non_snake_case)]
fn test_RELEASE_011_update_cargo_toml_preserves_deps() {
let temp_dir = std::env::temp_dir().join("batuta_release_011_preserves_deps");
let _ = std::fs::remove_dir_all(&temp_dir);
std::fs::create_dir_all(&temp_dir).unwrap();
let manifest = temp_dir.join("Cargo.toml");
std::fs::write(
&manifest,
"[package]\nname = \"deps-test\"\nversion = \"0.5.0\"\n\n[dependencies]\ntrueno = \"0.14.0\"\naprender = { version = \"0.24.0\", features = [\"gpu\"] }\n",
)
.unwrap();
let graph = create_test_graph();
let checker = StackChecker::with_graph(graph);
let config = ReleaseConfig::default();
let orchestrator = ReleaseOrchestrator::new(checker, config);
let result = orchestrator.update_cargo_toml(&manifest, &semver::Version::new(0, 6, 0));
assert!(result.is_ok());
let content = std::fs::read_to_string(&manifest).unwrap();
assert!(content.contains("\"0.6.0\""), "Package version should be updated");
assert!(
content.contains("trueno = \"0.14.0\""),
"Dependency trueno version should NOT be changed"
);
assert!(
content.contains("version = \"0.24.0\""),
"Dependency aprender version should NOT be changed"
);
let _ = std::fs::remove_dir_all(&temp_dir);
}
#[test]
#[allow(non_snake_case)]
fn test_RELEASE_011_update_cargo_toml_no_package_version() {
let temp_dir = std::env::temp_dir().join("batuta_release_011_no_version");
let _ = std::fs::remove_dir_all(&temp_dir);
std::fs::create_dir_all(&temp_dir).unwrap();
let manifest = temp_dir.join("Cargo.toml");
std::fs::write(&manifest, "[workspace]\nmembers = [\"crate-a\"]\n").unwrap();
let graph = create_test_graph();
let checker = StackChecker::with_graph(graph);
let config = ReleaseConfig::default();
let orchestrator = ReleaseOrchestrator::new(checker, config);
let result = orchestrator.update_cargo_toml(&manifest, &semver::Version::new(1, 0, 0));
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("Could not find version field"),
"Error should mention missing version, got: {}",
err_msg
);
let _ = std::fs::remove_dir_all(&temp_dir);
}
#[test]
#[allow(non_snake_case)]
fn test_RELEASE_011_update_cargo_toml_missing_file() {
let graph = create_test_graph();
let checker = StackChecker::with_graph(graph);
let config = ReleaseConfig::default();
let orchestrator = ReleaseOrchestrator::new(checker, config);
let result = orchestrator.update_cargo_toml(
Path::new("/tmp/nonexistent_dir_xyz/Cargo.toml"),
&semver::Version::new(1, 0, 0),
);
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("Failed to read"),
"Error should mention failed read, got: {}",
err_msg
);
}
#[test]
#[allow(non_snake_case)]
fn test_RELEASE_011_update_cargo_toml_multi_section() {
let temp_dir = std::env::temp_dir().join("batuta_release_011_multi_section");
let _ = std::fs::remove_dir_all(&temp_dir);
std::fs::create_dir_all(&temp_dir).unwrap();
let manifest = temp_dir.join("Cargo.toml");
std::fs::write(
&manifest,
"[package]\nname = \"multi\"\nversion = \"2.3.4\"\n\n[dependencies]\nversion_check = \"0.9\"\n\n[dev-dependencies]\nversion-sync = \"0.9\"\n\n[build-dependencies]\nversion_check = \"0.9\"\n",
)
.unwrap();
let graph = create_test_graph();
let checker = StackChecker::with_graph(graph);
let config = ReleaseConfig::default();
let orchestrator = ReleaseOrchestrator::new(checker, config);
let result = orchestrator.update_cargo_toml(&manifest, &semver::Version::new(3, 0, 0));
assert!(result.is_ok());
let content = std::fs::read_to_string(&manifest).unwrap();
assert!(content.contains("\"3.0.0\""));
assert_eq!(
content.matches("version_check = \"0.9\"").count(),
2,
"Both version_check deps should remain"
);
assert!(content.contains("version-sync = \"0.9\""));
let _ = std::fs::remove_dir_all(&temp_dir);
}
#[test]
#[allow(non_snake_case)]
fn test_RELEASE_011_execute_no_manifest_crate() {
let graph = create_test_graph();
let checker = StackChecker::with_graph(graph);
let config = ReleaseConfig { publish: false, ..Default::default() };
let orchestrator = ReleaseOrchestrator::new(checker, config);
let plan = ReleasePlan {
releases: vec![PlannedRelease {
crate_name: "nonexistent-crate".to_string(),
current_version: semver::Version::new(0, 1, 0),
new_version: semver::Version::new(0, 1, 1),
dependents: vec![],
ready: true,
}],
dry_run: false,
preflight_results: HashMap::new(),
};
let result = orchestrator.execute(&plan);
assert!(result.is_ok());
let release_result = result.unwrap();
assert!(release_result.success);
assert_eq!(release_result.released_crates.len(), 1);
assert!(!release_result.released_crates[0].published);
}
#[test]
#[allow(non_snake_case)]
fn test_RELEASE_012_plan_single_default_bump() {
let graph = create_test_graph();
let checker = StackChecker::with_graph(graph);
let config = ReleaseConfig { bump_type: None, ..Default::default() };
let mut orchestrator = ReleaseOrchestrator::new(checker, config);
let plan = orchestrator.plan_release("trueno").unwrap();
let trueno_release = plan.releases.iter().find(|r| r.crate_name == "trueno").unwrap();
assert_eq!(trueno_release.current_version, semver::Version::new(1, 2, 0));
assert_eq!(trueno_release.new_version, semver::Version::new(1, 2, 1));
}
#[test]
#[allow(non_snake_case)]
fn test_RELEASE_012_plan_single_major_bump() {
let graph = create_test_graph();
let checker = StackChecker::with_graph(graph);
let config = ReleaseConfig { bump_type: Some(BumpType::Major), ..Default::default() };
let mut orchestrator = ReleaseOrchestrator::new(checker, config);
let plan = orchestrator.plan_release("trueno").unwrap();
let trueno_release = plan.releases.iter().find(|r| r.crate_name == "trueno").unwrap();
assert_eq!(trueno_release.new_version, semver::Version::new(2, 0, 0));
}
#[test]
#[allow(non_snake_case)]
fn test_RELEASE_013_run_preflight_full_verification() {
let temp_dir = std::env::temp_dir().join("batuta_release_013_preflight_full");
let _ = std::fs::remove_dir_all(&temp_dir);
std::fs::create_dir_all(&temp_dir).unwrap();
let _ = std::process::Command::new("git").args(["init"]).current_dir(&temp_dir).output();
let graph = create_test_graph();
let checker = StackChecker::with_graph(graph);
let config = ReleaseConfig {
no_verify: false,
lint_command: "true".to_string(),
coverage_command: "true".to_string(),
comply_command: "true".to_string(),
quality_gate_command: "true".to_string(),
tdg_command: "true".to_string(),
dead_code_command: "true".to_string(),
complexity_command: "true".to_string(),
satd_command: "true".to_string(),
popper_command: "true".to_string(),
book_command: "true".to_string(),
examples_command: "true".to_string(),
..Default::default()
};
let mut orchestrator = ReleaseOrchestrator::new(checker, config);
let result = orchestrator.run_preflight("trueno", &temp_dir).unwrap();
assert_eq!(result.checks.len(), 14, "Expected 14 preflight checks");
let plan = orchestrator.plan_release("trueno").unwrap();
let trueno = plan.releases.iter().find(|r| r.crate_name == "trueno").unwrap();
assert!(trueno.ready);
let _ = std::fs::remove_dir_all(&temp_dir);
}
#[test]
#[allow(non_snake_case)]
fn test_RELEASE_013_run_preflight_stores_failure() {
let graph = create_test_graph();
let checker = StackChecker::with_graph(graph);
let config = ReleaseConfig {
no_verify: false,
lint_command: "false".to_string(),
coverage_command: "true".to_string(),
comply_command: String::new(),
quality_gate_command: String::new(),
tdg_command: String::new(),
dead_code_command: String::new(),
complexity_command: String::new(),
satd_command: String::new(),
popper_command: String::new(),
book_command: String::new(),
examples_command: String::new(),
..Default::default()
};
let mut orchestrator = ReleaseOrchestrator::new(checker, config);
let result = orchestrator.run_preflight("trueno", Path::new(".")).unwrap();
assert!(!result.passed);
let plan = orchestrator.plan_release("trueno").unwrap();
let trueno = plan.releases.iter().find(|r| r.crate_name == "trueno").unwrap();
assert!(!trueno.ready);
}
#[test]
#[allow(non_snake_case)]
fn test_RELEASE_012_execute_dry_run() {
let graph = create_test_graph();
let checker = StackChecker::with_graph(graph);
let config = ReleaseConfig { publish: false, ..Default::default() };
let orchestrator = ReleaseOrchestrator::new(checker, config);
let plan = ReleasePlan {
releases: vec![PlannedRelease {
crate_name: "trueno".to_string(),
current_version: semver::Version::new(1, 2, 0),
new_version: semver::Version::new(1, 2, 1),
dependents: vec![],
ready: true,
}],
dry_run: true,
preflight_results: HashMap::new(),
};
let result = orchestrator.execute(&plan);
assert!(result.is_ok());
let release_result = result.unwrap();
assert!(release_result.success);
assert!(release_result.released_crates.is_empty());
assert!(release_result.message.contains("Dry run"));
}
#[test]
#[allow(non_snake_case)]
fn test_RELEASE_012_execute_preflight_failed() {
let graph = create_test_graph();
let checker = StackChecker::with_graph(graph);
let config = ReleaseConfig { publish: false, ..Default::default() };
let orchestrator = ReleaseOrchestrator::new(checker, config);
let mut preflight_results = HashMap::new();
preflight_results.insert(
"trueno".to_string(),
PreflightResult {
crate_name: "trueno".to_string(),
checks: vec![PreflightCheck::fail("lint", "clippy warnings")],
passed: false,
},
);
let plan = ReleasePlan {
releases: vec![PlannedRelease {
crate_name: "trueno".to_string(),
current_version: semver::Version::new(1, 2, 0),
new_version: semver::Version::new(1, 2, 1),
dependents: vec![],
ready: false,
}],
dry_run: false,
preflight_results,
};
let result = orchestrator.execute(&plan);
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("Pre-flight checks failed"),
"Error should mention preflight failure, got: {}",
err_msg
);
}
#[test]
#[allow(non_snake_case)]
fn test_RELEASE_012_plan_all_releases() {
let graph = create_test_graph();
let checker = StackChecker::with_graph(graph);
let config = ReleaseConfig { bump_type: Some(BumpType::Patch), ..Default::default() };
let mut orchestrator = ReleaseOrchestrator::new(checker, config);
let plan = orchestrator.plan_all_releases().unwrap();
assert!(plan.releases.len() >= 3, "Should have at least 3 crates, got {}", plan.releases.len());
for release in &plan.releases {
assert_eq!(
release.new_version.patch,
release.current_version.patch + 1,
"Patch should increment for {}",
release.crate_name
);
}
}
#[test]
#[allow(non_snake_case)]
fn test_RELEASE_011_update_preserves_deps_integration() {
let temp_dir = std::env::temp_dir().join("batuta_release_011_exec_preserves");
let _ = std::fs::remove_dir_all(&temp_dir);
std::fs::create_dir_all(&temp_dir).unwrap();
let manifest = temp_dir.join("Cargo.toml");
std::fs::write(
&manifest,
"[package]\nname = \"test-crate\"\nversion = \"1.0.0\"\n\n[dependencies]\ntrueno = \"0.14.0\"\naprender = { version = \"0.24.0\", features = [\"gpu\"] }\n",
)
.unwrap();
let mut graph = DependencyGraph::new();
graph.add_crate(CrateInfo::new("test-crate", semver::Version::new(1, 0, 0), manifest.clone()));
let checker = StackChecker::with_graph(graph);
let config = ReleaseConfig { publish: false, ..Default::default() };
let orchestrator = ReleaseOrchestrator::new(checker, config);
let new_version = semver::Version::new(1, 0, 1);
let result = orchestrator.update_cargo_toml(&manifest, &new_version);
assert!(result.is_ok(), "update_cargo_toml should succeed: {:?}", result.err());
let content = std::fs::read_to_string(&manifest).unwrap();
assert!(content.contains("\"1.0.1\""), "Package version should be updated to 1.0.1");
assert!(
content.contains("trueno = \"0.14.0\""),
"Dependency trueno version should NOT be changed"
);
assert!(
content.contains("version = \"0.24.0\""),
"Dependency aprender version should NOT be changed"
);
let _ = std::fs::remove_dir_all(&temp_dir);
}
#[test]
#[allow(non_snake_case)]
fn test_RELEASE_011_execute_dry_run_no_changes() {
let temp_dir = std::env::temp_dir().join("batuta_release_011_exec_real");
let _ = std::fs::remove_dir_all(&temp_dir);
std::fs::create_dir_all(&temp_dir).unwrap();
let manifest = temp_dir.join("Cargo.toml");
let original = "[package]\nname = \"my-crate\"\nversion = \"0.5.0\"\nedition = \"2021\"\n\n[dependencies]\nserde = \"1.0\"\n";
std::fs::write(&manifest, original).unwrap();
let mut graph = DependencyGraph::new();
graph.add_crate(CrateInfo::new("my-crate", semver::Version::new(0, 5, 0), manifest.clone()));
let checker = StackChecker::with_graph(graph);
let config = ReleaseConfig { publish: false, ..Default::default() };
let orchestrator = ReleaseOrchestrator::new(checker, config);
let plan = ReleasePlan {
releases: vec![PlannedRelease {
crate_name: "my-crate".to_string(),
current_version: semver::Version::new(0, 5, 0),
new_version: semver::Version::new(0, 6, 0),
dependents: vec![],
ready: true,
}],
dry_run: true,
preflight_results: HashMap::new(),
};
let result = orchestrator.execute(&plan);
assert!(result.is_ok());
let release_result = result.unwrap();
assert!(release_result.success);
assert!(release_result.released_crates.is_empty(), "Dry run should not release");
assert!(release_result.message.contains("Dry run"));
let content = std::fs::read_to_string(&manifest).unwrap();
assert_eq!(content, original, "Dry run should not modify files");
let _ = std::fs::remove_dir_all(&temp_dir);
}
#[test]
#[allow(non_snake_case)]
fn test_RELEASE_012_update_multi_section_manifest() {
let temp_dir = std::env::temp_dir().join("batuta_release_012_exec_real");
let _ = std::fs::remove_dir_all(&temp_dir);
std::fs::create_dir_all(&temp_dir).unwrap();
let manifest = temp_dir.join("Cargo.toml");
std::fs::write(
&manifest,
"[package]\nname = \"multi-crate\"\nversion = \"2.0.0\"\n\n[dependencies]\nversion_check = \"0.9\"\n\n[dev-dependencies]\ntempfile = \"3.0\"\n",
)
.unwrap();
let mut graph = DependencyGraph::new();
graph.add_crate(CrateInfo::new("multi-crate", semver::Version::new(2, 0, 0), manifest.clone()));
let checker = StackChecker::with_graph(graph);
let config = ReleaseConfig { publish: false, ..Default::default() };
let orchestrator = ReleaseOrchestrator::new(checker, config);
let new_version = semver::Version::new(2, 1, 0);
let result = orchestrator.update_cargo_toml(&manifest, &new_version);
assert!(result.is_ok());
let content = std::fs::read_to_string(&manifest).unwrap();
assert!(content.contains("\"2.1.0\""), "Package version updated");
assert!(content.contains("version_check = \"0.9\""), "Dependency version untouched");
assert!(content.contains("tempfile = \"3.0\""), "Dev-dependency untouched");
let _ = std::fs::remove_dir_all(&temp_dir);
}
#[test]
#[allow(non_snake_case)]
fn test_RELEASE_012_plan_single_unknown_crate() {
let graph = create_test_graph();
let checker = StackChecker::with_graph(graph);
let config = ReleaseConfig::default();
let mut orchestrator = ReleaseOrchestrator::new(checker, config);
let plan = orchestrator.plan_release("nonexistent-crate").unwrap();
assert!(
plan.releases.is_empty(),
"Unknown crate should result in empty release plan, got {} releases",
plan.releases.len()
);
}
#[test]
#[allow(non_snake_case)]
fn test_RELEASE_013_plan_single_unknown_crate() {
let graph = create_test_graph();
let checker = StackChecker::with_graph(graph);
let config = ReleaseConfig { bump_type: Some(BumpType::Minor), ..Default::default() };
let mut orchestrator = ReleaseOrchestrator::new(checker, config);
let plan = orchestrator.plan_release("unknown-lib").unwrap();
assert!(
plan.releases.is_empty(),
"Unknown crate with minor bump should still have empty plan, got {} releases",
plan.releases.len()
);
}
#[test]
#[allow(non_snake_case)]
fn test_RELEASE_014_update_cargo_toml_version_no_equals() {
let temp_dir = std::env::temp_dir().join("batuta_release_014_no_equals");
let _ = std::fs::remove_dir_all(&temp_dir);
std::fs::create_dir_all(&temp_dir).unwrap();
let manifest = temp_dir.join("Cargo.toml");
std::fs::write(
&manifest,
"[package]\nname = \"test\"\nversion \"1.0.0\"\nedition = \"2021\"\n",
)
.unwrap();
let graph = create_test_graph();
let checker = StackChecker::with_graph(graph);
let config = ReleaseConfig::default();
let orchestrator = ReleaseOrchestrator::new(checker, config);
let result = orchestrator.update_cargo_toml(&manifest, &semver::Version::new(2, 0, 0));
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("Could not find version field"),
"Error should mention missing version field, got: {}",
err_msg
);
let _ = std::fs::remove_dir_all(&temp_dir);
}
#[test]
#[allow(non_snake_case)]
fn test_RELEASE_014_update_cargo_toml_version_only_in_deps() {
let temp_dir = std::env::temp_dir().join("batuta_release_014_only_deps");
let _ = std::fs::remove_dir_all(&temp_dir);
std::fs::create_dir_all(&temp_dir).unwrap();
let manifest = temp_dir.join("Cargo.toml");
std::fs::write(&manifest, "[dependencies]\ntrueno = { version = \"0.14.0\" }\n").unwrap();
let graph = create_test_graph();
let checker = StackChecker::with_graph(graph);
let config = ReleaseConfig::default();
let orchestrator = ReleaseOrchestrator::new(checker, config);
let result = orchestrator.update_cargo_toml(&manifest, &semver::Version::new(1, 0, 0));
assert!(result.is_err());
let _ = std::fs::remove_dir_all(&temp_dir);
}
#[test]
#[allow(non_snake_case)]
fn test_RELEASE_014_update_cargo_toml_inline_table() {
let temp_dir = std::env::temp_dir().join("batuta_release_014_inline_table");
let _ = std::fs::remove_dir_all(&temp_dir);
std::fs::create_dir_all(&temp_dir).unwrap();
let manifest = temp_dir.join("Cargo.toml");
std::fs::write(
&manifest,
"[package]\nname = \"inline\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[dependencies]\nserde = { version = \"1.0\", features = [\"derive\"] }\n",
)
.unwrap();
let graph = create_test_graph();
let checker = StackChecker::with_graph(graph);
let config = ReleaseConfig::default();
let orchestrator = ReleaseOrchestrator::new(checker, config);
let result = orchestrator.update_cargo_toml(&manifest, &semver::Version::new(0, 2, 0));
assert!(result.is_ok());
let content = std::fs::read_to_string(&manifest).unwrap();
assert!(content.contains("\"0.2.0\""), "Package version updated");
assert!(
content.contains("serde = { version = \"1.0\", features = [\"derive\"] }"),
"Inline table dep preserved"
);
let _ = std::fs::remove_dir_all(&temp_dir);
}
#[test]
#[allow(non_snake_case)]
fn test_RELEASE_015_run_preflight_all_checks_executed() {
let temp_dir = std::env::temp_dir().join("batuta_release_015_all_checks");
let _ = std::fs::remove_dir_all(&temp_dir);
std::fs::create_dir_all(&temp_dir).unwrap();
let _ = std::process::Command::new("git").args(["init"]).current_dir(&temp_dir).output();
let _ = std::process::Command::new("git")
.args(["config", "user.email", "test@test.com"])
.current_dir(&temp_dir)
.output();
let _ = std::process::Command::new("git")
.args(["config", "user.name", "Test"])
.current_dir(&temp_dir)
.output();
std::fs::write(temp_dir.join("test.txt"), "hello").unwrap();
let _ = std::process::Command::new("git").args(["add", "."]).current_dir(&temp_dir).output();
let _ = std::process::Command::new("git")
.args(["commit", "--no-verify", "-m", "init"])
.current_dir(&temp_dir)
.output();
let graph = create_test_graph();
let checker = StackChecker::with_graph(graph);
let config = ReleaseConfig {
no_verify: false,
lint_command: "true".to_string(),
coverage_command: "true".to_string(),
comply_command: "true".to_string(),
quality_gate_command: "true".to_string(),
tdg_command: "true".to_string(),
dead_code_command: "true".to_string(),
complexity_command: "true".to_string(),
satd_command: "true".to_string(),
popper_command: "true".to_string(),
book_command: String::new(),
examples_command: String::new(),
..Default::default()
};
let mut orchestrator = ReleaseOrchestrator::new(checker, config);
let result = orchestrator.run_preflight("trueno", &temp_dir);
assert!(result.is_ok());
let preflight = result.unwrap();
assert_eq!(
preflight.checks.len(),
14,
"Expected 14 preflight checks, got {}",
preflight.checks.len()
);
let check_names: Vec<&str> = preflight.checks.iter().map(|c| c.name.as_str()).collect();
assert!(check_names.contains(&"git_clean"), "Missing git_clean check");
assert!(check_names.contains(&"no_path_deps"), "Missing no_path_deps check");
assert!(check_names.contains(&"version_bumped"), "Missing version_bumped check");
let _ = std::fs::remove_dir_all(&temp_dir);
}
#[test]
#[allow(non_snake_case)]
fn test_RELEASE_015_run_preflight_empty_commands() {
let graph = create_test_graph();
let checker = StackChecker::with_graph(graph);
let config = ReleaseConfig {
no_verify: false,
lint_command: String::new(),
coverage_command: String::new(),
comply_command: String::new(),
quality_gate_command: String::new(),
tdg_command: String::new(),
dead_code_command: String::new(),
complexity_command: String::new(),
satd_command: String::new(),
popper_command: String::new(),
book_command: String::new(),
examples_command: String::new(),
..Default::default()
};
let mut orchestrator = ReleaseOrchestrator::new(checker, config);
let result = orchestrator.run_preflight("trueno", Path::new("."));
assert!(result.is_ok());
let preflight = result.unwrap();
assert_eq!(preflight.checks.len(), 14);
let plan = orchestrator.plan_release("trueno").unwrap();
assert!(!plan.preflight_results.is_empty());
let trueno_release = plan.releases.iter().find(|r| r.crate_name == "trueno").unwrap();
let _ = trueno_release.ready;
}
#[test]
#[allow(non_snake_case)]
fn test_RELEASE_015_preflight_results_stored_in_plan() {
let graph = create_test_graph();
let checker = StackChecker::with_graph(graph);
let config = ReleaseConfig { no_verify: true, ..Default::default() };
let mut orchestrator = ReleaseOrchestrator::new(checker, config);
let _ = orchestrator.run_preflight("trueno", Path::new(".")).unwrap();
let _ = orchestrator.run_preflight("aprender", Path::new(".")).unwrap();
let plan = orchestrator.plan_all_releases().unwrap();
assert!(!plan.preflight_results.is_empty());
assert!(plan.preflight_results.contains_key("trueno"));
assert!(plan.preflight_results.contains_key("aprender"));
}
#[test]
#[allow(non_snake_case)]
fn test_RELEASE_016_plan_single_with_failed_preflight() {
let graph = create_test_graph();
let checker = StackChecker::with_graph(graph);
let config = ReleaseConfig {
no_verify: false,
lint_command: "false".to_string(), coverage_command: String::new(),
comply_command: String::new(),
quality_gate_command: String::new(),
tdg_command: String::new(),
dead_code_command: String::new(),
complexity_command: String::new(),
satd_command: String::new(),
popper_command: String::new(),
book_command: String::new(),
examples_command: String::new(),
..Default::default()
};
let mut orchestrator = ReleaseOrchestrator::new(checker, config);
let preflight = orchestrator.run_preflight("trueno", Path::new(".")).unwrap();
assert!(!preflight.passed, "Preflight should fail with 'false' lint command");
let plan = orchestrator.plan_release("trueno").unwrap();
let trueno = plan.releases.iter().find(|r| r.crate_name == "trueno").unwrap();
assert!(!trueno.ready, "trueno should not be ready after failed preflight");
}