Files
dirigent/crates/dirigent_acp_api/tests/common/mod.rs
T
2026-05-08 01:59:04 +02:00

339 lines
10 KiB
Rust

//! 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<Mutex<Vec<MockEvent>>>,
/// Sender for simulating HTTP POST responses
pub response_tx: mpsc::UnboundedSender<(Value, oneshot::Sender<Result<()>>)>,
}
impl MockSseClient {
pub fn new(client_id: String) -> (Self, mpsc::UnboundedReceiver<(Value, oneshot::Sender<Result<()>>)>) {
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<MockEvent> {
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<MockEvent> {
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<Mutex<HashMap<Value, oneshot::Sender<Value>>>>,
}
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<Value> {
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<Mutex<HashMap<String, MockSseClient>>>,
/// Mock connectors by connector_id
pub connectors: Arc<Mutex<HashMap<String, MockConnector>>>,
}
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<Result<()>>)>) {
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<MockSseClient> {
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<MockConnector> {
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");
}
}