pub mod campaign;
pub mod chain_of_command;
pub mod conclave;
pub mod council;
pub mod formation;
pub mod grove;
pub mod phalanx;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use std::time::Duration;
use uuid::Uuid;
use crate::platform::container::execution_result::PaladinResult;
use crate::platform::container::paladin_error::PaladinError;
use crate::platform::container::registry_error::RegistryError;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BattalionConfig {
pub name: String,
pub description: Option<String>,
pub timeout_seconds: u64,
pub retry_policy: RetryPolicy,
pub error_strategy: ErrorStrategy,
pub metadata_output_dir: Option<PathBuf>,
}
impl BattalionConfig {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
description: None,
timeout_seconds: 300,
retry_policy: RetryPolicy::default(),
error_strategy: ErrorStrategy::default(),
metadata_output_dir: None,
}
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn with_timeout(mut self, seconds: u64) -> Self {
self.timeout_seconds = seconds;
self
}
pub fn with_retry_policy(mut self, policy: RetryPolicy) -> Self {
self.retry_policy = policy;
self
}
pub fn with_error_strategy(mut self, strategy: ErrorStrategy) -> Self {
self.error_strategy = strategy;
self
}
pub fn with_metadata_dir(mut self, dir: PathBuf) -> Self {
self.metadata_output_dir = Some(dir);
self
}
pub fn validate_metadata_dir(&self) -> Result<(), String> {
if let Some(dir) = &self.metadata_output_dir {
if !dir.exists() {
std::fs::create_dir_all(dir).map_err(|e| {
format!(
"Metadata output directory '{}' does not exist and could not be created: {}",
dir.display(),
e
)
})?;
}
if !dir.is_dir() {
return Err(format!(
"Metadata output path '{}' is not a directory",
dir.display()
));
}
let test_path = dir.join(".paladin_write_test");
std::fs::write(&test_path, b"test").map_err(|e| {
format!(
"Metadata output directory '{}' is not writable: {}",
dir.display(),
e
)
})?;
let _ = std::fs::remove_file(&test_path);
}
Ok(())
}
}
impl Default for BattalionConfig {
fn default() -> Self {
Self::new("default_battalion")
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RetryPolicy {
pub max_attempts: u32,
pub base_delay: Duration,
pub max_delay: Duration,
pub exponential_backoff: bool,
pub jitter: bool,
}
impl Default for RetryPolicy {
fn default() -> Self {
Self {
max_attempts: 3,
base_delay: Duration::from_millis(100),
max_delay: Duration::from_secs(10),
exponential_backoff: true,
jitter: true,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum ErrorStrategy {
#[default]
FailFast,
ContinueOnError,
RetryThenContinue,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum BattalionStrategy {
Formation,
Phalanx,
Campaign,
ChainOfCommand,
Conclave,
Council,
Grove,
Maneuver,
Auto,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum BattalionStatus {
#[default]
Idle,
Running,
Paused,
Completed,
Failed,
Cancelled,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub struct TokenUsage {
pub prompt_tokens: u32,
pub completion_tokens: u32,
pub total_tokens: u32,
}
impl TokenUsage {
pub fn new(prompt_tokens: u32, completion_tokens: u32) -> Self {
Self {
prompt_tokens,
completion_tokens,
total_tokens: prompt_tokens + completion_tokens,
}
}
pub fn from_total(total_tokens: u32) -> Self {
Self {
prompt_tokens: 0,
completion_tokens: 0,
total_tokens,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BattalionResult {
pub battalion_id: Uuid,
pub battalion_name: String,
pub started_at: DateTime<Utc>,
pub completed_at: DateTime<Utc>,
pub final_output: String,
pub paladin_results: Vec<PaladinResult>,
pub status: BattalionStatus,
pub strategy_used: BattalionStrategy,
pub strategy_selection_reasoning: Option<String>,
pub strategy_selection_time_ms: u64,
pub per_paladin_times: HashMap<String, u64>,
pub per_paladin_tokens: HashMap<String, TokenUsage>,
pub total_tokens: u64,
pub paladin_success_count: usize,
pub paladin_failure_count: usize,
}
impl BattalionResult {
pub fn new(
battalion_id: Uuid,
battalion_name: String,
started_at: DateTime<Utc>,
final_output: String,
paladin_results: Vec<PaladinResult>,
) -> Self {
let paladin_success_count = paladin_results
.iter()
.filter(|r| {
matches!(
r.stop_reason,
crate::platform::container::execution_result::StopReason::Completed
)
})
.count();
let paladin_failure_count = paladin_results.len() - paladin_success_count;
Self {
battalion_id,
battalion_name,
started_at,
completed_at: Utc::now(),
final_output,
paladin_results,
status: BattalionStatus::Completed,
strategy_used: BattalionStrategy::Formation, strategy_selection_reasoning: None,
strategy_selection_time_ms: 0,
per_paladin_times: HashMap::new(),
per_paladin_tokens: HashMap::new(),
total_tokens: 0,
paladin_success_count,
paladin_failure_count,
}
}
pub fn from_paladin_results(
battalion_id: Uuid,
battalion_name: String,
started_at: DateTime<Utc>,
results: Vec<PaladinResult>,
) -> Self {
let final_output = results.last().map(|r| r.output.clone()).unwrap_or_default();
Self::new(
battalion_id,
battalion_name,
started_at,
final_output,
results,
)
}
pub fn duration(&self) -> Duration {
(self.completed_at - self.started_at)
.to_std()
.unwrap_or_default()
}
pub fn with_strategy(mut self, strategy: BattalionStrategy) -> Self {
self.strategy_used = strategy;
self
}
pub fn with_selection_reasoning(mut self, reasoning: String) -> Self {
self.strategy_selection_reasoning = Some(reasoning);
self
}
pub fn with_selection_time_ms(mut self, time_ms: u64) -> Self {
self.strategy_selection_time_ms = time_ms;
self
}
pub fn with_paladin_times(mut self, times: HashMap<String, u64>) -> Self {
self.per_paladin_times = times;
self
}
pub fn with_paladin_tokens(mut self, tokens: HashMap<String, TokenUsage>) -> Self {
self.per_paladin_tokens = tokens;
self
}
pub fn with_total_tokens(mut self, total_tokens: u64) -> Self {
self.total_tokens = total_tokens;
self
}
}
#[derive(Debug, Clone, thiserror::Error)]
pub enum CouncilError {
#[error("No participants configured")]
NoParticipants,
#[error("Moderator required for ModeratorDirected strategy")]
ModeratorRequired,
#[error("Participant execution failed: {0}")]
ParticipantError(String),
#[error("Invalid turn strategy configuration: {0}")]
InvalidStrategy(String),
#[error("Maximum rounds must be greater than zero")]
InvalidMaxRounds,
}
#[derive(Debug, Clone, thiserror::Error)]
pub enum GroveError {
#[error("No trees configured")]
NoTrees,
#[error("No agents in grove")]
NoAgents,
#[error("Routing failed: {0}")]
RoutingFailed(String),
#[error("No agent meets similarity threshold {0}")]
NoMatchingAgent(f32),
#[error("Embeddings required for SemanticSimilarity strategy")]
EmbeddingsRequired,
#[error("Invalid similarity threshold: {0} (must be between 0.0 and 1.0)")]
InvalidSimilarityThreshold(f32),
}
#[derive(Debug, Clone, thiserror::Error)]
pub enum BattalionError {
#[error("Configuration error: {0}")]
ConfigurationError(String),
#[error("Paladin error: {0}")]
PaladinError(String),
#[error("Formation error: {0}")]
FormationError(String),
#[error("Phalanx error: {0}")]
PhalanxError(String),
#[error("Campaign error: {0}")]
CampaignError(String),
#[error("Invalid graph: {0}")]
InvalidGraph(String),
#[error("Chain of Command error: {0}")]
ChainOfCommandError(String),
#[error("Commander validation error: {0}")]
CommanderValidation(String),
#[error("Strategy selection failed: {0}")]
StrategySelection(String),
#[error("Council error: {0}")]
CouncilError(#[from] CouncilError),
#[error("Grove error: {0}")]
GroveError(#[from] GroveError),
#[error("Routing error: {0}")]
RoutingError(String),
#[error("Paladin not found in registry: {0}")]
PaladinNotFound(String),
#[error("Grove routing failed: {0}")]
GroveRoutingFailed(String),
#[error("Metadata export failed: {0}")]
MetadataExportFailed(String),
#[error("Battalion execution timed out after {0} seconds")]
Timeout(u64),
#[error("Validation error: {0}")]
ValidationError(String),
#[error("Aggregation error: {0}")]
AggregationError(String),
#[error("Battalion execution was cancelled")]
Cancelled,
#[error("Execution error: {0}")]
ExecutionError(String),
}
impl From<RegistryError> for BattalionError {
fn from(error: RegistryError) -> Self {
match error {
RegistryError::DuplicateId(id) => {
BattalionError::ConfigurationError(format!("Duplicate Paladin ID: {}", id))
}
RegistryError::InvalidId(id) => {
BattalionError::ValidationError(format!("Invalid Paladin ID: {}", id))
}
RegistryError::AccessFailed(msg) => {
BattalionError::ExecutionError(format!("Registry access failed: {}", msg))
}
}
}
}
impl From<PaladinError> for BattalionError {
fn from(err: PaladinError) -> Self {
BattalionError::PaladinError(err.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_battalion_config_default() {
let config = BattalionConfig::default();
assert_eq!(config.name, "default_battalion");
assert_eq!(config.timeout_seconds, 300);
assert_eq!(config.error_strategy, ErrorStrategy::FailFast);
}
#[test]
fn test_battalion_config_builder() {
let config = BattalionConfig::new("test_battalion")
.with_description("Test description")
.with_timeout(600)
.with_error_strategy(ErrorStrategy::ContinueOnError);
assert_eq!(config.name, "test_battalion");
assert_eq!(config.description, Some("Test description".to_string()));
assert_eq!(config.timeout_seconds, 600);
assert_eq!(config.error_strategy, ErrorStrategy::ContinueOnError);
}
#[test]
fn test_retry_policy_default() {
let policy = RetryPolicy::default();
assert_eq!(policy.max_attempts, 3);
assert_eq!(policy.base_delay, Duration::from_millis(100));
assert_eq!(policy.max_delay, Duration::from_secs(10));
assert!(policy.exponential_backoff);
assert!(policy.jitter);
}
#[test]
fn test_error_strategy_variants() {
let fail_fast = ErrorStrategy::FailFast;
let continue_on_error = ErrorStrategy::ContinueOnError;
let retry_then_continue = ErrorStrategy::RetryThenContinue;
assert_eq!(fail_fast, ErrorStrategy::FailFast);
assert_eq!(continue_on_error, ErrorStrategy::ContinueOnError);
assert_eq!(retry_then_continue, ErrorStrategy::RetryThenContinue);
assert_ne!(fail_fast, continue_on_error);
}
#[test]
fn test_error_strategy_default() {
let strategy = ErrorStrategy::default();
assert_eq!(strategy, ErrorStrategy::FailFast);
}
#[test]
fn test_battalion_status_variants() {
assert_eq!(BattalionStatus::default(), BattalionStatus::Idle);
let statuses = vec![
BattalionStatus::Idle,
BattalionStatus::Running,
BattalionStatus::Paused,
BattalionStatus::Completed,
BattalionStatus::Failed,
BattalionStatus::Cancelled,
];
for status in statuses {
let _ = status;
}
}
#[test]
fn test_battalion_result_new() {
let battalion_id = Uuid::new_v4();
let started_at = Utc::now();
let result = BattalionResult::new(
battalion_id,
"test_battalion".to_string(),
started_at,
"final output".to_string(),
vec![],
);
assert_eq!(result.battalion_id, battalion_id);
assert_eq!(result.battalion_name, "test_battalion");
assert_eq!(result.final_output, "final output");
assert_eq!(result.status, BattalionStatus::Completed);
assert!(result.paladin_results.is_empty());
}
#[test]
fn test_battalion_result_duration() {
let battalion_id = Uuid::new_v4();
let started_at = Utc::now();
let mut result = BattalionResult::new(
battalion_id,
"test_battalion".to_string(),
started_at,
"output".to_string(),
vec![],
);
result.completed_at = started_at + chrono::Duration::seconds(2);
let duration = result.duration();
assert_eq!(duration.as_secs(), 2);
}
#[test]
fn test_battalion_config_serialization() {
let config = BattalionConfig::new("test").with_timeout(120);
let json = serde_json::to_string(&config).unwrap();
let deserialized: BattalionConfig = serde_json::from_str(&json).unwrap();
assert_eq!(config.name, deserialized.name);
assert_eq!(config.timeout_seconds, deserialized.timeout_seconds);
}
#[test]
fn test_retry_policy_serialization() {
let policy = RetryPolicy::default();
let json = serde_json::to_string(&policy).unwrap();
let deserialized: RetryPolicy = serde_json::from_str(&json).unwrap();
assert_eq!(policy.max_attempts, deserialized.max_attempts);
assert_eq!(policy.exponential_backoff, deserialized.exponential_backoff);
}
#[test]
fn test_error_strategy_serialization() {
let strategy = ErrorStrategy::ContinueOnError;
let json = serde_json::to_string(&strategy).unwrap();
let deserialized: ErrorStrategy = serde_json::from_str(&json).unwrap();
assert_eq!(strategy, deserialized);
}
#[test]
fn test_battalion_status_serialization() {
let status = BattalionStatus::Running;
let json = serde_json::to_string(&status).unwrap();
let deserialized: BattalionStatus = serde_json::from_str(&json).unwrap();
assert_eq!(status, deserialized);
}
#[test]
fn test_battalion_result_serialization() {
let result = BattalionResult::new(
Uuid::new_v4(),
"test".to_string(),
Utc::now(),
"output".to_string(),
vec![],
);
let json = serde_json::to_string(&result).unwrap();
let deserialized: BattalionResult = serde_json::from_str(&json).unwrap();
assert_eq!(result.battalion_id, deserialized.battalion_id);
assert_eq!(result.battalion_name, deserialized.battalion_name);
assert_eq!(result.final_output, deserialized.final_output);
}
#[test]
fn test_battalion_error_variants() {
let errors = vec![
BattalionError::ConfigurationError("test".to_string()),
BattalionError::PaladinError("test".to_string()),
BattalionError::FormationError("test".to_string()),
BattalionError::PhalanxError("test".to_string()),
BattalionError::CampaignError("test".to_string()),
BattalionError::InvalidGraph("test".to_string()),
BattalionError::ChainOfCommandError("test".to_string()),
BattalionError::Timeout(300),
BattalionError::ValidationError("test".to_string()),
BattalionError::AggregationError("test".to_string()),
BattalionError::PaladinNotFound("paladin123".to_string()),
BattalionError::GroveRoutingFailed("no matching agent".to_string()),
BattalionError::MetadataExportFailed("disk full".to_string()),
];
for error in errors {
let msg = error.to_string();
assert!(!msg.is_empty());
}
}
#[test]
fn test_new_error_messages_format() {
let error = BattalionError::PaladinNotFound("test_paladin".to_string());
assert_eq!(
error.to_string(),
"Paladin not found in registry: test_paladin"
);
let error = BattalionError::GroveRoutingFailed("confidence too low".to_string());
assert_eq!(
error.to_string(),
"Grove routing failed: confidence too low"
);
let error = BattalionError::MetadataExportFailed("permission denied".to_string());
assert_eq!(
error.to_string(),
"Metadata export failed: permission denied"
);
}
#[test]
fn test_registry_error_conversion() {
use crate::platform::container::registry_error::RegistryError;
let registry_error = RegistryError::DuplicateId("duplicate_id".to_string());
let battalion_error: BattalionError = registry_error.into();
assert!(matches!(
battalion_error,
BattalionError::ConfigurationError(_)
));
assert!(
battalion_error
.to_string()
.contains("Duplicate Paladin ID: duplicate_id")
);
let registry_error = RegistryError::InvalidId("".to_string());
let battalion_error: BattalionError = registry_error.into();
assert!(matches!(
battalion_error,
BattalionError::ValidationError(_)
));
assert!(battalion_error.to_string().contains("Invalid Paladin ID"));
let registry_error = RegistryError::AccessFailed("lock poisoned".to_string());
let battalion_error: BattalionError = registry_error.into();
assert!(matches!(battalion_error, BattalionError::ExecutionError(_)));
assert!(
battalion_error
.to_string()
.contains("Registry access failed")
);
}
#[test]
fn test_token_usage_new() {
let usage = TokenUsage::new(100, 50);
assert_eq!(usage.prompt_tokens, 100);
assert_eq!(usage.completion_tokens, 50);
assert_eq!(usage.total_tokens, 150);
}
#[test]
fn test_token_usage_from_total() {
let usage = TokenUsage::from_total(200);
assert_eq!(usage.prompt_tokens, 0);
assert_eq!(usage.completion_tokens, 0);
assert_eq!(usage.total_tokens, 200);
}
#[test]
fn test_token_usage_default() {
let usage = TokenUsage::default();
assert_eq!(usage.prompt_tokens, 0);
assert_eq!(usage.completion_tokens, 0);
assert_eq!(usage.total_tokens, 0);
}
#[test]
fn test_token_usage_serialization() {
let usage = TokenUsage::new(100, 50);
let json = serde_json::to_string(&usage).unwrap();
let deserialized: TokenUsage = serde_json::from_str(&json).unwrap();
assert_eq!(usage, deserialized);
}
#[test]
fn test_battalion_metadata_serialization() {
let mut per_paladin_times = HashMap::new();
per_paladin_times.insert("analyst".to_string(), 1500u64);
per_paladin_times.insert("reviewer".to_string(), 2300u64);
let mut per_paladin_tokens = HashMap::new();
per_paladin_tokens.insert("analyst".to_string(), TokenUsage::new(500, 200));
per_paladin_tokens.insert("reviewer".to_string(), TokenUsage::new(300, 150));
let result = BattalionResult::new(
Uuid::new_v4(),
"metadata_test".to_string(),
Utc::now(),
"output".to_string(),
vec![],
)
.with_paladin_times(per_paladin_times.clone())
.with_paladin_tokens(per_paladin_tokens.clone())
.with_total_tokens(1150);
let json = serde_json::to_string(&result).unwrap();
let deserialized: BattalionResult = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.per_paladin_times.len(), 2);
assert_eq!(
deserialized.per_paladin_times.get("analyst"),
Some(&1500u64)
);
assert_eq!(
deserialized.per_paladin_times.get("reviewer"),
Some(&2300u64)
);
assert_eq!(deserialized.per_paladin_tokens.len(), 2);
assert_eq!(
deserialized.per_paladin_tokens.get("analyst"),
Some(&TokenUsage::new(500, 200))
);
assert_eq!(deserialized.total_tokens, 1150);
}
#[test]
fn test_token_usage_aggregation_calculation() {
let mut per_paladin_tokens = HashMap::new();
per_paladin_tokens.insert("agent_a".to_string(), TokenUsage::new(500, 200));
per_paladin_tokens.insert("agent_b".to_string(), TokenUsage::new(300, 150));
per_paladin_tokens.insert("agent_c".to_string(), TokenUsage::from_total(100));
let total_tokens: u64 = per_paladin_tokens
.values()
.map(|t| u64::from(t.total_tokens))
.sum();
assert_eq!(total_tokens, 1250);
let result = BattalionResult::new(
Uuid::new_v4(),
"aggregation_test".to_string(),
Utc::now(),
"output".to_string(),
vec![],
)
.with_paladin_tokens(per_paladin_tokens)
.with_total_tokens(total_tokens);
assert_eq!(result.total_tokens, 1250);
assert_eq!(result.per_paladin_tokens.len(), 3);
let agent_a = result.per_paladin_tokens.get("agent_a").unwrap();
assert_eq!(agent_a.prompt_tokens, 500);
assert_eq!(agent_a.completion_tokens, 200);
assert_eq!(agent_a.total_tokens, 700);
let agent_b = result.per_paladin_tokens.get("agent_b").unwrap();
assert_eq!(agent_b.prompt_tokens, 300);
assert_eq!(agent_b.completion_tokens, 150);
assert_eq!(agent_b.total_tokens, 450);
}
#[test]
fn test_battalion_result_new_initializes_empty_metrics() {
let result = BattalionResult::new(
Uuid::new_v4(),
"test".to_string(),
Utc::now(),
"output".to_string(),
vec![],
);
assert!(result.per_paladin_times.is_empty());
assert!(result.per_paladin_tokens.is_empty());
assert_eq!(result.total_tokens, 0);
}
#[test]
fn test_battalion_result_builder_methods() {
let mut times = HashMap::new();
times.insert("paladin_1".to_string(), 1000u64);
let mut tokens = HashMap::new();
tokens.insert("paladin_1".to_string(), TokenUsage::new(100, 50));
let result = BattalionResult::new(
Uuid::new_v4(),
"builder_test".to_string(),
Utc::now(),
"output".to_string(),
vec![],
)
.with_paladin_times(times)
.with_paladin_tokens(tokens)
.with_total_tokens(150);
assert_eq!(result.per_paladin_times.len(), 1);
assert_eq!(result.per_paladin_times.get("paladin_1"), Some(&1000u64));
assert_eq!(result.per_paladin_tokens.len(), 1);
assert_eq!(result.total_tokens, 150);
}
}
#[test]
fn test_battalion_strategy_creation() {
let formation = BattalionStrategy::Formation;
let phalanx = BattalionStrategy::Phalanx;
let campaign = BattalionStrategy::Campaign;
let chain = BattalionStrategy::ChainOfCommand;
let auto = BattalionStrategy::Auto;
assert_eq!(formation, BattalionStrategy::Formation);
assert_eq!(phalanx, BattalionStrategy::Phalanx);
assert_eq!(campaign, BattalionStrategy::Campaign);
assert_eq!(chain, BattalionStrategy::ChainOfCommand);
assert_eq!(auto, BattalionStrategy::Auto);
assert_ne!(formation, phalanx);
}
#[test]
fn test_battalion_strategy_serialization() {
let strategy = BattalionStrategy::Formation;
let serialized = serde_json::to_string(&strategy).unwrap();
let deserialized: BattalionStrategy = serde_json::from_str(&serialized).unwrap();
assert_eq!(strategy, deserialized);
let auto = BattalionStrategy::Auto;
let serialized = serde_json::to_string(&auto).unwrap();
let deserialized: BattalionStrategy = serde_json::from_str(&serialized).unwrap();
assert_eq!(auto, deserialized);
}
#[test]
fn test_battalion_config_with_metadata_dir() {
let dir = std::env::temp_dir().join("paladin_test_metadata_8_0");
let config = BattalionConfig::new("meta_test")
.with_timeout(120)
.with_metadata_dir(dir.clone());
assert_eq!(config.metadata_output_dir, Some(dir.clone()));
assert!(config.validate_metadata_dir().is_ok());
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn test_battalion_config_without_metadata_dir() {
let config = BattalionConfig::new("no_meta_test").with_timeout(120);
assert_eq!(config.metadata_output_dir, None);
assert!(config.validate_metadata_dir().is_ok());
}
#[test]
fn test_battalion_config_metadata_dir_not_a_directory() {
let file_path = std::env::temp_dir().join("paladin_test_not_a_dir_8_0");
std::fs::write(&file_path, b"not a directory").unwrap();
let config = BattalionConfig::new("not_dir_test")
.with_timeout(120)
.with_metadata_dir(file_path.clone());
let result = config.validate_metadata_dir();
assert!(result.is_err());
assert!(result.unwrap_err().contains("is not a directory"));
let _ = std::fs::remove_file(&file_path);
}
#[test]
fn test_battalion_config_metadata_dir_auto_creates() {
let dir = std::env::temp_dir().join("paladin_test_auto_create_8_0");
let _ = std::fs::remove_dir_all(&dir);
assert!(!dir.exists());
let config = BattalionConfig::new("auto_create_test")
.with_timeout(120)
.with_metadata_dir(dir.clone());
assert!(config.validate_metadata_dir().is_ok());
assert!(dir.exists());
assert!(dir.is_dir());
let _ = std::fs::remove_dir_all(&dir);
}