#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use std::path::PathBuf;
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::{Arc, Mutex};
use llmenv::mcp::proxy::{EnsureOutcome, ensure_running, is_alive};
use tempfile::tempdir;
#[derive(Default)]
struct SpawnLog {
bind_args: Mutex<Vec<String>>,
next_pid: AtomicU32,
}
impl SpawnLog {
fn calls(&self) -> usize {
self.bind_args.lock().expect("lock").len()
}
}
fn spawner(log: Arc<SpawnLog>) -> impl Fn(&str) -> anyhow::Result<u32> {
move |bind: &str| {
log.bind_args.lock().expect("lock").push(bind.to_owned());
let pid = log.next_pid.fetch_add(1, Ordering::SeqCst) + 4_000_000;
Ok(pid)
}
}
#[test]
fn ensure_running_spawns_when_no_pidfile() {
let tmp = tempdir().expect("tempdir");
let pid_path: PathBuf = tmp.path().join("mcp-proxy.pid");
let log = Arc::new(SpawnLog::default());
let outcome =
ensure_running("127.0.0.1:8765", &pid_path, spawner(log.clone())).expect("ensure_running");
assert_eq!(outcome, EnsureOutcome::Spawned);
assert_eq!(log.calls(), 1, "spawn must be called exactly once");
assert!(pid_path.exists(), "pidfile must be written after spawn");
}
#[test]
fn ensure_running_no_op_when_pid_alive() {
let tmp = tempdir().expect("tempdir");
let pid_path: PathBuf = tmp.path().join("mcp-proxy.pid");
let my_pid = std::process::id();
std::fs::write(&pid_path, my_pid.to_string()).expect("write pidfile");
let log = Arc::new(SpawnLog::default());
let outcome =
ensure_running("127.0.0.1:8765", &pid_path, spawner(log.clone())).expect("ensure_running");
assert_eq!(outcome, EnsureOutcome::AlreadyRunning);
assert_eq!(log.calls(), 0, "spawn must not be called when pid is alive");
}
#[test]
fn ensure_running_respawns_when_pid_dead() {
let tmp = tempdir().expect("tempdir");
let pid_path: PathBuf = tmp.path().join("mcp-proxy.pid");
std::fs::write(&pid_path, "4000001").expect("write stale pidfile");
let log = Arc::new(SpawnLog::default());
let outcome =
ensure_running("127.0.0.1:8765", &pid_path, spawner(log.clone())).expect("ensure_running");
assert_eq!(outcome, EnsureOutcome::Spawned);
assert_eq!(log.calls(), 1, "spawn must be called when pidfile is stale");
let contents = std::fs::read_to_string(&pid_path).expect("read pid");
let parsed: u32 = contents.trim().parse().expect("parse pid");
assert_ne!(
parsed, 4_000_001,
"pidfile must be overwritten with new pid"
);
}
#[test]
fn ensure_running_passes_bind_to_spawner() {
let tmp = tempdir().expect("tempdir");
let pid_path: PathBuf = tmp.path().join("mcp-proxy.pid");
let log = Arc::new(SpawnLog::default());
ensure_running("0.0.0.0:9999", &pid_path, spawner(log.clone())).expect("ensure_running");
let calls = log.bind_args.lock().expect("lock");
assert_eq!(calls.as_slice(), &["0.0.0.0:9999".to_owned()]);
}
#[test]
fn is_alive_returns_false_for_almost_certainly_dead_pid() {
assert!(!is_alive(4_000_002));
}
#[test]
fn is_alive_returns_true_for_self() {
let my_pid = std::process::id();
assert!(is_alive(my_pid));
}
#[test]
fn ensure_running_errors_when_lock_is_held_and_pid_dead() {
let tmp = tempdir().expect("tempdir");
let pid_path: PathBuf = tmp.path().join("mcp-proxy.pid");
let lock_path: PathBuf = tmp.path().join("mcp-proxy.pid.lock");
std::fs::write(&lock_path, "").expect("write lockfile");
let log = Arc::new(SpawnLog::default());
let result = ensure_running("127.0.0.1:8765", &pid_path, spawner(log.clone()));
assert!(result.is_err(), "should error when peer holds lock");
assert_eq!(log.calls(), 0, "must not spawn while lock is held");
}
#[test]
fn ensure_running_accepts_peer_published_pid() {
let tmp = tempdir().expect("tempdir");
let pid_path: PathBuf = tmp.path().join("mcp-proxy.pid");
let lock_path: PathBuf = tmp.path().join("mcp-proxy.pid.lock");
std::fs::write(&lock_path, "").expect("write lockfile");
std::fs::write(&pid_path, std::process::id().to_string()).expect("write pid");
let log = Arc::new(SpawnLog::default());
let outcome =
ensure_running("127.0.0.1:8765", &pid_path, spawner(log.clone())).expect("ensure_running");
assert_eq!(outcome, llmenv::mcp::proxy::EnsureOutcome::AlreadyRunning);
assert_eq!(log.calls(), 0);
}