use std::path::PathBuf; use spacesh_proto::codec::{read_frame, write_frame}; use spacesh_proto::{Cmd, Envelope, SurfaceId}; use spacesh_proto::status::SurfaceState; use spacesh_cli::client; use tokio::net::UnixListener; // These tests mutate the process-global SPACESH_SOCK env var, so they must not // run concurrently. Serialize them on a process-wide lock (poison-tolerant). static SERIAL: std::sync::Mutex<()> = std::sync::Mutex::new(()); fn serial() -> std::sync::MutexGuard<'static, ()> { SERIAL.lock().unwrap_or_else(|e| e.into_inner()) } fn tmp_sock(name: &str) -> PathBuf { let mut p = std::env::temp_dir(); let n = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos(); p.push(format!("spacesh-cli-{name}-{n}.sock")); p } /// One-shot mock daemon: accept one connection, read one request, send `reply`. fn mock_daemon(sock: PathBuf, reply: Envelope) { let listener = UnixListener::bind(&sock).unwrap(); tokio::spawn(async move { if let Ok((mut stream, _)) = listener.accept().await { if let Ok(Some(_req)) = read_frame(&mut stream).await { let _ = write_frame(&mut stream, &reply).await; } } }); } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn request_returns_data_from_daemon() { let _g = serial(); let sock = tmp_sock("req"); std::env::set_var("SPACESH_SOCK", &sock); mock_daemon(sock.clone(), Envelope::Res { id: 1, ok: true, data: serde_json::json!({ "workspace_id": "w_1" }), error: None, }); tokio::time::sleep(std::time::Duration::from_millis(50)).await; // let the listener bind let data = client::request(Cmd::Open { path: "/tmp".into() }).await.unwrap(); std::env::remove_var("SPACESH_SOCK"); let _ = std::fs::remove_file(&sock); assert_eq!(data["workspace_id"], "w_1"); } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn request_surfaces_daemon_error() { let _g = serial(); let sock = tmp_sock("err"); std::env::set_var("SPACESH_SOCK", &sock); mock_daemon(sock.clone(), Envelope::Res { id: 1, ok: false, data: serde_json::Value::Null, error: Some(spacesh_proto::ErrorBody { code: "NOT_FOUND".into(), msg: "surface".into() }), }); tokio::time::sleep(std::time::Duration::from_millis(50)).await; let res = client::request(Cmd::Close { surface_id: SurfaceId("s_x".into()) }).await; std::env::remove_var("SPACESH_SOCK"); let _ = std::fs::remove_file(&sock); assert!(res.is_err()); assert!(res.unwrap_err().to_string().contains("NOT_FOUND")); } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn notify_with_no_daemon_is_silent_success() { let _g = serial(); let sock = tmp_sock("nodaemon"); // never bound std::env::set_var("SPACESH_SOCK", &sock); let r = client::notify(Cmd::SetState { surface_id: SurfaceId("s_1".into()), state: SurfaceState::Done }).await; std::env::remove_var("SPACESH_SOCK"); assert!(r.is_ok(), "notify must be a silent success when no daemon is listening"); }