malvin 0.2.5

Non-interactive research and coding agent
use super::{SOCKET_TIMEOUT, classify_reply, send_request, send_request_checked};
use serde_json::json;
use std::io::{Read, Write};
use std::os::unix::net::UnixListener;
#[cfg(target_os = "linux")]
use std::os::unix::net::UnixStream;
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
#[cfg(target_os = "linux")]
use std::time::Instant;

fn bind_or_skip(path: &std::path::Path) -> Option<UnixListener> {
    match UnixListener::bind(path) {
        Ok(listener) => Some(listener),
        Err(error) if error.kind() == std::io::ErrorKind::PermissionDenied => {
            eprintln!("skipping Unix-socket send test: bind denied: {error}");
            None
        }
        Err(error) => panic!("bind: {error}"),
    }
}

#[test]
fn send_request_writes_ndjson_line_to_unix_socket() {
    let dir = tempfile::tempdir().expect("tempdir");
    let sock = dir.path().join("t.sock");
    let Some(listener) = bind_or_skip(&sock) else {
        return;
    };
    let (tx, rx) = mpsc::channel();
    thread::spawn(move || {
        let (mut conn, _) = listener.accept().expect("accept");
        let mut buf = Vec::new();
        let _ = conn.read_to_end(&mut buf);
        let _ = conn.write_all(br#"{"result":{"type":"ok"}}"#);
        let _ = tx.send(buf);
    });
    let req = json!({"id":"t","method":"ping","params":{}});
    send_request(&sock, &req);
    let got = rx.recv_timeout(Duration::from_secs(2)).expect("recv");
    let text = String::from_utf8_lossy(&got);
    assert!(text.ends_with('\n'), "expected NDJSON newline: {text:?}");
    assert!(text.contains("\"method\":\"ping\""));
    let _ = SOCKET_TIMEOUT;
    let _ = send_request_checked;
}

#[test]
fn send_request_swallows_missing_socket() {
    send_request(std::path::Path::new("/no/such/herdr.sock"), &json!({}));
    assert!(send_request_checked(std::path::Path::new("/no/such/herdr.sock"), &json!({})).is_err());
}

#[test]
fn classify_reply_detects_herdr_error_json() {
    assert!(classify_reply(br#"{"id":"1","error":{"code":"x","message":"no"}}"#).is_err());
    assert!(classify_reply(br#"{"result":{"type":"ok"}}"#).is_ok());
    assert!(classify_reply(b"").is_err());
    assert!(classify_reply(b"HTTP/1.1 500").is_err());
    assert!(classify_reply(br#"{"id":"1"}"#).is_err());
}

#[cfg(target_os = "linux")]
#[test]
fn send_request_returns_when_accept_queue_is_wedged() {
    let Some(wedge) = WedgedUnixSocket::new() else {
        return;
    };
    let req = json!({"id": "t", "method": "ping", "params": {}});
    assert_connect_times_out(|| send_request_checked(&wedge.path, &req));
    assert_returns_within_budget(|| send_request(&wedge.path, &req));
}

#[cfg(target_os = "linux")]
struct WedgedUnixSocket {
    path: std::path::PathBuf,
    _dir: tempfile::TempDir,
    _listener: UnixListener,
    _holders: [UnixStream; 2],
}

#[cfg(target_os = "linux")]
impl WedgedUnixSocket {
    fn new() -> Option<Self> {
        #![allow(unsafe_code)]
        use std::os::fd::AsRawFd;

        let dir = tempfile::tempdir().expect("tempdir");
        let path = dir.path().join("wedge.sock");
        let listener = bind_or_skip(&path)?;
        assert_eq!(unsafe { libc::listen(listener.as_raw_fd(), 1) }, 0);
        let holders = [
            UnixStream::connect(&path).expect("holder a"),
            UnixStream::connect(&path).expect("holder b"),
        ];
        Some(Self {
            path,
            _dir: dir,
            _listener: listener,
            _holders: holders,
        })
    }
}

#[cfg(target_os = "linux")]
fn connect_budget() -> Duration {
    SOCKET_TIMEOUT + Duration::from_millis(750)
}

#[cfg(target_os = "linux")]
fn assert_connect_times_out(send: impl FnOnce() -> Result<(), String>) {
    let start = Instant::now();
    let err = send().expect_err("wedged accept must not succeed");
    assert!(
        start.elapsed() <= connect_budget(),
        "expected return near {:?}, got {:?}; err={err}",
        SOCKET_TIMEOUT,
        start.elapsed()
    );
    assert!(
        err.contains("timed out"),
        "expected connect timeout, got: {err}"
    );
}

#[cfg(target_os = "linux")]
fn assert_returns_within_budget(send: impl FnOnce()) {
    let start = Instant::now();
    send();
    assert!(
        start.elapsed() <= connect_budget(),
        "send_request must also bound connect"
    );
}