From 615b90e887985f04a460dc2c1cdf95f297e1a3ce Mon Sep 17 00:00:00 2001 From: Vassiliy Yegorov Date: Wed, 10 Jun 2026 08:35:38 +0700 Subject: [PATCH] test(daemon): event log survives cold daemon restart Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/spaceshd/src/server.rs | 76 +++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/crates/spaceshd/src/server.rs b/crates/spaceshd/src/server.rs index ee3f740..ac3c214 100644 --- a/crates/spaceshd/src/server.rs +++ b/crates/spaceshd/src/server.rs @@ -1163,6 +1163,82 @@ mod tests { assert_eq!(res_data(&log)["unread"].as_u64().unwrap(), 0); } + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn event_log_persists_across_daemon_restart() { + let _serial = crate::test_support::serial(); + let dir = tempdir_path(); + let state_path = dir.join("state.json"); + let sock = dir.join("sock"); + + let event_id: u64; + let ws_id: String; + + // ── Instance A ──────────────────────────────────────────────────────── + { + let store: std::sync::Arc = + std::sync::Arc::new(crate::state_store::JsonStateStore::new(state_path.clone())); + let event_store = make_event_store(&dir); + let sock2 = sock.clone(); + tokio::spawn(async move { let _ = serve(&sock2, store, event_store).await; }); + wait_for_socket(&sock).await; + + let mut s = UnixStream::connect(&sock).await.unwrap(); + + // Open workspace, spawn surface. + let r = req(&mut s, 1, Cmd::Open { path: std::env::temp_dir().to_string_lossy().into() }).await; + ws_id = res_data(&r)["workspace_id"].as_str().unwrap().to_string(); + + let r = req(&mut s, 2, Cmd::NewSurface { + workspace_id: spacesh_proto::WorkspaceId(ws_id.clone()), + command: Some("/bin/sh".into()), + args: vec!["-c".into(), "sleep 5".into()], + cols: 80, rows: 24, + }).await; + let sid = res_data(&r)["surface_id"].as_str().unwrap().to_string(); + + // Drive an Error state → one unread event is logged. + let _ = req(&mut s, 3, Cmd::SetState { + surface_id: spacesh_proto::SurfaceId(sid.clone()), + state: spacesh_proto::status::SurfaceState::Error, + }).await; + + // Query and assert unread == 1 before restart. + let log = req(&mut s, 4, Cmd::EventLog { limit: None }).await; + let data = res_data(&log); + assert_eq!(data["unread"].as_u64().unwrap(), 1, "instance A: expected 1 unread event"); + assert_eq!(data["events"][0]["kind"].as_str().unwrap(), "error"); + assert_eq!(data["events"][0]["workspace_id"].as_str().unwrap(), ws_id); + event_id = data["events"][0]["id"].as_u64().unwrap(); + + // Wait comfortably longer than the 500 ms debounce so events.json is flushed. + tokio::time::sleep(tokio::time::Duration::from_millis(900)).await; + // Drop `s` (and instance A's task) by falling out of scope. + } + + // ── Instance B (same dir, fresh socket path) ────────────────────────── + let sock_b = dir.join("sock2"); + let store_b: std::sync::Arc = + std::sync::Arc::new(crate::state_store::JsonStateStore::new(state_path.clone())); + let event_store_b = make_event_store(&dir); + let sb2 = sock_b.clone(); + tokio::spawn(async move { let _ = serve(&sock_b, store_b, event_store_b).await; }); + wait_for_socket(&sb2).await; + + let mut s2 = UnixStream::connect(&sb2).await.unwrap(); + + // Query event log on instance B — the persisted event must survive the restart. + let log = req(&mut s2, 1, Cmd::EventLog { limit: None }).await; + let data = res_data(&log); + assert_eq!(data["unread"].as_u64().unwrap(), 1, + "instance B: event log unread count must survive cold restart"); + assert_eq!(data["events"][0]["id"].as_u64().unwrap(), event_id, + "instance B: event id must match"); + assert_eq!(data["events"][0]["kind"].as_str().unwrap(), "error", + "instance B: event kind must be 'error'"); + assert_eq!(data["events"][0]["workspace_id"].as_str().unwrap(), ws_id, + "instance B: workspace_id must match"); + } + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn focus_marks_surface_events_read() { let _serial = crate::test_support::serial();