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));
|
assert!(controller.send(CoreCommand::Leave));
|
||||||
std::fs::remove_dir_all(&dir).ok();
|
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