//! Common test utilities for bidirectional flow testing. //! //! This module provides mock implementations and test helpers for testing //! the bidirectional request/response flow in the ACP Server. use anyhow::Result; use serde_json::{json, Value}; use std::sync::Arc; use tokio::sync::{mpsc, oneshot, Mutex}; use std::collections::HashMap; /// Mock event for testing event bridge #[derive(Debug, Clone)] pub struct MockEvent { pub event_type: String, pub data: Value, } /// Mock SSE client for testing /// /// Simulates an HTTP client that receives SSE events and posts responses. pub struct MockSseClient { pub client_id: String, /// Events received via SSE pub received_events: Arc>>, /// Sender for simulating HTTP POST responses pub response_tx: mpsc::UnboundedSender<(Value, oneshot::Sender>)>, } impl MockSseClient { pub fn new(client_id: String) -> (Self, mpsc::UnboundedReceiver<(Value, oneshot::Sender>)>) { let (response_tx, response_rx) = mpsc::unbounded_channel(); ( Self { client_id, received_events: Arc::new(Mutex::new(Vec::new())), response_tx, }, response_rx, ) } /// Simulate receiving an SSE event pub async fn receive_sse(&self, event_type: String, data: Value) { let mut events = self.received_events.lock().await; events.push(MockEvent { event_type, data }); } /// Get all received events pub async fn get_events(&self) -> Vec { let events = self.received_events.lock().await; events.clone() } /// Get the most recent event of a specific type pub async fn get_latest_event(&self, event_type: &str) -> Option { let events = self.received_events.lock().await; events.iter() .filter(|e| e.event_type == event_type) .last() .cloned() } /// Clear received events pub async fn clear_events(&self) { let mut events = self.received_events.lock().await; events.clear(); } /// Simulate sending a response to /acp/agent_response pub async fn send_response(&self, response: Value) -> Result<()> { let (tx, rx) = oneshot::channel(); self.response_tx.send((response, tx)) .map_err(|_| anyhow::anyhow!("Failed to send response"))?; rx.await? } } /// Mock connector for testing /// /// Simulates an ACP connector that can send agent requests and receive responses. pub struct MockConnector { pub connector_id: String, /// Channel for receiving agent requests from this connector pub request_tx: mpsc::UnboundedSender<(Value, String, String, Value)>, /// Pending responses (request_id -> sender) pub pending_responses: Arc>>>, } impl MockConnector { pub fn new( connector_id: String, ) -> (Self, mpsc::UnboundedReceiver<(Value, String, String, Value)>) { let (request_tx, request_rx) = mpsc::unbounded_channel(); ( Self { connector_id, request_tx, pending_responses: Arc::new(Mutex::new(HashMap::new())), }, request_rx, ) } /// Simulate sending an agent request pub async fn send_agent_request( &self, session_id: String, request_id: Value, method: String, params: Value, ) -> oneshot::Receiver { let (tx, rx) = oneshot::channel(); // Store the response sender let mut pending = self.pending_responses.lock().await; pending.insert(request_id.clone(), tx); drop(pending); // Send the request self.request_tx.send((request_id, session_id, method, params)) .expect("Failed to send agent request"); rx } /// Complete a pending response (simulating response from ACP Server) pub async fn complete_response(&self, request_id: Value, response: Value) -> Result<()> { let mut pending = self.pending_responses.lock().await; if let Some(tx) = pending.remove(&request_id) { tx.send(response) .map_err(|_| anyhow::anyhow!("Failed to send response to connector"))?; Ok(()) } else { Err(anyhow::anyhow!("No pending response for request_id: {}", request_id)) } } } /// Test context for integration tests /// /// Provides a complete test environment with mocked components. pub struct TestContext { /// Mock SSE clients by client_id pub clients: Arc>>, /// Mock connectors by connector_id pub connectors: Arc>>, } impl TestContext { pub fn new() -> Self { Self { clients: Arc::new(Mutex::new(HashMap::new())), connectors: Arc::new(Mutex::new(HashMap::new())), } } /// Create a mock SSE client pub async fn create_client( &self, client_id: String, ) -> (MockSseClient, mpsc::UnboundedReceiver<(Value, oneshot::Sender>)>) { let (client, response_rx) = MockSseClient::new(client_id.clone()); let mut clients = self.clients.lock().await; clients.insert(client_id.clone(), client.clone()); (client, response_rx) } /// Create a mock connector pub async fn create_connector( &self, connector_id: String, ) -> (MockConnector, mpsc::UnboundedReceiver<(Value, String, String, Value)>) { let (connector, request_rx) = MockConnector::new(connector_id.clone()); let mut connectors = self.connectors.lock().await; connectors.insert(connector_id.clone(), connector.clone()); (connector, request_rx) } /// Get a client by ID pub async fn get_client(&self, client_id: &str) -> Option { let clients = self.clients.lock().await; clients.get(client_id).cloned() } /// Get a connector by ID pub async fn get_connector(&self, connector_id: &str) -> Option { let connectors = self.connectors.lock().await; connectors.get(connector_id).cloned() } } impl Default for TestContext { fn default() -> Self { Self::new() } } /// Helper to create a sample permission request pub fn sample_permission_request(request_id: u64) -> Value { json!({ "jsonrpc": "2.0", "id": request_id, "method": "session/request_permission", "params": { "sessionId": "test-session", "tool": "Write", "parameters": { "path": "/tmp/test.txt", "content": "test" } } }) } /// Helper to create a sample permission response pub fn sample_permission_response(request_id: u64, allow: bool) -> Value { json!({ "jsonrpc": "2.0", "id": request_id, "result": { "selectedOptionId": if allow { "allow" } else { "deny" } } }) } /// Helper to extract agent_request data from SSE event pub fn extract_agent_request(event: &MockEvent) -> Option<(Value, String, Value)> { if event.event_type != "session/update" { return None; } let update = event.data.get("update")?; if update.get("sessionUpdate")?.as_str()? != "agent_request" { return None; } let request_id = update.get("requestId")?.clone(); let method = update.get("method")?.as_str()?.to_string(); let params = update.get("params")?.clone(); Some((request_id, method, params)) } #[cfg(test)] mod tests { use super::*; #[tokio::test] async fn test_mock_client() { let (client, _response_rx) = MockSseClient::new("test-client".to_string()); // Simulate receiving an event client.receive_sse("session/update".to_string(), json!({"test": "data"})).await; // Verify event was received let events = client.get_events().await; assert_eq!(events.len(), 1); assert_eq!(events[0].event_type, "session/update"); } #[tokio::test] async fn test_mock_connector() { let (connector, mut request_rx) = MockConnector::new("test-connector".to_string()); // Simulate sending an agent request let response_fut = connector.send_agent_request( "session-1".to_string(), json!(0), "session/request_permission".to_string(), json!({"tool": "Write"}), ).await; // Verify request was sent let request = request_rx.recv().await.unwrap(); assert_eq!(request.0, json!(0)); assert_eq!(request.1, "session-1"); assert_eq!(request.2, "session/request_permission"); // Simulate completing the response connector.complete_response(json!(0), json!({"result": "success"})).await.unwrap(); // Verify response was received let response = response_fut.await.unwrap(); assert_eq!(response, json!({"result": "success"})); } #[tokio::test] async fn test_test_context() { let ctx = TestContext::new(); // Create client and connector let (client, _) = ctx.create_client("client-1".to_string()).await; let (connector, _) = ctx.create_connector("connector-1".to_string()).await; // Verify we can retrieve them assert!(ctx.get_client("client-1").await.is_some()); assert!(ctx.get_connector("connector-1").await.is_some()); assert!(ctx.get_client("non-existent").await.is_none()); } #[test] fn test_sample_helpers() { let request = sample_permission_request(0); assert_eq!(request["method"], "session/request_permission"); let response = sample_permission_response(0, true); assert_eq!(response["result"]["selectedOptionId"], "allow"); } #[test] fn test_extract_agent_request() { let event = MockEvent { event_type: "session/update".to_string(), data: json!({ "sessionId": "session-1", "update": { "sessionUpdate": "agent_request", "requestId": 0, "method": "session/request_permission", "params": {"tool": "Write"} } }), }; let (request_id, method, params) = extract_agent_request(&event).unwrap(); assert_eq!(request_id, json!(0)); assert_eq!(method, "session/request_permission"); assert_eq!(params["tool"], "Write"); } }