use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Mutex;
use std::time::Duration;
pub const PREFLIGHT_HEADER: &str = "x-openlatch-preflight";
const UPSTREAM_UNREACHABLE_HEADER: &str = "x-openlatch-upstream";
pub const PREFLIGHT_TIMEOUT: Duration = Duration::from_secs(5);
const PREFLIGHT_BODY: &str =
r#"{"model":"claude-sonnet-4-5","max_tokens":1,"messages":[{"role":"user","content":"ping"}]}"#;
const PREFLIGHT_API_KEY: &str = "ol-preflight-not-a-key";
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub enum Verdict {
#[default]
Pending,
Ok,
Failed(String),
}
impl Verdict {
pub fn label(&self) -> &'static str {
match self {
Self::Pending => "pending",
Self::Ok => "ok",
Self::Failed(_) => "failed",
}
}
pub fn error(&self) -> Option<&str> {
match self {
Self::Failed(e) => Some(e.as_str()),
_ => None,
}
}
pub fn is_ok(&self) -> bool {
matches!(self, Self::Ok)
}
}
#[derive(Debug, Default)]
pub struct WiringState {
wired: AtomicBool,
verdict: Mutex<Verdict>,
}
impl WiringState {
pub fn is_wired(&self) -> bool {
self.wired.load(Ordering::Relaxed)
}
pub fn set_wired(&self, wired: bool) {
self.wired.store(wired, Ordering::Relaxed);
}
pub fn verdict(&self) -> Verdict {
match self.verdict.lock() {
Ok(v) => v.clone(),
Err(poisoned) => poisoned.into_inner().clone(),
}
}
pub fn set_verdict(&self, verdict: Verdict) {
match self.verdict.lock() {
Ok(mut v) => *v = verdict,
Err(poisoned) => *poisoned.into_inner() = verdict,
}
}
}
pub async fn probe(port: u16, upstream: &str, timeout: Duration) -> Result<(), String> {
let client = match reqwest::Client::builder().timeout(timeout).build() {
Ok(c) => c,
Err(e) => return Err(format!("could not build the preflight client: {e}")),
};
let url = format!("http://127.0.0.1:{port}/v1/messages");
let sent = client
.post(&url)
.header("content-type", "application/json")
.header("anthropic-version", "2023-06-01")
.header("x-api-key", PREFLIGHT_API_KEY)
.header(PREFLIGHT_HEADER, "1")
.body(PREFLIGHT_BODY)
.send()
.await;
let resp = match sent {
Ok(r) => r,
Err(e) if e.is_timeout() => {
return Err(format!(
"no response from the boundary on 127.0.0.1:{port} within {}s",
timeout.as_secs()
))
}
Err(e) => {
return Err(format!(
"could not reach the boundary on 127.0.0.1:{port}: {}",
e.without_url()
))
}
};
if resp.headers().contains_key(UPSTREAM_UNREACHABLE_HEADER) {
return Err(format!(
"the boundary is listening but could not reach {upstream} — model calls would fail"
));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::boundary::{mock, serve_ephemeral, BoundaryState};
use std::sync::Arc;
fn state_for(upstream_port: u16) -> Arc<BoundaryState> {
let base = reqwest::Url::parse(&format!("http://127.0.0.1:{upstream_port}")).unwrap();
Arc::new(BoundaryState::new(base, 0, 8, &[]))
}
#[tokio::test(flavor = "multi_thread")]
async fn probe_passes_when_upstream_answers() {
let upstream = mock::spawn_capture_200().await;
let port = serve_ephemeral(state_for(upstream.port)).await;
assert_eq!(
probe(port, crate::boundary::ANTHROPIC_BASE, PREFLIGHT_TIMEOUT).await,
Ok(())
);
}
#[tokio::test(flavor = "multi_thread")]
async fn probe_fails_when_upstream_is_unreachable() {
let dead = mock::closed_port().await;
let port = serve_ephemeral(state_for(dead)).await;
let err = probe(port, crate::boundary::ANTHROPIC_BASE, PREFLIGHT_TIMEOUT)
.await
.unwrap_err();
assert!(
err.contains("could not reach"),
"an unreachable upstream must be named as such, got: {err}"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn probe_fails_fast_on_a_silent_upstream() {
let hung = mock::spawn_hang_after_accept().await;
let upstream = reqwest::Url::parse(&format!("http://127.0.0.1:{hung}")).unwrap();
let state = Arc::new(
BoundaryState::new(upstream, 0, 8, &[]).with_header_timeout(Duration::from_secs(60)),
);
let port = serve_ephemeral(state).await;
let started = std::time::Instant::now();
let err = probe(
port,
crate::boundary::ANTHROPIC_BASE,
Duration::from_millis(300),
)
.await
.unwrap_err();
assert!(
err.contains("no response"),
"a silent upstream must read as no response, got: {err}"
);
assert!(
started.elapsed() < Duration::from_secs(5),
"the probe must return on its own budget, not the forward path's"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn probe_fails_when_nothing_is_listening() {
let port = mock::closed_port().await;
assert!(probe(
port,
crate::boundary::ANTHROPIC_BASE,
Duration::from_millis(500)
)
.await
.is_err());
}
#[test]
fn verdict_labels_are_stable() {
assert_eq!(Verdict::default(), Verdict::Pending);
assert_eq!(Verdict::Pending.label(), "pending");
assert_eq!(Verdict::Ok.label(), "ok");
assert_eq!(Verdict::Failed("boom".into()).label(), "failed");
assert_eq!(Verdict::Failed("boom".into()).error(), Some("boom"));
assert_eq!(Verdict::Ok.error(), None);
assert!(Verdict::Ok.is_ok());
assert!(!Verdict::Pending.is_ok());
}
#[test]
fn wiring_state_round_trips() {
let st = WiringState::default();
assert!(!st.is_wired());
assert_eq!(st.verdict(), Verdict::Pending);
st.set_wired(true);
st.set_verdict(Verdict::Ok);
assert!(st.is_wired());
assert_eq!(st.verdict(), Verdict::Ok);
}
}