test: the owed Stop Share SIGINT gate, against the real pixelpass
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user