impl TestHarness {
pub fn new() -> Self {
let service = UnifiedService::new()
.with_template_service(DefaultTemplateService)
.with_analysis_service(DefaultAnalysisService);
Self {
service,
mcp_adapter: McpAdapter::new(),
http_adapter: HttpAdapter::new("127.0.0.1:0".parse().unwrap()),
cli_adapter: CliAdapter::new(),
test_results: Arc::new(Mutex::new(TestResults::default())),
}
}
pub fn with_template_service<T: TemplateService + 'static>(mut self, service: T) -> Self {
self.service = self.service.with_template_service(service);
self
}
pub fn with_analysis_service<A: AnalysisService + 'static>(mut self, service: A) -> Self {
self.service = self.service.with_analysis_service(service);
self
}
pub async fn test_endpoint<
T: Serialize,
R: for<'de> Deserialize<'de> + PartialEq + std::fmt::Debug,
>(
&self,
test_name: &str,
method: &str,
path: &str,
body: T,
) -> Result<(), TestError> {
info!("Running test: {}", test_name);
let mut results = self.test_results.lock().await;
results.tests_run += 1;
let protocols = [Protocol::Mcp, Protocol::Http, Protocol::Cli];
let mut responses = HashMap::with_capacity(64);
for protocol in &protocols {
match self.test_protocol(protocol, method, path, &body).await {
Ok(response) => {
responses.insert(*protocol, response);
}
Err(e) => {
results
.protocol_failures
.entry(*protocol)
.or_insert_with(Vec::new)
.push(format!("{}: {}", test_name, e));
debug!("Protocol {} failed for test {}: {}", protocol, test_name, e);
}
}
}
if responses.len() >= 2 {
let protocol_pairs: Vec<_> = responses.keys().collect();
for i in 0..protocol_pairs.len() {
for j in i + 1..protocol_pairs.len() {
let p1 = *protocol_pairs[i];
let p2 = *protocol_pairs[j];
if let (Some(r1), Some(r2)) = (responses.get(&p1), responses.get(&p2)) {
let normalized_r1 = self.normalize_response(r1, p1);
let normalized_r2 = self.normalize_response(r2, p2);
if normalized_r1 != normalized_r2 {
results.equivalence_failures.push(EquivalenceFailure {
test_name: test_name.to_string(),
protocols: (p1, p2),
expected: normalized_r1.clone(),
actual: normalized_r2.clone(),
difference: format!(
"Protocols {} and {} returned different results",
p1, p2
),
});
}
}
}
}
}
if responses.len() == protocols.len() {
results.tests_passed += 1;
Ok(())
} else {
results.tests_failed += 1;
Err(TestError::ProtocolFailure(format!(
"Test {} failed on {} protocols",
test_name,
protocols.len() - responses.len()
)))
}
}
pub async fn test_template_generation(&self) -> Result<(), TestError> {
let request_body = json!({
"template_uri": "template://makefile/rust/cli",
"parameters": {
"project_name": "test-project"
}
});
self.test_endpoint::<_, Value>(
"template_generation",
"POST",
"/api/v1/generate",
request_body,
)
.await
}
pub async fn test_template_listing(&self) -> Result<(), TestError> {
self.test_endpoint::<_, Value>("template_listing", "GET", "/api/v1/templates", json!({}))
.await
}
pub async fn test_complexity_analysis(&self) -> Result<(), TestError> {
let request_body = json!({
"project_path": ".",
"toolchain": "rust",
"format": "json"
});
self.test_endpoint::<_, Value>(
"complexity_analysis",
"POST",
"/api/v1/analyze/complexity",
request_body,
)
.await
}
pub async fn test_error_handling(&self) -> Result<(), TestError> {
let request_body = json!({
"template_uri": "template://nonexistent/template",
"parameters": {}
});
let protocols = [Protocol::Mcp, Protocol::Http, Protocol::Cli];
let mut error_responses = HashMap::with_capacity(64);
for protocol in &protocols {
match self
.test_protocol(protocol, "POST", "/api/v1/generate", &request_body)
.await
{
Ok(_) => {
return Err(TestError::UnexpectedSuccess(format!(
"Protocol {} should have failed but succeeded",
protocol
)));
}
Err(e) => {
error_responses.insert(*protocol, e.to_string());
}
}
}
if error_responses.len() == protocols.len() {
info!("Error handling test passed - all protocols failed as expected");
Ok(())
} else {
Err(TestError::InconsistentBehavior(
"Not all protocols failed consistently".to_string(),
))
}
}
pub async fn run_test_suite(&self) -> TestSuiteResults {
info!("Starting unified protocol test suite");
let tests = vec![
(
"template_generation",
Box::new(|| Box::pin(self.test_template_generation())) as TestFunction,
),
(
"template_listing",
Box::new(|| Box::pin(self.test_template_listing())),
),
(
"complexity_analysis",
Box::new(|| Box::pin(self.test_complexity_analysis())),
),
(
"error_handling",
Box::new(|| Box::pin(self.test_error_handling())),
),
];
let mut suite_results = TestSuiteResults::new();
for (test_name, test_fn) in tests {
info!("Running test: {}", test_name);
match test_fn().await {
Ok(()) => {
suite_results.passed.push(test_name.to_string());
info!("✅ Test {} passed", test_name);
}
Err(e) => {
suite_results
.failed
.push((test_name.to_string(), e.to_string()));
info!("❌ Test {} failed: {}", test_name, e);
}
}
}
let final_results = self.test_results.lock().await.clone();
suite_results.summary = Some(final_results);
info!(
"Test suite completed: {} passed, {} failed",
suite_results.passed.len(),
suite_results.failed.len()
);
suite_results
}
async fn test_protocol<T: Serialize>(
&self,
protocol: &Protocol,
method: &str,
path: &str,
body: &T,
) -> Result<Value, TestError> {
let unified_request = UnifiedRequest::new(
method
.parse()
.map_err(|e| TestError::InvalidRequest(format!("Invalid method: {}", e)))?,
path.to_string(),
)
.with_body(Body::from(serde_json::to_vec(body)?))
.with_header("content-type", "application/json")
.with_extension("protocol", *protocol);
let response = self
.service
.process_request(unified_request)
.await
.map_err(|e| TestError::ServiceError(e.to_string()))?;
if response.status.is_success() {
let body_bytes = axum::body::to_bytes(response.body, usize::MAX)
.await
.map_err(|e| {
TestError::ResponseError(format!("Failed to read response body: {}", e))
})?;
serde_json::from_slice(&body_bytes)
.map_err(|e| TestError::ResponseError(format!("Invalid JSON response: {}", e)))
} else {
Err(TestError::HttpError(
response.status,
String::from_utf8_lossy(
&axum::body::to_bytes(response.body, usize::MAX)
.await
.unwrap_or_default(),
)
.to_string(),
))
}
}
fn normalize_response(&self, response: &Value, protocol: Protocol) -> Value {
let mut normalized = response.clone();
match protocol {
Protocol::Mcp => {
if let Value::Object(ref mut obj) = normalized {
obj.remove("jsonrpc");
obj.remove("id");
}
}
Protocol::Http => {
}
Protocol::Cli => {
if let Value::Object(ref mut obj) = normalized {
obj.remove("exit_code");
}
}
Protocol::WebSocket => {
}
}
normalized
}
}
impl Default for TestHarness {
fn default() -> Self {
Self::new()
}
}