From 3df23788314091c6efa9b2a40914bf7e027f4569 Mon Sep 17 00:00:00 2001 From: Mollusk Date: Fri, 31 Jul 2026 01:10:11 -0400 Subject: [PATCH] test: the owed Stop Share SIGINT gate, against the real pixelpass MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 0c half (ii) had never been field-run: SIGINT sent, child exits within the bound, no fallback kill on the normal path. Now it's a repeatable live gate instead of a one-off manual check: a real whole-desktop host (idle — no viewer, so no capture) is stopped and must reach ScreenShareStopped inside STOP_GRACE. The SIGKILL fallback is indistinguishable from success in the event stream, so time is the discriminator: the fallback first waits out the full 2 s grace, while a host honouring SIGINT exits in milliseconds. Also holds the SIGINTed host's late stdout EOF to the same staleness contract as the fake-host gate. Verified green on this desktop; no stray pixelpass processes after the run. Co-Authored-By: Claude Fable 5 --- tests/screenshare_host_fault.rs | 98 +++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/tests/screenshare_host_fault.rs b/tests/screenshare_host_fault.rs index 6a90dab..4700fd1 100644 --- a/tests/screenshare_host_fault.rs +++ b/tests/screenshare_host_fault.rs @@ -195,3 +195,101 @@ async fn a_dead_host_is_torn_down_and_a_clean_stop_stays_clean() { assert!(controller.send(CoreCommand::Leave)); std::fs::remove_dir_all(&dir).ok(); } + +/// The long-owed Stop Share SIGINT gate (0c half (ii)), against the REAL +/// pixelpass binary: a Stop Share must end the host through the graceful +/// SIGINT path — child exits within [`STOP_GRACE`], no SIGKILL fallback, no +/// "couldn't confirm" warning — because SIGKILL would skip pixelpass's own +/// teardown (it unloads its capture sink on the way out in sink-owning modes). +/// +/// The fallback is indistinguishable from success in the event stream (both +/// end in a confirmed reap), so the discriminator is TIME: the fallback path +/// first waits out the full 2 s grace, while a host honouring SIGINT exits in +/// milliseconds. The bound asserts the stop completed inside the grace. +/// +/// Live: needs `pixelpass` on `$PATH` plus a real solo room (audio + network). +#[tokio::test] +#[ignore = "live: real pixelpass host + a real solo room (audio backend, network bind)"] +async fn stop_share_ends_the_real_host_via_sigint_within_the_grace() { + /// Mirrors `core::teardown::STOP_GRACE` (private): the graceful wait + /// before the SIGKILL fallback. + const STOP_GRACE: Duration = Duration::from_secs(2); + + let (ui_tx, mut ui_rx) = tokio::sync::mpsc::channel(256); + let controller = CoreController::new(ui_tx); + + // No override: resolve the real binary from $PATH. + assert!(controller.send(CoreCommand::SetPixelpassPath(None))); + assert!(controller.send(CoreCommand::Join { + name: "sigint-gate".into(), + ticket: "create".into(), + room_name: "s2".into(), + input_device: None, + output_device: None, + echo_cancellation: false, + avatar: Default::default(), + })); + wait_for(&mut ui_rx, "RoomJoined", |ev| match ev { + UiEvent::RoomJoined { .. } => Some(()), + UiEvent::Error(e) => panic!("join failed: {e}"), + _ => None, + }) + .await; + + // Whole-desktop share: no viewers ever connect, so the real host sits idle + // after its ticket (capture starts on first viewer) — exactly the state a + // Stop Share most often hits. + assert!(controller.send(CoreCommand::StartScreenShare { + audio_app: None, + settings: Default::default(), + quality: Default::default(), + })); + wait_for( + &mut ui_rx, + "ScreenShareStarted (real pixelpass)", + |ev| match ev { + UiEvent::ScreenShareStarted => Some(()), + UiEvent::Error(e) => panic!("real pixelpass host failed to start: {e}"), + _ => None, + }, + ) + .await; + + let stop_started = std::time::Instant::now(); + assert!(controller.send(CoreCommand::StopScreenShare)); + wait_for( + &mut ui_rx, + "ScreenShareStopped (real pixelpass)", + |ev| match ev { + UiEvent::ScreenShareStopped => Some(()), + // An Unconfirmed reap surfaces exactly this way; it means the + // SIGINT AND the SIGKILL both failed to end the host. + UiEvent::Error(e) => panic!("stop of the real host was not clean: {e}"), + _ => None, + }, + ) + .await; + let elapsed = stop_started.elapsed(); + assert!( + elapsed < STOP_GRACE, + "stop took {elapsed:?} — at or past the {STOP_GRACE:?} grace, i.e. the \ + SIGKILL fallback fired instead of pixelpass honouring SIGINT" + ); + + // And the late stdout EOF from the SIGINTed host must stay silent (same + // staleness contract the fake-host half pins). + let deadline = tokio::time::Instant::now() + QUIET_WINDOW; + while let Ok(Some(ev)) = tokio::time::timeout_at(deadline, ui_rx.recv()).await { + match ev { + UiEvent::ScreenShareStopped => { + panic!("stale fault from the SIGINTed real host re-emitted ScreenShareStopped") + } + UiEvent::Error(e) if e.contains("unexpectedly") => { + panic!("stale fault from the SIGINTed real host surfaced as an error: {e}") + } + _ => {} + } + } + + assert!(controller.send(CoreCommand::Leave)); +}