From f1630633e5acfe89297dbdd41163812ac68fbc35 Mon Sep 17 00:00:00 2001 From: Vassiliy Yegorov Date: Tue, 9 Jun 2026 20:39:24 +0700 Subject: [PATCH] test(daemon): serialize heavy socket/PTY integration tests Process-wide serial lock around the socket-binding and PTY-spawning integration tests in spaceshd. Running several at once on a many-core box starved each other's async tasks and tripped timing assumptions, causing ~1/10 flakes under cargo test --workspace. Unit tests stay parallel. 0/20 spaceshd + 0/5 workspace runs after the change. Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/spaceshd/src/main.rs | 18 ++++++++++++++++++ crates/spaceshd/src/server.rs | 3 +++ crates/spaceshd/src/surface.rs | 3 +++ 3 files changed, 24 insertions(+) diff --git a/crates/spaceshd/src/main.rs b/crates/spaceshd/src/main.rs index 5ac916a..d26842b 100644 --- a/crates/spaceshd/src/main.rs +++ b/crates/spaceshd/src/main.rs @@ -6,6 +6,24 @@ mod surface; use anyhow::Result; +/// Test-only support shared across the crate's test modules. +#[cfg(test)] +pub(crate) mod test_support { + use std::sync::{Mutex, MutexGuard}; + + /// Process-wide serialization lock for the heavy socket/PTY integration tests. + /// These bind sockets and spawn real PTYs/processes; running several at once on a + /// many-core box starves each other's tasks and trips timing assumptions. Unit + /// tests stay parallel; only guarded integration tests serialize on this lock. + static SERIAL: Mutex<()> = Mutex::new(()); + + /// Acquire the serial lock for the duration of a test. Poison-tolerant so one + /// panicking test does not cascade-fail the rest. + pub(crate) fn serial() -> MutexGuard<'static, ()> { + SERIAL.lock().unwrap_or_else(|e| e.into_inner()) + } +} + #[tokio::main] async fn main() -> Result<()> { let arg = std::env::args().nth(1); diff --git a/crates/spaceshd/src/server.rs b/crates/spaceshd/src/server.rs index 608678a..5c82164 100644 --- a/crates/spaceshd/src/server.rs +++ b/crates/spaceshd/src/server.rs @@ -323,6 +323,7 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn open_new_surface_attach_streams_output() { + let _serial = crate::test_support::serial(); let dir = tempdir_path(); let sock = dir.join("sock"); let sock_for_task = sock.clone(); @@ -360,6 +361,7 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn unknown_surface_returns_not_found() { + let _serial = crate::test_support::serial(); let dir = tempdir_path(); let sock = dir.join("sock"); let sock_for_task = sock.clone(); @@ -401,6 +403,7 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn reattach_returns_snapshot_with_prior_output() { + let _serial = crate::test_support::serial(); let dir = tempdir_path(); let sock = dir.join("sock"); let sock_for_task = sock.clone(); diff --git a/crates/spaceshd/src/surface.rs b/crates/spaceshd/src/surface.rs index 556d04e..bc68b14 100644 --- a/crates/spaceshd/src/surface.rs +++ b/crates/spaceshd/src/surface.rs @@ -132,6 +132,7 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn attach_receives_output() { + let _serial = crate::test_support::serial(); let pty = PtyHandle::spawn(spec("printf HELLO; sleep 0.3")).unwrap(); let (exit_tx, _exit_rx) = mpsc::unbounded_channel(); let handle = spawn_surface(SurfaceId("s_1".into()), WorkspaceId("w_1".into()), pty, 80, 24, exit_tx); @@ -155,6 +156,7 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn exit_is_reported() { + let _serial = crate::test_support::serial(); let pty = PtyHandle::spawn(spec("exit 7")).unwrap(); let (exit_tx, mut exit_rx) = mpsc::unbounded_channel(); let _handle = spawn_surface(SurfaceId("s_2".into()), WorkspaceId("w_1".into()), pty, 80, 24, exit_tx); @@ -166,6 +168,7 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn attach_snapshot_reflects_prior_output() { + let _serial = crate::test_support::serial(); let pty = PtyHandle::spawn(spec("printf SNAPME; sleep 0.5")).unwrap(); let (exit_tx, _exit_rx) = mpsc::unbounded_channel(); let handle = spawn_surface(SurfaceId("s_s".into()), WorkspaceId("w_1".into()), pty, 80, 24, exit_tx);