diff --git a/swarm-controller/Cargo.toml b/swarm-controller/Cargo.toml
index e0ad9742..d5929e7b 100644
--- a/swarm-controller/Cargo.toml
+++ b/swarm-controller/Cargo.toml
@@ -120,5 +120,10 @@ utoipa-axum.workspace = true
# `agent_renewal.rs` reads an agent certificate's validity window.
x509-cert.workspace = true
+# `test-util` for `#[tokio::test(start_paused = true)]`: `agent_renewal`'s
+# tests wait out its five-minute bound on paused time.
+[dev-dependencies]
+tokio = { workspace = true, features = ["test-util"] }
+
[lints]
workspace = true
diff --git a/swarm-controller/src/agent_renewal.rs b/swarm-controller/src/agent_renewal.rs
index f315322d..6097fc98 100644
--- a/swarm-controller/src/agent_renewal.rs
+++ b/swarm-controller/src/agent_renewal.rs
@@ -15,13 +15,13 @@
//! an open connection is unaffected, but a reconnect before that restart
//! presents the old secret and is denied.
//!
-//! [`spawn`]'s pass, at start and every [`RECONCILE_INTERVAL`], inserts a job
-//! per agent with anything due: `MintAgentIdentity` for the certificate
-//! ([`crate::agent_identity::mint_and_verify`], which keeps the queue secret as
-//! it is) and `RenewAgentQueueCredential` ([`renew`]) for the secret, the
-//! second after the first when both are due. The decisions are pure
-//! ([`live_agents`], [`cert_is_due`], [`secret_step`], [`plan`]) so the tests
-//! pin them; the IO on either side only reads or acts.
+//! [`spawn`]'s pass, once the queue is up and every [`RECONCILE_INTERVAL`],
+//! inserts a job per agent with anything due: `MintAgentIdentity` for the
+//! certificate ([`crate::agent_identity::mint_and_verify`], which keeps the
+//! queue secret as it is) and `RenewAgentQueueCredential` ([`renew`]) for the
+//! secret, the second after the first when both are due. The decisions are
+//! pure ([`live_agents`], [`cert_is_due`], [`secret_step`], [`plan`]) so the
+//! tests pin them; the IO on either side only reads or acts.
//!
//! **Only agents some hive is declared to run are renewed** ([`live_agents`]):
//! the wanted-state declarations are where a destroy is recorded, and an agent
@@ -44,9 +44,13 @@ use crate::wanted::WantedWriter;
/// cadence.
pub const SECRET_RENEW_AFTER: std::time::Duration = std::time::Duration::from_hours(45 * 24);
-/// How often [`spawn`] re-checks every agent.
+/// How often [`spawn`] re-checks every agent. Also the longest it waits for
+/// the queue connection before its first pass.
const RECONCILE_INTERVAL: std::time::Duration = std::time::Duration::from_mins(5);
+/// How often [`spawn`] checks the queue connection before its first pass.
+const CONNECT_POLL: std::time::Duration = std::time::Duration::from_secs(5);
+
/// Now, in the unix seconds [`queue::AgentCredential::minted_at`] holds.
pub fn unix_now() -> i64 {
std::time::SystemTime::now()
@@ -344,44 +348,81 @@ async fn observe_all(
Ok(observed)
}
-/// Check every live agent now and every [`RECONCILE_INTERVAL`] after, and hand
-/// the renewals due to `enqueue`, which inserts a job for each.
+/// Wait until `connected` holds, checking every [`CONNECT_POLL`] for at most
+/// [`RECONCILE_INTERVAL`]. Returns whether it held.
+async fn wait_for_queue(connected: impl Fn() -> bool) -> bool {
+ let deadline = tokio::time::Instant::now() + RECONCILE_INTERVAL;
+ loop {
+ if connected() {
+ return true;
+ }
+ if tokio::time::Instant::now() >= deadline {
+ return false;
+ }
+ tokio::time::sleep(CONNECT_POLL).await;
+ }
+}
+
+/// [`spawn`]'s loop, with the connection check and the pass handed in.
+async fn run