use std::sync::atomic::{AtomicUsize, Ordering};
use super::*;
use crate::a2a::configured::A2A_RETRY_BACKOFF;
use crate::a2a::configured::testing::{agent_at, free_port, serve_card};
use crate::agents::AgentResolver;
async fn counted(port: u16) -> (ConfiguredAgents, Arc<AtomicUsize>) {
let changes = Arc::new(AtomicUsize::new(0));
let counter = Arc::clone(&changes);
let agents = ConfiguredAgents::connect(vec![agent_at("reviewer", port)]).await;
agents.on_change(Arc::new(move || {
counter.fetch_add(1, Ordering::SeqCst);
}));
(agents, changes)
}
fn backdate(agents: &ConfiguredAgents, name: &str) {
let roster = agents.snapshot();
let agent = roster.get(name).expect("configured");
*agent.attempted.lock().unwrap() = Instant::now() - 2 * agents.backoff;
}
#[tokio::test]
async fn an_agent_that_came_up_is_reached_once_its_backoff_has_passed() {
let port = free_port();
let (agents, changes) = counted(port).await;
assert!(
agents
.snapshot()
.get("reviewer")
.unwrap()
.client()
.is_none()
);
assert!(matches!(agents.standing("reviewer"), Route::Backoff(_)));
assert!(matches!(
agents.reach("reviewer").await,
Err(Unresolved::Backoff(_))
));
serve_card(port);
backdate(&agents, "reviewer");
assert_eq!(
agents.standing("reviewer"),
Route::Known,
"due a retry: the call may go ahead"
);
agents.reach("reviewer").await.expect("it came up");
assert_eq!(agents.standing("reviewer"), Route::Known);
assert_eq!(changes.load(Ordering::SeqCst), 1, "reaching it is a change");
}
#[tokio::test]
async fn a_failed_retry_waits_out_a_whole_backoff_again() {
let (agents, changes) = counted(free_port()).await;
backdate(&agents, "reviewer");
let Err(Unresolved::Unreachable { retry_in, .. }) = agents.reach("reviewer").await else {
panic!("nothing listens, so the retry fails");
};
assert_eq!(retry_in, Some(A2A_RETRY_BACKOFF));
assert!(
matches!(agents.standing("reviewer"), Route::Backoff(_)),
"the failed attempt starts the clock again"
);
assert_eq!(changes.load(Ordering::SeqCst), 0, "nothing changed");
}
#[tokio::test]
async fn two_calls_for_one_down_agent_cost_one_attempt() {
let (agents, _) = counted(free_port()).await;
backdate(&agents, "reviewer");
let (first, second) = tokio::join!(agents.reach("reviewer"), agents.reach("reviewer"));
let attempts = [&first, &second]
.iter()
.filter(|result| matches!(result, Err(Unresolved::Unreachable { .. })))
.count();
assert_eq!(attempts, 1, "{first:?} {second:?}");
}
#[tokio::test]
async fn a_name_nobody_configured_is_unknown() {
let (agents, _) = counted(free_port()).await;
assert_eq!(agents.standing("nobody"), Route::Unknown);
assert!(matches!(
agents.reach("nobody").await,
Err(Unresolved::Unknown)
));
}