diff --git a/crates/botloftd/src/platform/unix/env.rs b/crates/botloftd/src/platform/unix/env.rs index bfacaa2c..2695b882 100644 --- a/crates/botloftd/src/platform/unix/env.rs +++ b/crates/botloftd/src/platform/unix/env.rs @@ -85,8 +85,16 @@ fn login_environment() -> Option { let _ = done.send(bytes); }); let bytes = output.recv_timeout(SHELL_TIMEOUT).ok(); + if bytes.is_none() { + let _ = job.terminate(); + } + // The pipe closes a moment before the process counts as exited: killing + // the group right at end of output could turn a clean exit into SIGKILL. + // With the output complete, `env` is exiting; wait for it, then sweep + // whatever the profile left behind. + let status = child.wait().ok(); let _ = job.terminate(); - let status = child.wait().ok()?; + let status = status?; let vars = parse(&bytes?)?; status.success().then_some(vars) } diff --git a/crates/botloftd/tests/browser_panel.rs b/crates/botloftd/tests/browser_panel.rs index eba3aa8a..16f7430a 100644 --- a/crates/botloftd/tests/browser_panel.rs +++ b/crates/botloftd/tests/browser_panel.rs @@ -47,11 +47,16 @@ async fn until_window(b: &mut Browsing, size: &str) -> String { panic!("the window never became {size}:\n{page}"); } -/// The next frame that is `height` tall. +/// The next frame that is `height` tall, in what it says and in its pixels. +/// Right after the page is resized the browser can still send a frame drawn +/// for the old surface under the new size's metadata (a 1000 x 700 page in a +/// 1000 x 560 picture, seen on Chromium 154 on Linux); the next repaint is +/// right. async fn frame_of(app: &mut Client, height: u64) -> Value { for _ in 0..50 { let frame = app.notification("browser.frame").await; - if frame["height"] == height { + let (width, tall) = jpeg_size(frame["data"].as_str().expect("picture")); + if frame["height"] == height && frame["width"] == width && u64::from(tall) == height { return frame; } } diff --git a/crates/botloftd/tests/common/browsing.rs b/crates/botloftd/tests/common/browsing.rs index ae8cd626..ea429948 100644 --- a/crates/botloftd/tests/common/browsing.rs +++ b/crates/botloftd/tests/common/browsing.rs @@ -1,7 +1,10 @@ //! The browser tests' shared setup (spec 21): a bot in Manual mode, a local //! site, and helpers to call tools in the background and answer requests. +use std::sync::OnceLock; + use serde_json::{Value, json}; +use tokio::sync::{OwnedSemaphorePermit, Semaphore}; use tokio::task::JoinHandle; use super::bots::ready_bot; @@ -14,6 +17,19 @@ pub struct Browsing { pub bot: Value, pub mcp: Mcp, pub site: String, + /// Held until the test ends, after the daemon has closed its browser. + _slot: OwnedSemaphorePermit, +} + +/// Real browsers a test binary has open at once. A dozen cold Chromium +/// profiles starting together on a small CI runner could pass the daemon's +/// 30 s start limit, so the tests wait their turn instead. +const BROWSERS_AT_ONCE: usize = 3; + +async fn browser_slot() -> OwnedSemaphorePermit { + static SLOTS: OnceLock> = OnceLock::new(); + let slots = SLOTS.get_or_init(|| std::sync::Arc::new(Semaphore::new(BROWSERS_AT_ONCE))); + slots.clone().acquire_owned().await.expect("open semaphore") } /// A bot in Manual mode and a local site, or `None` without Edge. @@ -22,6 +38,7 @@ pub async fn setup() -> Option { eprintln!("Microsoft Edge is not installed; skipping"); return None; } + let slot = browser_slot().await; // A real browser can be slow to ask on a busy CI runner. let t = TestDaemon::start_supervised_patient().await; let mut app = t.session().await; @@ -37,6 +54,7 @@ pub async fn setup() -> Option { bot, mcp, site, + _slot: slot, }) }