2025-10-14 14:01:11 -07:00
|
|
|
use std::sync::Arc;
|
|
|
|
|
|
|
|
|
|
use hermesllm::apis::openai::{ChatCompletionsRequest, Message, MessageContent, Role};
|
|
|
|
|
use hyper::header::HeaderMap;
|
|
|
|
|
|
|
|
|
|
use crate::handlers::agent_selector::{AgentSelectionError, AgentSelector};
|
|
|
|
|
use crate::handlers::pipeline_processor::PipelineProcessor;
|
2025-12-22 18:05:49 -08:00
|
|
|
use crate::router::plano_orchestrator::OrchestratorService;
|
2026-02-24 14:34:33 -08:00
|
|
|
use common::errors::BrightStaffError;
|
|
|
|
|
use http_body_util::BodyExt;
|
|
|
|
|
use hyper::StatusCode;
|
2025-10-14 14:01:11 -07:00
|
|
|
/// Integration test that demonstrates the modular agent chat flow
|
|
|
|
|
/// This test shows how the three main components work together:
|
2025-12-22 18:05:49 -08:00
|
|
|
/// 1. AgentSelector - selects the appropriate agents based on orchestration
|
2025-10-14 14:01:11 -07:00
|
|
|
/// 2. PipelineProcessor - executes the agent pipeline
|
|
|
|
|
/// 3. ResponseHandler - handles response streaming
|
|
|
|
|
#[cfg(test)]
|
2025-12-25 21:08:37 -08:00
|
|
|
mod tests {
|
2025-10-14 14:01:11 -07:00
|
|
|
use super::*;
|
2026-03-18 17:58:20 -07:00
|
|
|
use common::configuration::{Agent, AgentFilterChain, Listener, ListenerType};
|
2025-10-14 14:01:11 -07:00
|
|
|
|
2025-12-22 18:05:49 -08:00
|
|
|
fn create_test_orchestrator_service() -> Arc<OrchestratorService> {
|
|
|
|
|
Arc::new(OrchestratorService::new(
|
2025-10-14 14:01:11 -07:00
|
|
|
"http://localhost:8080".to_string(),
|
|
|
|
|
"test-model".to_string(),
|
2026-03-15 09:36:11 -07:00
|
|
|
"plano-orchestrator".to_string(),
|
2025-10-14 14:01:11 -07:00
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn create_test_message(role: Role, content: &str) -> Message {
|
|
|
|
|
Message {
|
|
|
|
|
role,
|
2026-01-16 16:24:03 -08:00
|
|
|
content: Some(MessageContent::Text(content.to_string())),
|
2025-10-14 14:01:11 -07:00
|
|
|
name: None,
|
|
|
|
|
tool_calls: None,
|
|
|
|
|
tool_call_id: None,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn test_modular_agent_chat_flow() {
|
|
|
|
|
// Setup services
|
2025-12-22 18:05:49 -08:00
|
|
|
let orchestrator_service = create_test_orchestrator_service();
|
|
|
|
|
let agent_selector = AgentSelector::new(orchestrator_service);
|
2025-12-17 17:30:14 -08:00
|
|
|
let mut pipeline_processor = PipelineProcessor::default();
|
2025-10-14 14:01:11 -07:00
|
|
|
|
|
|
|
|
// Create test data
|
|
|
|
|
let agents = vec![
|
|
|
|
|
Agent {
|
|
|
|
|
id: "filter-agent".to_string(),
|
2025-12-17 17:30:14 -08:00
|
|
|
agent_type: Some("filter".to_string()),
|
2025-10-14 14:01:11 -07:00
|
|
|
url: "http://localhost:8081".to_string(),
|
2025-12-17 17:30:14 -08:00
|
|
|
tool: None,
|
|
|
|
|
transport: None,
|
2025-10-14 14:01:11 -07:00
|
|
|
},
|
|
|
|
|
Agent {
|
|
|
|
|
id: "terminal-agent".to_string(),
|
2025-12-17 17:30:14 -08:00
|
|
|
agent_type: Some("terminal".to_string()),
|
2025-10-14 14:01:11 -07:00
|
|
|
url: "http://localhost:8082".to_string(),
|
2025-12-17 17:30:14 -08:00
|
|
|
tool: None,
|
|
|
|
|
transport: None,
|
2025-10-14 14:01:11 -07:00
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
let agent_pipeline = AgentFilterChain {
|
|
|
|
|
id: "terminal-agent".to_string(),
|
2026-03-18 17:58:20 -07:00
|
|
|
input_filters: Some(vec![
|
2025-12-25 21:08:37 -08:00
|
|
|
"filter-agent".to_string(),
|
|
|
|
|
"terminal-agent".to_string(),
|
|
|
|
|
]),
|
2025-10-14 14:01:11 -07:00
|
|
|
description: Some("Test pipeline".to_string()),
|
|
|
|
|
default: Some(true),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let listener = Listener {
|
2026-03-18 17:58:20 -07:00
|
|
|
listener_type: ListenerType::Agent,
|
2025-10-14 14:01:11 -07:00
|
|
|
name: "test-listener".to_string(),
|
|
|
|
|
agents: Some(vec![agent_pipeline.clone()]),
|
2026-03-18 17:58:20 -07:00
|
|
|
input_filters: None,
|
|
|
|
|
output_filters: None,
|
2025-10-14 14:01:11 -07:00
|
|
|
port: 8080,
|
|
|
|
|
router: None,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let listeners = vec![listener];
|
|
|
|
|
let messages = vec![create_test_message(Role::User, "Hello world!")];
|
|
|
|
|
|
|
|
|
|
// Test 1: Agent Selection
|
|
|
|
|
let selected_listener = agent_selector
|
|
|
|
|
.find_listener(Some("test-listener"), &listeners)
|
|
|
|
|
.await;
|
|
|
|
|
|
|
|
|
|
assert!(selected_listener.is_ok());
|
|
|
|
|
let listener = selected_listener.unwrap();
|
|
|
|
|
assert_eq!(listener.name, "test-listener");
|
|
|
|
|
|
|
|
|
|
// Test 2: Agent Map Creation
|
|
|
|
|
let agent_map = agent_selector.create_agent_map(&agents);
|
|
|
|
|
assert_eq!(agent_map.len(), 2);
|
|
|
|
|
assert!(agent_map.contains_key("filter-agent"));
|
|
|
|
|
assert!(agent_map.contains_key("terminal-agent"));
|
|
|
|
|
|
|
|
|
|
// Test 3: Pipeline Processing (empty filter chain for testing)
|
|
|
|
|
let request = ChatCompletionsRequest {
|
|
|
|
|
messages: messages.clone(),
|
|
|
|
|
model: "test-model".to_string(),
|
|
|
|
|
..Default::default()
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Create a pipeline with empty filter chain to avoid network calls
|
|
|
|
|
let test_pipeline = AgentFilterChain {
|
|
|
|
|
id: "terminal-agent".to_string(),
|
2026-03-18 17:58:20 -07:00
|
|
|
input_filters: Some(vec![]), // Empty filter chain - no network calls needed
|
2025-10-14 14:01:11 -07:00
|
|
|
description: None,
|
|
|
|
|
default: None,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let headers = HeaderMap::new();
|
2026-03-18 17:58:20 -07:00
|
|
|
let request_bytes = serde_json::to_vec(&request).expect("failed to serialize request");
|
2025-10-14 14:01:11 -07:00
|
|
|
let result = pipeline_processor
|
2026-03-18 17:58:20 -07:00
|
|
|
.process_raw_filter_chain(
|
|
|
|
|
&request_bytes,
|
|
|
|
|
&test_pipeline,
|
|
|
|
|
&agent_map,
|
|
|
|
|
&headers,
|
|
|
|
|
"/v1/chat/completions",
|
|
|
|
|
)
|
2025-10-14 14:01:11 -07:00
|
|
|
.await;
|
|
|
|
|
|
|
|
|
|
println!("Pipeline processing result: {:?}", result);
|
|
|
|
|
|
|
|
|
|
assert!(result.is_ok());
|
2026-03-18 17:58:20 -07:00
|
|
|
let processed_bytes = result.unwrap();
|
|
|
|
|
// With empty filter chain, should return the original bytes unchanged
|
|
|
|
|
let processed_request: ChatCompletionsRequest =
|
|
|
|
|
serde_json::from_slice(&processed_bytes).expect("failed to deserialize response");
|
|
|
|
|
assert_eq!(processed_request.messages.len(), 1);
|
|
|
|
|
if let Some(MessageContent::Text(content)) = &processed_request.messages[0].content {
|
2025-10-14 14:01:11 -07:00
|
|
|
assert_eq!(content, "Hello world!");
|
|
|
|
|
} else {
|
|
|
|
|
panic!("Expected text content");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Test 4: Error Response Creation
|
2026-02-24 14:34:33 -08:00
|
|
|
let err = BrightStaffError::ModelNotFound("gpt-5-secret".to_string());
|
|
|
|
|
let response = err.into_response();
|
|
|
|
|
|
|
|
|
|
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
|
|
|
|
|
|
|
|
|
// Helper to extract body as JSON
|
|
|
|
|
let body_bytes = response.into_body().collect().await.unwrap().to_bytes();
|
|
|
|
|
let body: serde_json::Value = serde_json::from_slice(&body_bytes).unwrap();
|
|
|
|
|
|
|
|
|
|
assert_eq!(body["error"]["code"], "ModelNotFound");
|
|
|
|
|
assert_eq!(
|
|
|
|
|
body["error"]["details"]["rejected_model_id"],
|
|
|
|
|
"gpt-5-secret"
|
|
|
|
|
);
|
|
|
|
|
assert!(body["error"]["message"]
|
|
|
|
|
.as_str()
|
|
|
|
|
.unwrap()
|
|
|
|
|
.contains("gpt-5-secret"));
|
2025-10-14 14:01:11 -07:00
|
|
|
|
|
|
|
|
println!("✅ All modular components working correctly!");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn test_error_handling_flow() {
|
2025-12-22 18:05:49 -08:00
|
|
|
let router_service = create_test_orchestrator_service();
|
2025-10-14 14:01:11 -07:00
|
|
|
let agent_selector = AgentSelector::new(router_service);
|
|
|
|
|
|
|
|
|
|
// Test listener not found
|
|
|
|
|
let result = agent_selector.find_listener(Some("nonexistent"), &[]).await;
|
|
|
|
|
|
|
|
|
|
assert!(result.is_err());
|
|
|
|
|
assert!(matches!(
|
|
|
|
|
result.unwrap_err(),
|
|
|
|
|
AgentSelectionError::ListenerNotFound(_)
|
|
|
|
|
));
|
|
|
|
|
|
2026-02-24 14:34:33 -08:00
|
|
|
let technical_reason = "Database connection timed out";
|
|
|
|
|
let err = BrightStaffError::InternalServerError(technical_reason.to_string());
|
|
|
|
|
|
|
|
|
|
let response = err.into_response();
|
|
|
|
|
|
|
|
|
|
// --- 1. EXTRACT BYTES ---
|
|
|
|
|
let body_bytes = response.into_body().collect().await.unwrap().to_bytes();
|
|
|
|
|
|
|
|
|
|
// --- 2. DECLARE body_json HERE ---
|
|
|
|
|
let body_json: serde_json::Value =
|
|
|
|
|
serde_json::from_slice(&body_bytes).expect("Failed to parse JSON body");
|
|
|
|
|
|
|
|
|
|
// --- 3. USE body_json ---
|
|
|
|
|
assert_eq!(body_json["error"]["code"], "InternalServerError");
|
|
|
|
|
assert_eq!(body_json["error"]["details"]["reason"], technical_reason);
|
2025-10-14 14:01:11 -07:00
|
|
|
|
|
|
|
|
println!("✅ Error handling working correctly!");
|
|
|
|
|
}
|
|
|
|
|
}
|