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

230 lines
6.8 KiB
Rust

//! Integration test for timeout handling (T049)
//!
//! This test verifies that the system handles timeouts gracefully when a client
//! fails to respond to an agent request within the timeout period.
//!
//! Test scenario:
//! 1. Register a pending agent request
//! 2. Wait for timeout (using reduced timeout for testing)
//! 3. Verify timeout occurs
//! 4. Verify cleanup happens correctly
//! 5. Verify no resource leaks
use dirigent_acp_api::agent_requests::AgentRequestTracker;
use serde_json::json;
use tokio::time::{timeout, Duration};
#[tokio::test]
async fn test_timeout_basic() {
// Create tracker
let tracker = AgentRequestTracker::new();
let client_id = "test-client";
let request_id = json!(0);
// Register request
let receiver = tracker.register(client_id, request_id.clone());
assert_eq!(tracker.pending_count(), 1);
// Wait for timeout (use short timeout for testing)
let result = timeout(Duration::from_millis(100), receiver).await;
// Should timeout
assert!(result.is_err(), "Expected timeout but request completed");
// Manually trigger cleanup (in production, event bridge does this)
tracker.timeout(client_id, request_id);
// Verify cleanup
assert_eq!(tracker.pending_count(), 0);
}
#[tokio::test]
async fn test_timeout_cleanup() {
let tracker = AgentRequestTracker::new();
let client_id = "test-client";
let request_id = json!(123);
// Register request
let receiver = tracker.register(client_id, request_id.clone());
assert_eq!(tracker.pending_count(), 1);
// Trigger timeout before waiting
tracker.timeout(client_id, request_id);
// Verify cleanup happened
assert_eq!(tracker.pending_count(), 0);
// Receiver should get error (channel closed)
let result = receiver.await;
assert!(result.is_err(), "Expected receiver to get error after timeout");
}
#[tokio::test]
async fn test_timeout_multiple_clients() {
let tracker = AgentRequestTracker::new();
let client1 = "client-1";
let client2 = "client-2";
// Register requests for both clients
let rx1 = tracker.register(client1, json!(0));
let rx2 = tracker.register(client2, json!(0));
assert_eq!(tracker.pending_count(), 2);
assert_eq!(tracker.client_pending_count(client1), 1);
assert_eq!(tracker.client_pending_count(client2), 1);
// Timeout only client1's request
tracker.timeout(client1, json!(0));
// Verify only client1's request is removed
assert_eq!(tracker.pending_count(), 1);
assert_eq!(tracker.client_pending_count(client1), 0);
assert_eq!(tracker.client_pending_count(client2), 1);
// Client1's receiver should error
assert!(rx1.await.is_err());
// Complete client2's request normally
let response = json!({"result": "success"});
let result = tracker.complete(client2, json!(0), response);
assert!(result.is_ok());
// Client2's receiver should get response
let received = rx2.await.unwrap();
assert_eq!(received, json!({"result": "success"}));
// All cleaned up
assert_eq!(tracker.pending_count(), 0);
}
#[tokio::test]
async fn test_timeout_no_double_cleanup() {
let tracker = AgentRequestTracker::new();
let client_id = "test-client";
let request_id = json!(0);
// Register request
let _receiver = tracker.register(client_id, request_id.clone());
assert_eq!(tracker.pending_count(), 1);
// First timeout - should remove
tracker.timeout(client_id, request_id.clone());
assert_eq!(tracker.pending_count(), 0);
// Second timeout - should be no-op (not panic)
tracker.timeout(client_id, request_id);
assert_eq!(tracker.pending_count(), 0);
}
#[tokio::test]
async fn test_timeout_race_with_complete() {
let tracker = AgentRequestTracker::new();
let client_id = "test-client";
let request_id = json!(0);
// Register request
let receiver = tracker.register(client_id, request_id.clone());
assert_eq!(tracker.pending_count(), 1);
// Complete the request
let response = json!({"result": "success"});
let result = tracker.complete(client_id, request_id.clone(), response.clone());
assert!(result.is_ok());
assert_eq!(tracker.pending_count(), 0);
// Try to timeout after completion - should be no-op
tracker.timeout(client_id, request_id);
assert_eq!(tracker.pending_count(), 0);
// Receiver should still get the response
let received = receiver.await.unwrap();
assert_eq!(received, response);
}
#[tokio::test]
async fn test_concurrent_timeouts() {
let tracker = AgentRequestTracker::new();
let client_id = "test-client";
// Register 10 requests
let mut receivers = Vec::new();
for i in 0..10 {
let rx = tracker.register(client_id, json!(i));
receivers.push((i, rx));
}
assert_eq!(tracker.pending_count(), 10);
// Spawn tasks to timeout each request after random delays
let tracker_clone = tracker.clone();
let timeout_handles: Vec<_> = (0..10)
.map(|i| {
let tracker = tracker_clone.clone();
tokio::spawn(async move {
// Small random-ish delay based on index
tokio::time::sleep(Duration::from_millis((i * 10) as u64)).await;
tracker.timeout(client_id, json!(i));
})
})
.collect();
// Wait for all timeouts to complete
for handle in timeout_handles {
handle.await.unwrap();
}
// All should be cleaned up
assert_eq!(tracker.pending_count(), 0);
// All receivers should get errors
for (_i, rx) in receivers {
assert!(rx.await.is_err());
}
}
#[tokio::test]
async fn test_timeout_with_actual_delay() {
// This test uses actual time delays to verify timeout behavior more realistically
let tracker = AgentRequestTracker::new();
let client_id = "test-client";
let request_id = json!(0);
let start = std::time::Instant::now();
// Register request
let receiver = tracker.register(client_id, request_id.clone());
// Spawn task to timeout after 200ms
let tracker_clone = tracker.clone();
tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(200)).await;
tracker_clone.timeout(client_id, json!(0));
});
// Wait on receiver with longer timeout
let result = timeout(Duration::from_secs(1), receiver).await;
let elapsed = start.elapsed();
// Should complete due to timeout() call, not tokio::time::timeout
assert!(result.is_ok(), "Should complete when timeout() is called");
assert!(result.unwrap().is_err(), "Receiver should get error");
// Should take approximately 200ms
assert!(
elapsed >= Duration::from_millis(180) && elapsed < Duration::from_millis(300),
"Expected ~200ms but took {:?}",
elapsed
);
// Should be cleaned up
assert_eq!(tracker.pending_count(), 0);
}