use std::time::{Duration, Instant};
use super::*;
use crate::a2a::A2aReload;
fn working() -> Conversation {
Conversation {
context_id: Some("context-1".to_string()),
outstanding: Some(Outstanding {
task_id: "task-1".to_string(),
waiting_for: Waiting::Agent,
endpoint: None,
}),
}
}
#[test]
fn a_remembered_agent_finds_its_outstanding_task_after_a_restart() {
let dir = tempfile::tempdir().unwrap();
let before = RemoteAgents::unreachable_in(&["reviewer"], dir.path());
before
.record("agent-1", "reviewer", working(), true)
.expect("written through");
drop(before);
let after = RemoteAgents::unreachable_in(&["reviewer"], dir.path());
assert_eq!(after.conversation("agent-1", "reviewer", true), working());
assert_eq!(
after.conversation("agent-2", "reviewer", true),
Conversation::default(),
"another session's record is its own"
);
}
#[test]
fn a_child_keeps_its_conversation_in_memory_only() {
let dir = tempfile::tempdir().unwrap();
let agents = RemoteAgents::unreachable_in(&["reviewer"], dir.path());
agents
.record("child-1", "reviewer", working(), false)
.expect("nothing to write");
assert_eq!(agents.conversation("child-1", "reviewer", false), working());
assert!(
std::fs::read_dir(dir.path()).is_err()
|| std::fs::read_dir(dir.path()).unwrap().next().is_none(),
"a delegated child is never resumed, so nothing is written for it"
);
}
#[test]
fn a_runtime_that_keeps_no_history_keeps_no_record() {
let agents = RemoteAgents::unreachable(&["reviewer"]);
agents
.record("agent-1", "reviewer", working(), true)
.expect("nothing to write");
assert_eq!(agents.conversation("agent-1", "reviewer", true), working());
}
#[test]
fn forgetting_a_conversation_removes_its_record() {
let dir = tempfile::tempdir().unwrap();
let agents = RemoteAgents::unreachable_in(&["reviewer"], dir.path());
agents
.record("agent-1", "reviewer", working(), true)
.expect("written through");
forget(dir.path(), "agent-1").expect("removed");
forget(dir.path(), "agent-1").expect("and absent is not an error");
let after = RemoteAgents::unreachable_in(&["reviewer"], dir.path());
assert_eq!(
after.conversation("agent-1", "reviewer", true),
Conversation::default()
);
}
#[test]
fn an_id_that_is_not_a_file_name_is_never_made_into_a_path() {
let dir = tempfile::tempdir().unwrap();
let agents = RemoteAgents::unreachable_in(&["reviewer"], dir.path());
agents
.record("../escape", "reviewer", working(), true)
.expect("kept in memory");
assert!(!dir.path().join("../escape.json").exists());
assert_eq!(
agents.conversation("../escape", "reviewer", true),
working()
);
}
fn down(name: &str, port: u16) -> A2aAgent {
testing::agent_at(name, port)
}
#[tokio::test]
async fn a_reload_reports_the_difference_by_name() {
let agents = RemoteAgents::connect(
vec![down("kept", 1), down("moved", 1), down("gone", 1)],
None,
)
.await;
let before = agents.snapshot();
let report = agents
.reload(vec![down("kept", 1), down("moved", 2), down("new", 1)])
.await;
assert_eq!(report.added, ["new"]);
assert_eq!(report.removed, ["gone"]);
assert_eq!(report.changed, ["moved"]);
assert_eq!(report.refreshed, ["kept"]);
assert_eq!(report.unreachable, ["kept", "moved", "new"]);
assert_eq!(agents.names(), ["kept", "moved", "new"]);
assert_eq!(
before.names(),
["kept", "moved", "gone"],
"a snapshot taken before the reload is the set it was taken from"
);
}
#[tokio::test]
async fn a_reload_keeps_every_conversation_whatever_it_did_to_the_agent() {
let agents = RemoteAgents::connect(vec![down("reviewer", 1)], None).await;
agents
.record("agent-1", "reviewer", working(), false)
.expect("in memory");
agents.reload(Vec::new()).await;
agents.reload(vec![down("reviewer", 2)]).await;
assert_eq!(agents.conversation("agent-1", "reviewer", false), working());
}
#[test]
fn a_reload_report_names_no_value_it_read() {
let report = A2aReload {
added: vec!["nous".to_string()],
..A2aReload::default()
};
assert!(report.changed_configuration());
assert!(!A2aReload::default().changed_configuration());
assert_eq!(
format!("{report:?}"),
"A2aReload { added: [\"nous\"], removed: [], changed: [], refreshed: [], unreachable: [] }"
);
}
#[tokio::test]
async fn a_client_a_retry_installs_mid_reload_survives_the_reload() {
let port = testing::free_port();
let stalling = testing::serve_card_stalling_first(port);
let agents = Arc::new(RemoteAgents::with_roster(
Roster {
agents: vec![Arc::new(RemoteAgent::new(down("reviewer", port), None))],
removed: BTreeSet::new(),
},
None,
));
let reloading = tokio::spawn({
let agents = Arc::clone(&agents);
async move { agents.reload(vec![down("reviewer", port)]).await }
});
let first_arrived = stalling.first_arrived;
tokio::task::spawn_blocking(move || first_arrived.recv_timeout(Duration::from_secs(5)))
.await
.unwrap()
.expect("the reload's fetch reached the agent");
*agents.snapshot().agents[0].attempted.lock().unwrap() = Instant::now() - 2 * A2A_RETRY_BACKOFF;
agents
.reach("reviewer")
.await
.expect("the retry reaches it");
let _ = stalling.release.send(());
let report = reloading.await.unwrap();
assert_eq!(
report.unreachable,
["reviewer"],
"the reload's own fetch failed"
);
assert!(
agents
.snapshot()
.get("reviewer")
.unwrap()
.client()
.is_some(),
"and the retry's client stands"
);
}
#[tokio::test]
async fn a_failed_fetch_never_keeps_a_connection_made_with_another_configuration() {
let port = testing::free_port();
testing::serve_card(port);
let agents = RemoteAgents::connect(vec![down("reviewer", port)], None).await;
assert!(
agents
.snapshot()
.get("reviewer")
.unwrap()
.client()
.is_some()
);
agents
.reload(vec![down("reviewer", testing::free_port())])
.await;
assert!(
agents
.snapshot()
.get("reviewer")
.unwrap()
.client()
.is_none(),
"a moved agent is not reached at its old address"
);
}
#[tokio::test]
async fn an_exchange_in_flight_keeps_its_client_across_a_reload() {
let (old, new) = (testing::free_port(), testing::free_port());
testing::serve_card(old);
testing::serve_card(new);
let agents = RemoteAgents::connect(vec![down("reviewer", old)], None).await;
let in_flight = agents.reach("reviewer").await.expect("connected");
agents.reload(vec![down("reviewer", new)]).await;
agents.reload(Vec::new()).await;
agents.reload(vec![down("reviewer", new)]).await;
assert_eq!(
in_flight.client.card_url().port(),
Some(old),
"still the client it started with, alive and unchanged"
);
let next = agents.reach("reviewer").await.expect("connected");
assert_eq!(next.client.card_url().port(), Some(new));
assert!(!Arc::ptr_eq(&in_flight.client, &next.client));
assert_ne!(in_flight.endpoint, next.endpoint);
}