sync from monorepo @ 2452e92e
This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
//! 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);
|
||||
}
|
||||
Reference in New Issue
Block a user