use std::collections::VecDeque;
use std::fmt;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
use axon_frontend::ir_nodes::{IRUpstream, IRUpstreamMapRule};
use futures::{SinkExt, StreamExt};
use tokio::net::TcpStream;
use tokio::sync::{mpsc, Mutex, Notify, OwnedSemaphorePermit, Semaphore};
use tokio_tungstenite::tungstenite::client::IntoClientRequest;
use tokio_tungstenite::tungstenite::http::HeaderValue;
use tokio_tungstenite::tungstenite::Message;
use tokio_tungstenite::{connect_async, MaybeTlsStream, WebSocketStream};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum UpstreamError {
MissingConfig { upstream: String, key: String },
MissingSecret { upstream: String, key: String },
UnwitnessedLifecycle { upstream: String, detail: String },
Dial { upstream: String, detail: String },
BadUrl { upstream: String, detail: String },
UnmappedOutbound { upstream: String, message: String },
Overflow { upstream: String },
Exhausted { upstream: String, attempts: u32 },
Closed { upstream: String },
LeaseBreach { upstream: String, detail: String },
}
impl fmt::Display for UpstreamError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
UpstreamError::MissingConfig { upstream, key } => {
write!(f, "upstream '{upstream}': no value for resolve key '{key}' (set the per-tenant config or the AXON_* env fallback)")
}
UpstreamError::MissingSecret { upstream, key } => {
write!(f, "upstream '{upstream}': no value for secret key '{key}'")
}
UpstreamError::UnwitnessedLifecycle { upstream, detail } => {
write!(f, "upstream '{upstream}': lifecycle witness refused — refusing to dial (fail-closed): {detail}")
}
UpstreamError::Dial { upstream, detail } => write!(f, "upstream '{upstream}': dial failed: {detail}"),
UpstreamError::BadUrl { upstream, detail } => write!(f, "upstream '{upstream}': bad resolved URL: {detail}"),
UpstreamError::UnmappedOutbound { upstream, message } => {
write!(f, "upstream '{upstream}': message '{message}' has no `send` projection rule")
}
UpstreamError::Overflow { upstream } => {
write!(f, "upstream '{upstream}': outbound queue full and overflow policy is `fail`")
}
UpstreamError::Exhausted { upstream, attempts } => {
write!(f, "upstream '{upstream}': reconnect budget exhausted after {attempts} attempts (on_exhausted: fail)")
}
UpstreamError::Closed { upstream } => write!(f, "upstream '{upstream}': connection closed"),
UpstreamError::LeaseBreach { upstream, detail } => {
write!(f, "upstream '{upstream}': {detail}")
}
}
}
}
impl std::error::Error for UpstreamError {}
pub trait UpstreamConfigResolver: Send + Sync {
fn resolve(&self, key: &str) -> Option<String>;
fn reveal_secret(&self, key: &str) -> Option<String>;
}
pub struct EnvConfigResolver;
pub fn env_var_for_key(key: &str) -> String {
let mut out = String::with_capacity(key.len() + 5);
out.push_str("AXON_");
for c in key.chars() {
match c {
'.' | '-' => out.push('_'),
c => out.push(c.to_ascii_uppercase()),
}
}
out
}
impl UpstreamConfigResolver for EnvConfigResolver {
fn resolve(&self, key: &str) -> Option<String> {
std::env::var(env_var_for_key(key)).ok().filter(|v| !v.is_empty())
}
fn reveal_secret(&self, key: &str) -> Option<String> {
std::env::var(env_var_for_key(key)).ok().filter(|v| !v.is_empty())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum UpstreamLifecycle {
Connected { attempt: u32 },
Reconnected { attempt: u32 },
Exhausted { attempts: u32 },
}
pub type WitnessFuture<'a> = std::pin::Pin<Box<dyn std::future::Future<Output = Result<(), String>> + Send + 'a>>;
pub trait UpstreamLifecycleWitness: Send + Sync {
fn witness<'a>(&'a self, upstream: &'a str, event: &'a UpstreamLifecycle) -> WitnessFuture<'a>;
}
pub struct TracingLifecycleWitness;
impl UpstreamLifecycleWitness for TracingLifecycleWitness {
fn witness<'a>(&'a self, upstream: &'a str, event: &'a UpstreamLifecycle) -> WitnessFuture<'a> {
Box::pin(async move {
tracing::info!(upstream, ?event, "upstream lifecycle");
Ok(())
})
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum OutboundPayload {
Json(serde_json::Value),
Bytes(Vec<u8>),
}
#[derive(Debug, Clone, PartialEq)]
pub enum InboundPayload {
Json(serde_json::Value),
Bytes(Vec<u8>),
}
#[derive(Debug, Clone, PartialEq)]
pub enum UpstreamEvent {
Message { message: String, payload: InboundPayload },
Unmapped { detail: String },
Reconnected { attempt: u32 },
Exhausted { attempts: u32 },
}
pub fn project_outbound(rule: &IRUpstreamMapRule, payload: &OutboundPayload) -> Message {
match rule.framing.as_str() {
"binary" => match payload {
OutboundPayload::Bytes(b) => Message::Binary(b.clone().into()),
OutboundPayload::Json(v) => Message::Binary(v.to_string().into_bytes().into()),
},
_ => {
let body = match payload {
OutboundPayload::Json(v) => v.clone(),
OutboundPayload::Bytes(b) => serde_json::Value::String(String::from_utf8_lossy(b).into_owned()),
};
let out = match &rule.tag {
None => body,
Some(tag) => match body {
serde_json::Value::Object(mut m) => {
m.insert("type".to_string(), serde_json::Value::String(tag.clone()));
serde_json::Value::Object(m)
}
other => serde_json::json!({ "type": tag, "payload": other }),
},
};
Message::Text(out.to_string().into())
}
}
}
pub fn classify_inbound(rules: &[IRUpstreamMapRule], frame: &Message) -> Option<(String, InboundPayload)> {
match frame {
Message::Binary(b) => rules
.iter()
.find(|r| r.direction == "receive" && r.framing == "binary")
.map(|r| (r.message.clone(), InboundPayload::Bytes(b.to_vec()))),
Message::Text(t) => {
let body: serde_json::Value = serde_json::from_str(t).ok()?;
let json_rules = || rules.iter().filter(|r| r.direction == "receive" && r.framing == "json");
for r in json_rules() {
let (field, expected) = match (&r.when_field, &r.when_value) {
(None, _) => ("type", r.message.as_str()),
(Some(f), Some(v)) => (f.as_str(), v.as_str()),
(Some(_), None) => continue, };
if body.get(field).and_then(|v| v.as_str()) == Some(expected) {
return Some((r.message.clone(), InboundPayload::Json(body)));
}
}
for r in json_rules() {
if let (Some(f), None) = (&r.when_field, &r.when_value) {
if body.get(f).is_some() {
return Some((r.message.clone(), InboundPayload::Json(body)));
}
}
}
None
}
_ => None, }
}
pub fn build_dial_request(
upstream: &str,
url: &str,
auth_kind: &str,
auth_name: Option<&str>,
auth_prefix: Option<&str>,
secret: &str,
) -> Result<tokio_tungstenite::tungstenite::handshake::client::Request, UpstreamError> {
let final_url = if auth_kind == "query" {
let param = auth_name.unwrap_or("token");
let sep = if url.contains('?') { '&' } else { '?' };
format!("{url}{sep}{param}={secret}")
} else {
url.to_string()
};
let mut req = final_url.into_client_request().map_err(|e| UpstreamError::BadUrl {
upstream: upstream.to_string(),
detail: e.to_string(),
})?;
if auth_kind == "header" {
let name = auth_name.unwrap_or("Authorization");
let value = format!("{}{}", auth_prefix.unwrap_or(""), secret);
let header_name: tokio_tungstenite::tungstenite::http::header::HeaderName =
name.parse().map_err(|_| UpstreamError::BadUrl {
upstream: upstream.to_string(),
detail: format!("invalid auth header name '{name}'"),
})?;
req.headers_mut().insert(
header_name,
HeaderValue::from_str(&value).map_err(|_| UpstreamError::BadUrl {
upstream: upstream.to_string(),
detail: "auth secret is not a valid header value".to_string(),
})?,
);
}
Ok(req)
}
pub fn backoff_delay(backoff_ms: i64, attempt: u32) -> Duration {
let base = (backoff_ms.max(1) as u64).saturating_mul(1u64 << attempt.saturating_sub(1).min(20));
let capped = base.min(30_000);
let mut x = attempt as u64 ^ 0x9E37_79B9_7F4A_7C15;
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
let jitter_pct = (x % 51) as i64 - 25; let jittered = capped as i64 + (capped as i64 * jitter_pct) / 100;
Duration::from_millis(jittered.max(1) as u64)
}
struct OverflowQueue {
inner: Mutex<VecDeque<Message>>,
notify: Notify,
capacity: usize,
policy: String,
closed: AtomicBool,
}
impl OverflowQueue {
fn new(capacity: usize, policy: String) -> Self {
OverflowQueue {
inner: Mutex::new(VecDeque::with_capacity(capacity)),
notify: Notify::new(),
capacity,
policy,
closed: AtomicBool::new(false),
}
}
async fn push(&self, upstream: &str, msg: Message) -> Result<usize, UpstreamError> {
loop {
if self.closed.load(Ordering::Acquire) {
return Err(UpstreamError::Closed { upstream: upstream.to_string() });
}
let mut q = self.inner.lock().await;
if q.len() < self.capacity {
q.push_back(msg);
drop(q);
self.notify.notify_waiters();
return Ok(0);
}
match self.policy.as_str() {
"drop_oldest" => {
let mut shed = 0usize;
while q.len() >= self.capacity {
q.pop_front();
shed += 1;
}
q.push_back(msg);
drop(q);
self.notify.notify_waiters();
return Ok(shed);
}
"pause_upstream" => {
drop(q);
self.notify.notified().await;
}
_ => return Err(UpstreamError::Overflow { upstream: upstream.to_string() }),
}
}
}
async fn pop(&self) -> Option<Message> {
loop {
{
let mut q = self.inner.lock().await;
if let Some(m) = q.pop_front() {
drop(q);
self.notify.notify_waiters();
return Some(m);
}
}
if self.closed.load(Ordering::Acquire) {
return None;
}
self.notify.notified().await;
}
}
fn close(&self) {
self.closed.store(true, Ordering::Release);
self.notify.notify_waiters();
}
}
const DEFAULT_QUEUE_CAPACITY: usize = 64;
pub struct UpstreamHandle {
name: String,
rules: Arc<Vec<IRUpstreamMapRule>>,
queue: Arc<OverflowQueue>,
events: mpsc::Receiver<UpstreamEvent>,
driver: tokio::task::JoinHandle<()>,
_instance_permit: Option<OwnedSemaphorePermit>,
}
impl fmt::Debug for UpstreamHandle {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("UpstreamHandle")
.field("name", &self.name)
.field("rules", &self.rules.len())
.finish_non_exhaustive()
}
}
impl UpstreamHandle {
pub async fn send(&self, message: &str, payload: OutboundPayload) -> Result<(), UpstreamError> {
let rule = self
.rules
.iter()
.find(|r| r.direction == "send" && r.message == message)
.ok_or_else(|| UpstreamError::UnmappedOutbound {
upstream: self.name.clone(),
message: message.to_string(),
})?;
let frame = project_outbound(rule, &payload);
let shed = self.queue.push(&self.name, frame).await?;
if shed > 0 {
tracing::warn!(upstream = %self.name, shed, "overflow drop_oldest shed outbound frames");
}
Ok(())
}
pub async fn recv(&mut self) -> Option<UpstreamEvent> {
self.events.recv().await
}
pub fn close(&self) {
self.queue.close();
self.driver.abort();
}
}
type WsStream = WebSocketStream<MaybeTlsStream<TcpStream>>;
static INSTANCE_BOUNDS: std::sync::OnceLock<
std::sync::Mutex<std::collections::HashMap<String, (i64, Arc<Semaphore>)>>,
> = std::sync::OnceLock::new();
fn instance_semaphore(upstream: &str, capacity: i64) -> Arc<Semaphore> {
let map = INSTANCE_BOUNDS.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::new()));
let mut map = map.lock().unwrap_or_else(|p| p.into_inner());
match map.get(upstream) {
Some((cap, sem)) if *cap == capacity => Arc::clone(sem),
_ => {
let sem = Arc::new(Semaphore::new(usize::try_from(capacity).unwrap_or(0).max(0)));
map.insert(upstream.to_string(), (capacity, Arc::clone(&sem)));
sem
}
}
}
pub async fn dial_upstream(
spec: &IRUpstream,
resolver: &dyn UpstreamConfigResolver,
witness: Arc<dyn UpstreamLifecycleWitness>,
lease: Option<&crate::resource_lease::ResourceLeaseGuard>,
) -> Result<UpstreamHandle, UpstreamError> {
let name = spec.name.clone();
if let Some(guard) = lease {
if !spec.resource_ref.is_empty() {
if let Err(breach) = guard.charge(&spec.resource_ref) {
return Err(UpstreamError::LeaseBreach {
upstream: name.clone(),
detail: breach.to_string(),
});
}
}
}
let url = resolver.resolve(&spec.resolve).ok_or_else(|| UpstreamError::MissingConfig {
upstream: name.clone(),
key: spec.resolve.clone(),
})?;
let secret = if spec.auth_kind == "signed_url" {
String::new() } else {
resolver.reveal_secret(&spec.secret).ok_or_else(|| UpstreamError::MissingSecret {
upstream: name.clone(),
key: spec.secret.clone(),
})?
};
let instance_permit = match spec.capacity {
Some(n) if !spec.resource_ref.is_empty() && n > 0 => Some(
instance_semaphore(&name, n)
.acquire_owned()
.await
.map_err(|_| UpstreamError::Closed { upstream: name.clone() })?,
),
_ => None,
};
witness
.witness(&name, &UpstreamLifecycle::Connected { attempt: 0 })
.await
.map_err(|detail| UpstreamError::UnwitnessedLifecycle { upstream: name.clone(), detail })?;
let request = build_dial_request(
&name,
&url,
&spec.auth_kind,
spec.auth_name.as_deref(),
spec.auth_prefix.as_deref(),
&secret,
)?;
let (ws, _resp) = connect_async(request).await.map_err(|e| UpstreamError::Dial {
upstream: name.clone(),
detail: e.to_string(),
})?;
let rules = Arc::new(spec.map.clone());
let capacity = spec
.backpressure_credit
.and_then(|n| usize::try_from(n).ok())
.filter(|n| *n > 0)
.unwrap_or(DEFAULT_QUEUE_CAPACITY);
let policy = spec.overflow.clone().unwrap_or_else(|| "fail".to_string());
let queue = Arc::new(OverflowQueue::new(capacity, policy));
let (event_tx, event_rx) = mpsc::channel::<UpstreamEvent>(capacity.max(16));
let driver = tokio::spawn(drive_upstream(
name.clone(),
ws,
Arc::clone(&rules),
Arc::clone(&queue),
event_tx,
DialParams {
url,
secret,
auth_kind: spec.auth_kind.clone(),
auth_name: spec.auth_name.clone(),
auth_prefix: spec.auth_prefix.clone(),
backoff_ms: spec.reconnect.as_ref().map(|r| r.backoff_ms).unwrap_or(500),
max_attempts: spec.reconnect.as_ref().map(|r| r.max_attempts).unwrap_or(0),
},
witness,
));
Ok(UpstreamHandle { name, rules, queue, events: event_rx, driver, _instance_permit: instance_permit })
}
struct DialParams {
url: String,
secret: String,
auth_kind: String,
auth_name: Option<String>,
auth_prefix: Option<String>,
backoff_ms: i64,
max_attempts: i64,
}
async fn drive_upstream(
name: String,
mut ws: WsStream,
rules: Arc<Vec<IRUpstreamMapRule>>,
queue: Arc<OverflowQueue>,
events: mpsc::Sender<UpstreamEvent>,
params: DialParams,
witness: Arc<dyn UpstreamLifecycleWitness>,
) {
loop {
let dropped = pump_connection(&name, &mut ws, &rules, &queue, &events).await;
if !dropped {
return;
}
let mut attempt: u32 = 0;
let reconnected = loop {
if attempt as i64 >= params.max_attempts {
let ev = UpstreamLifecycle::Exhausted { attempts: attempt };
if let Err(e) = witness.witness(&name, &ev).await {
tracing::error!(upstream = %name, error = %e, "exhaustion could not be witnessed");
}
let _ = events.send(UpstreamEvent::Exhausted { attempts: attempt }).await;
queue.close();
return;
}
attempt += 1;
tokio::time::sleep(backoff_delay(params.backoff_ms, attempt)).await;
let ev = UpstreamLifecycle::Reconnected { attempt };
if let Err(detail) = witness.witness(&name, &ev).await {
tracing::error!(upstream = %name, %detail, "reconnect refused by witness (fail-closed)");
continue;
}
let request = match build_dial_request(
&name,
¶ms.url,
¶ms.auth_kind,
params.auth_name.as_deref(),
params.auth_prefix.as_deref(),
¶ms.secret,
) {
Ok(r) => r,
Err(e) => {
tracing::error!(upstream = %name, error = %e, "re-dial request build failed");
continue;
}
};
match connect_async(request).await {
Ok((new_ws, _)) => break Some((new_ws, attempt)),
Err(e) => {
tracing::warn!(upstream = %name, attempt, error = %e, "re-dial failed");
continue;
}
}
};
match reconnected {
Some((new_ws, attempt)) => {
ws = new_ws;
let _ = events.send(UpstreamEvent::Reconnected { attempt }).await;
}
None => return,
}
}
}
async fn pump_connection(
name: &str,
ws: &mut WsStream,
rules: &Arc<Vec<IRUpstreamMapRule>>,
queue: &Arc<OverflowQueue>,
events: &mpsc::Sender<UpstreamEvent>,
) -> bool {
loop {
tokio::select! {
outbound = queue.pop() => {
match outbound {
Some(frame) => {
if let Err(e) = ws.send(frame).await {
tracing::warn!(upstream = %name, error = %e, "outbound send failed — wire dropped");
return true;
}
}
None => {
let _ = ws.close(None).await;
return false;
}
}
}
inbound = ws.next() => {
match inbound {
Some(Ok(frame @ (Message::Text(_) | Message::Binary(_)))) => {
match classify_inbound(rules, &frame) {
Some((message, payload)) => {
let _ = events.send(UpstreamEvent::Message { message, payload }).await;
}
None => {
let detail = match &frame {
Message::Text(t) => format!("unclassifiable text frame: {}", &t[..t.len().min(200)]),
_ => "unclassifiable binary frame (no `receive … as binary` rule)".to_string(),
};
let _ = events.send(UpstreamEvent::Unmapped { detail }).await;
}
}
}
Some(Ok(Message::Close(_))) | None => return true,
Some(Ok(_)) => { }
Some(Err(e)) => {
tracing::warn!(upstream = %name, error = %e, "inbound stream error — wire dropped");
return true;
}
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn rule(direction: &str, message: &str, framing: &str) -> IRUpstreamMapRule {
IRUpstreamMapRule {
node_type: "upstream_map_rule",
direction: direction.into(),
message: message.into(),
framing: framing.into(),
tag: None,
when_field: None,
when_value: None,
}
}
#[test]
fn env_var_mapping_mirrors_the_tool_convention() {
assert_eq!(env_var_for_key("upstream.deepgram.url"), "AXON_UPSTREAM_DEEPGRAM_URL");
assert_eq!(env_var_for_key("upstream.eleven-labs.api_key"), "AXON_UPSTREAM_ELEVEN_LABS_API_KEY");
}
#[test]
fn outbound_binary_is_raw_passthrough() {
let r = rule("send", "AudioChunk", "binary");
let m = project_outbound(&r, &OutboundPayload::Bytes(vec![1, 2, 3]));
assert_eq!(m, Message::Binary(vec![1, 2, 3].into()));
}
#[test]
fn outbound_json_without_tag_is_verbatim() {
let r = rule("send", "TextChunk", "json");
let m = project_outbound(&r, &OutboundPayload::Json(serde_json::json!({"text": "hola "})));
let Message::Text(t) = m else { panic!("expected text frame") };
assert_eq!(t, r#"{"text":"hola "}"#, "no envelope, no injected keys");
}
#[test]
fn outbound_json_with_tag_injects_type_into_the_object() {
let mut r = rule("send", "Configure", "json");
r.tag = Some("Settings".into());
let m = project_outbound(&r, &OutboundPayload::Json(serde_json::json!({"model": "nova-3"})));
let Message::Text(t) = m else { panic!("expected text frame") };
let v: serde_json::Value = serde_json::from_str(&t).unwrap();
assert_eq!(v["type"], "Settings", "tag injected at top level");
assert_eq!(v["model"], "nova-3", "payload keys merged, not nested");
let m2 = project_outbound(&r, &OutboundPayload::Json(serde_json::json!("scalar")));
let Message::Text(t2) = m2 else { panic!() };
let v2: serde_json::Value = serde_json::from_str(&t2).unwrap();
assert_eq!((v2["type"].as_str(), v2["payload"].as_str()), (Some("Settings"), Some("scalar")));
}
#[test]
fn inbound_presence_discriminator_classifies_after_equality() {
let mut server_content = rule("receive", "ServerContent", "json");
server_content.when_field = Some("serverContent".into());
server_content.when_value = None; let mut setup_done = rule("receive", "SetupComplete", "json");
setup_done.when_field = Some("setupComplete".into());
setup_done.when_value = None; let rules = vec![server_content, setup_done];
let frame = Message::Text(r#"{"serverContent":{"modelTurn":{}}}"#.into());
assert_eq!(classify_inbound(&rules, &frame).unwrap().0, "ServerContent");
let frame2 = Message::Text(r#"{"setupComplete":{}}"#.into());
assert_eq!(classify_inbound(&rules, &frame2).unwrap().0, "SetupComplete");
}
#[test]
fn inbound_json_classifies_on_discriminator_with_default() {
let mut results = rule("receive", "Transcript", "json");
results.when_field = Some("type".into());
results.when_value = Some("Results".into());
let default_rule = rule("receive", "SpeechStarted", "json"); let rules = vec![results, default_rule];
let frame = Message::Text(r#"{"type":"Results","channel":{"alternatives":[{"transcript":"hola"}]}}"#.into());
let (msg, payload) = classify_inbound(&rules, &frame).expect("classified");
assert_eq!(msg, "Transcript");
let InboundPayload::Json(v) = payload else { panic!("json payload") };
assert_eq!(v["channel"]["alternatives"][0]["transcript"], "hola", "whole body is the Json payload");
let frame2 = Message::Text(r#"{"type":"SpeechStarted"}"#.into());
assert_eq!(classify_inbound(&rules, &frame2).unwrap().0, "SpeechStarted", "default discriminator");
let unknown = Message::Text(r#"{"type":"Metadata"}"#.into());
assert!(classify_inbound(&rules, &unknown).is_none(), "unmatched frame surfaces as Unmapped upstream");
}
#[test]
fn inbound_binary_needs_the_binary_rule() {
let rules = vec![rule("receive", "AudioOut", "binary")];
let (msg, payload) = classify_inbound(&rules, &Message::Binary(vec![9].into())).unwrap();
assert_eq!(msg, "AudioOut");
assert_eq!(payload, InboundPayload::Bytes(vec![9]));
assert!(classify_inbound(&[], &Message::Binary(vec![9].into())).is_none());
}
#[test]
fn dial_request_header_auth_carries_prefix() {
let req = build_dial_request("U", "ws://x.test/v1", "header", Some("Authorization"), Some("Token "), "s3cr3t").unwrap();
assert_eq!(req.headers().get("Authorization").unwrap(), "Token s3cr3t");
}
#[test]
fn dial_request_query_auth_appends_param() {
let req = build_dial_request("U", "ws://x.test/v1?model=nova", "query", Some("token"), None, "k").unwrap();
assert_eq!(req.uri().query(), Some("model=nova&token=k"));
let req2 = build_dial_request("U", "ws://x.test/v1", "query", Some("key"), None, "k").unwrap();
assert_eq!(req2.uri().query(), Some("key=k"));
}
#[test]
fn dial_request_signed_url_dials_as_is() {
let req = build_dial_request("U", "ws://x.test/v1?sig=abc", "signed_url", None, None, "").unwrap();
assert_eq!(req.uri().query(), Some("sig=abc"));
assert!(req.headers().get("Authorization").is_none());
}
#[test]
fn backoff_doubles_capped_and_jittered_deterministically() {
let d1 = backoff_delay(500, 1);
let d2 = backoff_delay(500, 2);
let d3 = backoff_delay(500, 3);
assert!((375..=625).contains(&(d1.as_millis() as u64)), "{d1:?}");
assert!((750..=1250).contains(&(d2.as_millis() as u64)), "{d2:?}");
assert!((1500..=2500).contains(&(d3.as_millis() as u64)), "{d3:?}");
assert_eq!(backoff_delay(500, 2), backoff_delay(500, 2));
assert!(backoff_delay(500, 30).as_millis() as u64 <= 37_500);
}
}