use paladin::application::services::battalion::formation_service::FormationExecutionService;
use paladin::infrastructure::resilience::circuit_breaker::CircuitBreaker;
use paladin::core::base::entity::node::Node;
use paladin::core::platform::container::battalion::BattalionConfig;
use paladin::core::platform::container::battalion::formation::Formation;
use paladin::core::platform::container::paladin::{MaxLoops, PaladinData, PaladinStatus};
use std::sync::Arc;
use std::time::Duration;
use crate::helpers::{MockLlmAdapter, MockPaladinPort};
#[tokio::test]
async fn test_formation_basic_sequential_execution() {
let mock_llm = Arc::new(MockLlmAdapter::new());
mock_llm.add_success("Step 1: Data analyzed");
mock_llm.add_success("Step 2: Analysis refined from previous step");
mock_llm.add_success("Step 3: Final synthesis of all previous steps");
let paladin1_data = PaladinData {
system_prompt: "You are a data analyzer.".to_string(),
name: "Analyzer".to_string(),
user_name: "TestUser".to_string(),
model: "gpt-4".to_string(),
temperature: 0.7,
max_loops: MaxLoops::Fixed(1),
stop_words: vec![],
status: PaladinStatus::Idle,
vision_enabled: false,
..Default::default()
};
let paladin1 = Node::new(paladin1_data, None);
let paladin2_data = PaladinData {
system_prompt: "You refine analysis.".to_string(),
name: "Refiner".to_string(),
user_name: "TestUser".to_string(),
model: "gpt-4".to_string(),
temperature: 0.7,
max_loops: MaxLoops::Fixed(1),
stop_words: vec![],
status: PaladinStatus::Idle,
vision_enabled: false,
..Default::default()
};
let paladin2 = Node::new(paladin2_data, None);
let paladin3_data = PaladinData {
system_prompt: "You synthesize information.".to_string(),
name: "Synthesizer".to_string(),
user_name: "TestUser".to_string(),
model: "gpt-4".to_string(),
temperature: 0.7,
max_loops: MaxLoops::Fixed(1),
stop_words: vec![],
status: PaladinStatus::Idle,
vision_enabled: false,
..Default::default()
};
let paladin3 = Node::new(paladin3_data, None);
let paladins = vec![paladin1, paladin2, paladin3];
let config = BattalionConfig::new("test_formation");
let formation = Formation::new(paladins, config).expect("Failed to create Formation");
let circuit_breaker = Arc::new(CircuitBreaker::new(3, 2, Duration::from_secs(30)));
let paladin_port = Arc::new(MockPaladinPort::new(mock_llm.clone(), circuit_breaker));
let formation_service = FormationExecutionService::new(paladin_port);
let result = formation_service
.execute(&formation, "Analyze this data set")
.await;
assert!(result.is_ok(), "Formation execution should succeed");
let battalion_result = result.unwrap();
assert_eq!(mock_llm.call_count(), 3, "All 3 Paladins should be invoked");
assert_eq!(
battalion_result.paladin_results.len(),
3,
"Should have 3 Paladin results"
);
assert!(
battalion_result.final_output.contains("Step 3")
|| battalion_result.final_output.contains("synthesis"),
"Final output should contain last Paladin's output"
);
}
#[tokio::test]
async fn test_formation_output_chaining() {
let mock_llm = Arc::new(MockLlmAdapter::new());
mock_llm.add_success("OUTPUT_1: First processing done");
mock_llm.add_success("OUTPUT_2: Received OUTPUT_1 and processed it");
let paladin1_data = PaladinData {
system_prompt: "Process the input.".to_string(),
name: "FirstProcessor".to_string(),
user_name: "TestUser".to_string(),
model: "gpt-4".to_string(),
temperature: 0.7,
max_loops: MaxLoops::Fixed(1),
stop_words: vec![],
status: PaladinStatus::Idle,
vision_enabled: false,
..Default::default()
};
let paladin1 = Node::new(paladin1_data, None);
let paladin2_data = PaladinData {
system_prompt: "Process the previous output.".to_string(),
name: "SecondProcessor".to_string(),
user_name: "TestUser".to_string(),
model: "gpt-4".to_string(),
temperature: 0.7,
max_loops: MaxLoops::Fixed(1),
stop_words: vec![],
status: PaladinStatus::Idle,
vision_enabled: false,
..Default::default()
};
let paladin2 = Node::new(paladin2_data, None);
let paladins = vec![paladin1, paladin2];
let config = BattalionConfig::new("chaining_test");
let formation = Formation::new(paladins, config).expect("Failed to create Formation");
let circuit_breaker = Arc::new(CircuitBreaker::new(3, 2, Duration::from_secs(30)));
let paladin_port = Arc::new(MockPaladinPort::new(mock_llm.clone(), circuit_breaker));
let formation_service = FormationExecutionService::new(paladin_port);
let result = formation_service.execute(&formation, "Initial input").await;
assert!(result.is_ok(), "Formation execution should succeed");
assert_eq!(mock_llm.call_count(), 2, "Both Paladins should be invoked");
let invocations = mock_llm.invocations();
assert_eq!(invocations.len(), 2, "Should have 2 invocations");
assert!(
invocations[0].prompt.contains("Initial input"),
"First Paladin should receive initial input"
);
assert!(
invocations[1].prompt.contains("OUTPUT_1")
|| invocations[1].prompt.contains("First processing"),
"Second Paladin should receive output from first Paladin"
);
}
#[tokio::test]
async fn test_formation_with_shared_context() {
let mock_llm = Arc::new(MockLlmAdapter::new());
mock_llm.add_success("Response 1 with context");
mock_llm.add_success("Response 2 with context");
let paladin1_data = PaladinData {
system_prompt: "Process input.".to_string(),
name: "Paladin1".to_string(),
user_name: "TestUser".to_string(),
model: "gpt-4".to_string(),
temperature: 0.7,
max_loops: MaxLoops::Fixed(1),
stop_words: vec![],
status: PaladinStatus::Idle,
vision_enabled: false,
..Default::default()
};
let paladin1 = Node::new(paladin1_data, None);
let paladin2_data = PaladinData {
system_prompt: "Process input.".to_string(),
name: "Paladin2".to_string(),
user_name: "TestUser".to_string(),
model: "gpt-4".to_string(),
temperature: 0.7,
max_loops: MaxLoops::Fixed(1),
stop_words: vec![],
status: PaladinStatus::Idle,
vision_enabled: false,
..Default::default()
};
let paladin2 = Node::new(paladin2_data, None);
let paladins = vec![paladin1, paladin2];
let config = BattalionConfig::new("context_test");
let formation = Formation::new(paladins, config)
.expect("Failed to create Formation")
.with_shared_context("SHARED_CONTEXT: Use this in your analysis".to_string());
let circuit_breaker = Arc::new(CircuitBreaker::new(3, 2, Duration::from_secs(30)));
let paladin_port = Arc::new(MockPaladinPort::new(mock_llm.clone(), circuit_breaker));
let formation_service = FormationExecutionService::new(paladin_port);
let result = formation_service.execute(&formation, "Test input").await;
assert!(result.is_ok(), "Formation execution should succeed");
let invocations = mock_llm.invocations();
assert!(
invocations[0].prompt.contains("SHARED_CONTEXT"),
"First Paladin should receive shared context"
);
}
#[tokio::test]
async fn test_formation_error_propagation() {
let mock_llm = Arc::new(MockLlmAdapter::new());
mock_llm.add_success("First step succeeded");
mock_llm
.add_failure(paladin_ports::output::llm_port::LlmError::RateLimitExceeded);
let paladin1_data = PaladinData {
system_prompt: "First step.".to_string(),
name: "FirstStep".to_string(),
user_name: "TestUser".to_string(),
model: "gpt-4".to_string(),
temperature: 0.7,
max_loops: MaxLoops::Fixed(1),
stop_words: vec![],
status: PaladinStatus::Idle,
vision_enabled: false,
..Default::default()
};
let paladin1 = Node::new(paladin1_data, None);
let paladin2_data = PaladinData {
system_prompt: "Second step.".to_string(),
name: "SecondStep".to_string(),
user_name: "TestUser".to_string(),
model: "gpt-4".to_string(),
temperature: 0.7,
max_loops: MaxLoops::Fixed(1),
stop_words: vec![],
status: PaladinStatus::Idle,
vision_enabled: false,
..Default::default()
};
let paladin2 = Node::new(paladin2_data, None);
let paladins = vec![paladin1, paladin2];
let config = BattalionConfig::new("error_test");
let formation = Formation::new(paladins, config).expect("Failed to create Formation");
let circuit_breaker = Arc::new(CircuitBreaker::new(3, 2, Duration::from_secs(30)));
let paladin_port = Arc::new(MockPaladinPort::new(mock_llm.clone(), circuit_breaker));
let formation_service = FormationExecutionService::new(paladin_port);
let result = formation_service.execute(&formation, "Test input").await;
assert!(
result.is_err(),
"Formation should fail when a Paladin errors"
);
}