//! Live launch test: spawn a scratch daemon session, launch headless //! Chrome, navigate to `about:blank`, then close. Skipped under `--quick`. //! //! A `LaunchGuard` Drop impl ensures the scratch session is closed and its //! sidecar files cleaned even on panic or early return. use std::env; use std::time::{Duration, Instant, SystemTime}; use serde_json::{json, Value}; use super::helpers::new_id; use super::{Check, Status}; use crate::connection::{cleanup_stale_files, ensure_daemon, send_command, DaemonOptions}; pub(super) fn check(checks: &mut Vec) { let category = "Launch test"; if env::var("AGENT_BROWSER_PROVIDER").is_ok() { checks.push(Check::new( "launch.skipped.provider", category, Status::Info, "Skipped (AGENT_BROWSER_PROVIDER is set; would consume cloud quota)", )); return; } if env::var("AGENT_BROWSER_CDP").is_ok() { checks.push(Check::new( "launch.skipped.cdp", category, Status::Info, "Skipped (AGENT_BROWSER_CDP is set; would attach to a real browser)", )); return; } let session = format!( "doctor-{}-{}", std::process::id(), SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) .map(|d| d.as_millis()) .unwrap_or(0) ); // Armed after `ensure_daemon` succeeds so we don't send a stray `close` // or delete sidecar files for a daemon that never started. On every early // return past the `Some(...)` assignment below, Drop runs one close and // one `cleanup_stale_files`. let mut _guard: Option = None; let opts = DaemonOptions { headed: false, debug: false, executable_path: None, extensions: &[], init_scripts: &[], enable: &[], args: None, user_agent: None, proxy: None, proxy_bypass: None, proxy_username: None, proxy_password: None, ignore_https_errors: false, allow_file_access: false, hide_scrollbars: true, profile: None, state: None, provider: None, device: None, session_name: None, download_path: None, allowed_domains: None, action_policy: None, confirm_actions: None, engine: None, auto_connect: false, force_launch: false, idle_timeout: None, default_timeout: None, cdp: None, no_auto_dialog: false, }; let started = Instant::now(); if let Err(e) = ensure_daemon(&session, &opts) { checks.push( Check::new( "launch.daemon", category, Status::Fail, format!("Could not start daemon: {}", e), ) .with_fix("check Chrome install and re-run with --debug"), ); return; } _guard = Some(LaunchGuard { session: session.clone(), }); let launch_cmd = json!({ "id": new_id(), "action": "launch", "headless": true, }); if let Err(e) = send_json(launch_cmd, &session) { checks.push( Check::new( "launch.launch", category, Status::Fail, format!("Browser launch failed: {}", e), ) .with_fix("agent-browser install # or check --debug output"), ); return; } let open_cmd = json!({ "id": new_id(), "action": "navigate", "url": "about:blank", }); if let Err(e) = send_json(open_cmd, &session) { checks.push( Check::new( "launch.navigate", category, Status::Fail, format!("Navigation to about:blank failed: {}", e), ) .with_fix("re-run with --debug for full launch logs"), ); return; } // Close + stale-file cleanup happen exactly once via LaunchGuard::drop at // end of scope; no explicit close here. let elapsed = started.elapsed(); let secs = elapsed.as_secs_f64(); if elapsed > Duration::from_secs(5) { checks.push(Check::new( "launch.elapsed", category, Status::Warn, format!( "Headless launch + about:blank in {:.2}s (slow; expected < 5s)", secs ), )); } else { checks.push(Check::new( "launch.elapsed", category, Status::Pass, format!("Headless launch + about:blank in {:.2}s", secs), )); } } fn send_json(cmd: Value, session: &str) -> Result<(), String> { match send_command(cmd, session) { Ok(resp) => { if resp.success { Ok(()) } else { Err(resp.error.unwrap_or_else(|| "unknown error".to_string())) } } Err(e) => Err(e), } } /// Best-effort cleanup when the launch test panics or returns early. struct LaunchGuard { session: String, } impl Drop for LaunchGuard { fn drop(&mut self) { let close_cmd = json!({ "id": new_id(), "action": "close" }); let _ = send_command(close_cmd, &self.session); cleanup_stale_files(&self.session); } }