test(cli): wire-level integration tests via SPACESH_SOCK mock daemon
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,76 @@
|
|||||||
|
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");
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user