Codex Handbook
app-server-daemon/src/lib.rs 1034 lines
mod backend;mod client;mod managed_install;mod remote_control_client;mod settings;mod update_loop;use std::path::Path;use std::path::PathBuf;use std::time::Duration;use anyhow::Context;use anyhow::Result;use anyhow::anyhow;pub use backend::BackendKind;use backend::BackendPaths;use codex_app_server_protocol::RemoteControlConnectionStatus;use codex_app_server_transport::app_server_control_socket_path;use codex_utils_home_dir::find_codex_home;use managed_install::managed_codex_bin;#[cfg(unix)]use managed_install::managed_codex_version;use serde::Serialize;use settings::DaemonSettings;use tokio::time::sleep;const START_POLL_INTERVAL: Duration = Duration::from_millis(50);const START_TIMEOUT: Duration = Duration::from_secs(10);const OPERATION_LOCK_TIMEOUT: Duration = Duration::from_secs(75);const PID_FILE_NAME: &str = "app-server.pid";const UPDATE_PID_FILE_NAME: &str = "app-server-updater.pid";const OPERATION_LOCK_FILE_NAME: &str = "daemon.lock";const SETTINGS_FILE_NAME: &str = "settings.json";const STATE_DIR_NAME: &str = "app-server-daemon";#[derive(Debug, Clone, Copy, PartialEq, Eq)]pub enum LifecycleCommand {    Start,    Restart,    Stop,    Version,}#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]#[serde(rename_all = "camelCase")]pub enum LifecycleStatus {    AlreadyRunning,    Started,    Restarted,    Stopped,    NotRunning,    Running,}#[derive(Debug, Clone, PartialEq, Eq, Serialize)]#[serde(rename_all = "camelCase")]pub struct LifecycleOutput {    pub status: LifecycleStatus,    #[serde(skip_serializing_if = "Option::is_none")]    pub backend: Option<BackendKind>,    #[serde(skip_serializing_if = "Option::is_none")]    pub pid: Option<u32>,    pub managed_codex_path: PathBuf,    pub managed_codex_version: Option<String>,    pub socket_path: PathBuf,    #[serde(skip_serializing_if = "Option::is_none")]    pub cli_version: Option<String>,    #[serde(skip_serializing_if = "Option::is_none")]    pub app_server_version: Option<String>,}#[derive(Debug, Clone, Copy, PartialEq, Eq)]pub struct BootstrapOptions {    pub remote_control_enabled: bool,}/// Passively probes an existing app-server socket and returns its reported/// app-server version.pub async fn probe_app_server_version(socket_path: &Path) -> Result<String> {    Ok(client::probe(socket_path).await?.app_server_version)}#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]#[serde(rename_all = "camelCase")]pub enum BootstrapStatus {    Bootstrapped,}#[derive(Debug, Clone, PartialEq, Eq, Serialize)]#[serde(rename_all = "camelCase")]pub struct BootstrapOutput {    pub status: BootstrapStatus,    pub backend: BackendKind,    pub auto_update_enabled: bool,    pub remote_control_enabled: bool,    pub managed_codex_path: PathBuf,    pub managed_codex_version: Option<String>,    pub socket_path: PathBuf,    pub cli_version: String,    pub app_server_version: String,}#[derive(Debug, Clone, PartialEq, Eq, Serialize)]#[serde(untagged)]pub enum RemoteControlStartOutput {    Bootstrap(BootstrapOutput),    Start(LifecycleOutput),}#[derive(Debug, Clone, PartialEq, Eq)]pub struct RemoteControlReadyStatus {    pub status: RemoteControlConnectionStatus,    pub server_name: String,    pub environment_id: Option<String>,    pub timed_out: bool,}#[derive(Debug, Clone, PartialEq, Eq)]pub struct RemoteControlReadyOutput {    pub daemon: RemoteControlStartOutput,    pub remote_control: RemoteControlReadyStatus,}#[derive(Debug, Clone, Copy, PartialEq, Eq)]pub enum RemoteControlMode {    Enabled,    Disabled,}impl RemoteControlMode {    fn is_enabled(self) -> bool {        matches!(self, Self::Enabled)    }}#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]#[serde(rename_all = "camelCase")]pub enum RemoteControlStatus {    Enabled,    Disabled,    AlreadyEnabled,    AlreadyDisabled,}#[derive(Debug, Clone, PartialEq, Eq, Serialize)]#[serde(rename_all = "camelCase")]pub struct RemoteControlOutput {    pub status: RemoteControlStatus,    #[serde(skip_serializing_if = "Option::is_none")]    pub backend: Option<BackendKind>,    pub remote_control_enabled: bool,    pub socket_path: PathBuf,    pub cli_version: String,    #[serde(skip_serializing_if = "Option::is_none")]    pub app_server_version: Option<String>,}#[cfg(unix)]#[derive(Debug, Clone, Copy, PartialEq, Eq)]pub(crate) enum RestartIfRunningOutcome {    Busy,    NotRunning,    NotReady,    AlreadyCurrent,    Restarted,}#[cfg(unix)]#[derive(Debug, Clone, Copy, PartialEq, Eq)]pub(crate) enum RestartMode {    IfVersionChanged,    Always,}#[cfg(unix)]#[derive(Debug, Clone, Copy, PartialEq, Eq)]pub(crate) enum UpdaterRefreshMode {    None,    ReexecIfManagedBinaryChanged,}#[cfg(unix)]#[derive(Debug, Clone, Copy, PartialEq, Eq)]enum RestartDecision {    NotReady,    AlreadyCurrent,    Restart,}pub async fn run(command: LifecycleCommand) -> Result<LifecycleOutput> {    ensure_supported_platform()?;    Daemon::from_environment()?.run(command).await}pub async fn bootstrap(options: BootstrapOptions) -> Result<BootstrapOutput> {    ensure_supported_platform()?;    Daemon::from_environment()?.bootstrap(options).await}pub async fn ensure_remote_control_started() -> Result<RemoteControlStartOutput> {    ensure_supported_platform()?;    Daemon::from_environment()?        .ensure_remote_control_started()        .await}pub async fn ensure_remote_control_ready() -> Result<RemoteControlReadyOutput> {    ensure_supported_platform()?;    Daemon::from_environment()?        .ensure_remote_control_ready()        .await}pub async fn enable_remote_control_on_socket(    socket_path: &Path,    connect_timeout: Duration,    connect_retry_delay: Duration,) -> Result<RemoteControlReadyStatus> {    ensure_supported_platform()?;    remote_control_client::enable_remote_control_with_connect_retry(        socket_path,        connect_timeout,        connect_retry_delay,    )    .await}pub async fn set_remote_control(mode: RemoteControlMode) -> Result<RemoteControlOutput> {    ensure_supported_platform()?;    Daemon::from_environment()?.set_remote_control(mode).await}pub async fn run_pid_update_loop() -> Result<()> {    ensure_supported_platform()?;    update_loop::run().await}#[cfg(unix)]fn ensure_supported_platform() -> Result<()> {    Ok(())}#[cfg(not(unix))]fn ensure_supported_platform() -> Result<()> {    Err(anyhow!(        "codex app-server daemon lifecycle is only supported on Unix platforms"    ))}struct Daemon {    socket_path: PathBuf,    pid_file: PathBuf,    update_pid_file: PathBuf,    operation_lock_file: PathBuf,    settings_file: PathBuf,    managed_codex_bin: PathBuf,}impl Daemon {    fn from_environment() -> Result<Self> {        let codex_home = find_codex_home().context("failed to resolve CODEX_HOME")?;        let socket_path = app_server_control_socket_path(codex_home.as_path())?            .as_path()            .to_path_buf();        let state_dir = codex_home.as_path().join(STATE_DIR_NAME);        Ok(Self {            socket_path,            pid_file: state_dir.join(PID_FILE_NAME),            update_pid_file: state_dir.join(UPDATE_PID_FILE_NAME),            operation_lock_file: state_dir.join(OPERATION_LOCK_FILE_NAME),            settings_file: state_dir.join(SETTINGS_FILE_NAME),            managed_codex_bin: managed_codex_bin(codex_home.as_path()),        })    }    async fn run(&self, command: LifecycleCommand) -> Result<LifecycleOutput> {        match command {            LifecycleCommand::Start => {                let _operation_lock = self.acquire_operation_lock().await?;                self.start().await            }            LifecycleCommand::Restart => {                let _operation_lock = self.acquire_operation_lock().await?;                self.restart().await            }            LifecycleCommand::Stop => {                let _operation_lock = self.acquire_operation_lock().await?;                self.stop().await            }            LifecycleCommand::Version => self.version().await,        }    }    async fn start(&self) -> Result<LifecycleOutput> {        let settings = self.load_settings().await?;        if let Ok(info) = client::probe(&self.socket_path).await {            return Ok(self                .output(                    LifecycleStatus::AlreadyRunning,                    self.running_backend(&settings).await?,                    /*pid*/ None,                    Some(info.app_server_version),                )                .await);        }        if self.running_backend_instance(&settings).await?.is_some() {            let info = self.wait_until_ready().await?;            return Ok(self                .output(                    LifecycleStatus::AlreadyRunning,                    Some(BackendKind::Pid),                    /*pid*/ None,                    Some(info.app_server_version),                )                .await);        }        self.ensure_managed_codex_bin()?;        let pid = self.start_managed_backend(&settings).await?;        let info = self.wait_until_ready().await?;        Ok(self            .output(                LifecycleStatus::Started,                Some(BackendKind::Pid),                pid,                Some(info.app_server_version),            )            .await)    }    async fn restart(&self) -> Result<LifecycleOutput> {        let settings = self.load_settings().await?;        if client::probe(&self.socket_path).await.is_ok()            && self.running_backend(&settings).await?.is_none()        {            return Err(anyhow!(                "app server is running but is not managed by codex app-server daemon"            ));        }        self.ensure_managed_codex_bin()?;        if let Some(backend) = self.running_backend_instance(&settings).await? {            backend.stop().await?;        }        let pid = self.start_managed_backend(&settings).await?;        let info = self.wait_until_ready().await?;        Ok(self            .output(                LifecycleStatus::Restarted,                Some(BackendKind::Pid),                pid,                Some(info.app_server_version),            )            .await)    }    #[cfg(unix)]    pub(crate) async fn try_restart_if_running(        &self,        mode: RestartMode,        updater_refresh_mode: UpdaterRefreshMode,        managed_codex_bin: &Path,    ) -> Result<RestartIfRunningOutcome> {        let operation_lock = self.open_operation_lock_file().await?;        if !try_lock_file(&operation_lock)? {            return Ok(RestartIfRunningOutcome::Busy);        }        let settings = self.load_settings().await?;        let outcome = if let Some(backend) = self.running_backend_instance(&settings).await? {            let info = client::probe(&self.socket_path).await.ok();            let managed_version = if info.is_some() {                Some(managed_codex_version(managed_codex_bin).await?)            } else {                None            };            match restart_decision(mode, info.as_ref(), managed_version.as_deref()) {                RestartDecision::NotReady => return Ok(RestartIfRunningOutcome::NotReady),                RestartDecision::AlreadyCurrent => RestartIfRunningOutcome::AlreadyCurrent,                RestartDecision::Restart => {                    backend.stop().await?;                    let _ = self                        .start_managed_backend_with_bin(&settings, managed_codex_bin)                        .await?;                    self.wait_until_ready().await?;                    RestartIfRunningOutcome::Restarted                }            }        } else if client::probe(&self.socket_path).await.is_ok() {            return Err(anyhow!(                "app server is running but is not managed by codex app-server daemon"            ));        } else {            RestartIfRunningOutcome::NotRunning        };        if should_reexec_updater(updater_refresh_mode, outcome) {            crate::update_loop::reexec_managed_updater(managed_codex_bin)?;        }        Ok(outcome)    }    async fn stop(&self) -> Result<LifecycleOutput> {        let settings = self.load_settings().await?;        if let Some(backend) = self.running_backend_instance(&settings).await? {            backend.stop().await?;            return Ok(self                .output(                    LifecycleStatus::Stopped,                    Some(BackendKind::Pid),                    /*pid*/ None,                    /*app_server_version*/ None,                )                .await);        }        if client::probe(&self.socket_path).await.is_ok() {            return Err(anyhow!(                "app server is running but is not managed by codex app-server daemon"            ));        }        Ok(self            .output(                LifecycleStatus::NotRunning,                /*backend*/ None,                /*pid*/ None,                /*app_server_version*/ None,            )            .await)    }    async fn version(&self) -> Result<LifecycleOutput> {        let settings = self.load_settings().await?;        let info = client::probe(&self.socket_path).await?;        Ok(self            .output(                LifecycleStatus::Running,                self.running_backend(&settings).await?,                /*pid*/ None,                Some(info.app_server_version),            )            .await)    }    async fn wait_until_ready(&self) -> Result<client::ProbeInfo> {        let deadline = tokio::time::Instant::now() + START_TIMEOUT;        loop {            match client::probe(&self.socket_path).await {                Ok(info) => return Ok(info),                Err(err) if tokio::time::Instant::now() < deadline => {                    let _ = err;                    sleep(START_POLL_INTERVAL).await;                }                Err(err) => {                    let context = self.app_server_not_ready_context().await;                    return Err(err).context(context);                }            }        }    }    async fn app_server_not_ready_context(&self) -> String {        let mut context = format!(            "app server did not become ready on {}",            self.socket_path.display()        );        self.append_daemon_app_server_context(&mut context).await;        backend::append_stderr_log_tail_context(&self.pid_file, &mut context).await;        context    }    async fn append_daemon_app_server_context(&self, context: &mut String) {        let managed_codex_version = self            .managed_codex_version_best_effort()            .await            .unwrap_or_else(|| "unknown".to_string());        context.push_str(&format!(            "\n\nDaemon used app-server:\n  path: {}\n  version: {managed_codex_version}",            self.managed_codex_bin.display()        ));    }    async fn bootstrap(&self, options: BootstrapOptions) -> Result<BootstrapOutput> {        let _operation_lock = self.acquire_operation_lock().await?;        self.bootstrap_locked(options).await    }    async fn ensure_remote_control_started(&self) -> Result<RemoteControlStartOutput> {        let _operation_lock = self.acquire_operation_lock().await?;        let settings = self.load_settings().await?;        if self.is_bootstrapped(&settings).await? {            let _ = self                .set_remote_control_locked(RemoteControlMode::Enabled)                .await?;            let output = self.start().await?;            return Ok(RemoteControlStartOutput::Start(output));        }        let output = self            .bootstrap_locked(BootstrapOptions {                remote_control_enabled: true,            })            .await?;        Ok(RemoteControlStartOutput::Bootstrap(output))    }    async fn ensure_remote_control_ready(&self) -> Result<RemoteControlReadyOutput> {        let daemon = self.ensure_remote_control_started().await?;        let remote_control =            remote_control_client::enable_remote_control(&self.socket_path).await?;        Ok(RemoteControlReadyOutput {            daemon,            remote_control,        })    }    async fn set_remote_control(&self, mode: RemoteControlMode) -> Result<RemoteControlOutput> {        let _operation_lock = self.acquire_operation_lock().await?;        self.set_remote_control_locked(mode).await    }    async fn set_remote_control_locked(        &self,        mode: RemoteControlMode,    ) -> Result<RemoteControlOutput> {        let previous_settings = self.load_settings().await?;        let mut settings = previous_settings.clone();        let remote_control_enabled = mode.is_enabled();        let backend = self.running_backend_instance(&previous_settings).await?;        if backend.is_none() && client::probe(&self.socket_path).await.is_ok() {            return Err(anyhow!(                "app server is running but is not managed by codex app-server daemon"            ));        }        if settings.remote_control_enabled == remote_control_enabled {            let info = if backend.is_some() {                Some(self.wait_until_ready().await?)            } else {                None            };            if info.is_some() {                match mode {                    RemoteControlMode::Enabled => {                        remote_control_client::enable_remote_control(&self.socket_path).await?;                    }                    RemoteControlMode::Disabled => {                        remote_control_client::disable_remote_control(&self.socket_path).await?;                    }                }            }            return Ok(self.remote_control_output(                already_remote_control_status(mode),                backend.map(|_| BackendKind::Pid),                remote_control_enabled,                info.map(|info| info.app_server_version),            ));        }        settings.remote_control_enabled = remote_control_enabled;        settings.save(&self.settings_file).await?;        let app_server_version = if let Some(backend) = backend {            self.ensure_managed_codex_bin()?;            backend.stop().await?;            let _ = self.start_managed_backend(&settings).await?;            Some(self.wait_until_ready().await?.app_server_version)        } else {            None        };        Ok(self.remote_control_output(            remote_control_status(mode),            app_server_version.as_ref().map(|_| BackendKind::Pid),            remote_control_enabled,            app_server_version,        ))    }    async fn bootstrap_locked(&self, options: BootstrapOptions) -> Result<BootstrapOutput> {        self.ensure_managed_codex_bin()?;        let settings = DaemonSettings {            remote_control_enabled: options.remote_control_enabled,        };        if client::probe(&self.socket_path).await.is_ok()            && self.running_backend(&settings).await?.is_none()        {            return Err(anyhow!(                "app server is running but is not managed by codex app-server daemon"            ));        }        settings.save(&self.settings_file).await?;        if let Some(backend) = self.running_backend_instance(&settings).await? {            backend.stop().await?;        }        let backend = backend::pid_backend(self.backend_paths(&settings));        backend.start().await?;        let updater = backend::pid_update_loop_backend(self.backend_paths(&settings));        if updater.is_starting_or_running().await? {            updater.stop().await?;        }        updater.start().await?;        let info = self.wait_until_ready().await?;        let managed_codex_version = self.managed_codex_version_best_effort().await;        Ok(BootstrapOutput {            status: BootstrapStatus::Bootstrapped,            backend: BackendKind::Pid,            auto_update_enabled: true,            remote_control_enabled: settings.remote_control_enabled,            managed_codex_path: self.managed_codex_bin.clone(),            managed_codex_version,            socket_path: self.socket_path.clone(),            cli_version: env!("CARGO_PKG_VERSION").to_string(),            app_server_version: info.app_server_version,        })    }    async fn running_backend(&self, settings: &DaemonSettings) -> Result<Option<BackendKind>> {        Ok(self            .running_backend_instance(settings)            .await?            .map(|_| BackendKind::Pid))    }    async fn running_backend_instance(        &self,        settings: &DaemonSettings,    ) -> Result<Option<backend::PidBackend>> {        let backend = backend::pid_backend(self.backend_paths(settings));        if backend.is_starting_or_running().await? {            return Ok(Some(backend));        }        Ok(None)    }    async fn start_managed_backend(&self, settings: &DaemonSettings) -> Result<Option<u32>> {        self.start_managed_backend_with_bin(settings, &self.managed_codex_bin)            .await    }    async fn start_managed_backend_with_bin(        &self,        settings: &DaemonSettings,        managed_codex_bin: &Path,    ) -> Result<Option<u32>> {        let backend =            backend::pid_backend(self.backend_paths_with_bin(settings, managed_codex_bin));        backend.start().await    }    async fn is_bootstrapped(&self, settings: &DaemonSettings) -> Result<bool> {        let updater = backend::pid_update_loop_backend(self.backend_paths(settings));        updater.is_starting_or_running().await    }    fn ensure_managed_codex_bin(&self) -> Result<()> {        if self.managed_codex_bin.is_file() {            return Ok(());        }        let managed_codex_path = self.managed_codex_bin.display();        Err(anyhow!(            "managed standalone Codex install not found at {managed_codex_path}\n\n\             This command requires the standalone install managed by the Codex installer, because \             the daemon starts and updates app-server from that fixed path.\n\n\             Install it with:\n  curl -fsSL https://chatgpt.com/codex/install.sh | sh\n\n\             Then rerun the command you just tried."        ))    }    #[cfg(unix)]    async fn managed_codex_version_best_effort(&self) -> Option<String> {        managed_codex_version(&self.managed_codex_bin).await.ok()    }    #[cfg(not(unix))]    async fn managed_codex_version_best_effort(&self) -> Option<String> {        None    }    fn backend_paths(&self, settings: &DaemonSettings) -> BackendPaths {        self.backend_paths_with_bin(settings, &self.managed_codex_bin)    }    fn backend_paths_with_bin(        &self,        settings: &DaemonSettings,        managed_codex_bin: &Path,    ) -> BackendPaths {        BackendPaths {            codex_bin: managed_codex_bin.to_path_buf(),            pid_file: self.pid_file.clone(),            update_pid_file: self.update_pid_file.clone(),            remote_control_enabled: settings.remote_control_enabled,        }    }    async fn load_settings(&self) -> Result<DaemonSettings> {        DaemonSettings::load(&self.settings_file).await    }    async fn acquire_operation_lock(&self) -> Result<tokio::fs::File> {        let operation_lock = self.open_operation_lock_file().await?;        let deadline = tokio::time::Instant::now() + OPERATION_LOCK_TIMEOUT;        while !try_lock_file(&operation_lock)? {            if tokio::time::Instant::now() >= deadline {                return Err(anyhow!(                    "timed out waiting for daemon operation lock {}",                    self.operation_lock_file.display()                ));            }            sleep(START_POLL_INTERVAL).await;        }        Ok(operation_lock)    }    async fn open_operation_lock_file(&self) -> Result<tokio::fs::File> {        if let Some(parent) = self.operation_lock_file.parent() {            tokio::fs::create_dir_all(parent).await.with_context(|| {                format!(                    "failed to create daemon state directory {}",                    parent.display()                )            })?;        }        tokio::fs::OpenOptions::new()            .create(true)            .truncate(false)            .write(true)            .open(&self.operation_lock_file)            .await            .with_context(|| {                format!(                    "failed to open daemon operation lock {}",                    self.operation_lock_file.display()                )            })    }    async fn output(        &self,        status: LifecycleStatus,        backend: Option<BackendKind>,        pid: Option<u32>,        app_server_version: Option<String>,    ) -> LifecycleOutput {        let managed_codex_version = self.managed_codex_version_best_effort().await;        LifecycleOutput {            status,            backend,            pid,            managed_codex_path: self.managed_codex_bin.clone(),            managed_codex_version,            socket_path: self.socket_path.clone(),            cli_version: Some(env!("CARGO_PKG_VERSION").to_string()),            app_server_version,        }    }    fn remote_control_output(        &self,        status: RemoteControlStatus,        backend: Option<BackendKind>,        remote_control_enabled: bool,        app_server_version: Option<String>,    ) -> RemoteControlOutput {        RemoteControlOutput {            status,            backend,            remote_control_enabled,            socket_path: self.socket_path.clone(),            cli_version: env!("CARGO_PKG_VERSION").to_string(),            app_server_version,        }    }}fn remote_control_status(mode: RemoteControlMode) -> RemoteControlStatus {    match mode {        RemoteControlMode::Enabled => RemoteControlStatus::Enabled,        RemoteControlMode::Disabled => RemoteControlStatus::Disabled,    }}fn already_remote_control_status(mode: RemoteControlMode) -> RemoteControlStatus {    match mode {        RemoteControlMode::Enabled => RemoteControlStatus::AlreadyEnabled,        RemoteControlMode::Disabled => RemoteControlStatus::AlreadyDisabled,    }}#[cfg(unix)]fn restart_decision(    mode: RestartMode,    info: Option<&client::ProbeInfo>,    managed_version: Option<&str>,) -> RestartDecision {    match (mode, info, managed_version) {        (RestartMode::IfVersionChanged, None, _) => RestartDecision::NotReady,        (RestartMode::IfVersionChanged, Some(info), Some(managed_version))            if info.app_server_version == managed_version =>        {            RestartDecision::AlreadyCurrent        }        _ => RestartDecision::Restart,    }}#[cfg(unix)]fn should_reexec_updater(    updater_refresh_mode: UpdaterRefreshMode,    outcome: RestartIfRunningOutcome,) -> bool {    updater_refresh_mode == UpdaterRefreshMode::ReexecIfManagedBinaryChanged        && outcome == RestartIfRunningOutcome::Restarted}#[cfg(unix)]fn try_lock_file(file: &tokio::fs::File) -> Result<bool> {    use std::os::fd::AsRawFd;    let result = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) };    if result == 0 {        return Ok(true);    }    let err = std::io::Error::last_os_error();    if err.raw_os_error() == Some(libc::EWOULDBLOCK) {        return Ok(false);    }    Err(err).context("failed to lock daemon operation")}#[cfg(not(unix))]fn try_lock_file(_file: &tokio::fs::File) -> Result<bool> {    Ok(true)}#[cfg(all(test, unix))]mod tests {    use pretty_assertions::assert_eq;    use tempfile::TempDir;    use super::BackendKind;    use super::BootstrapOutput;    use super::BootstrapStatus;    use super::Daemon;    use super::LifecycleOutput;    use super::LifecycleStatus;    use super::RemoteControlStartOutput;    use super::RemoteControlStatus;    use super::RestartDecision;    use super::RestartIfRunningOutcome;    use super::RestartMode;    use super::UpdaterRefreshMode;    use super::restart_decision;    use super::should_reexec_updater;    use crate::client::ProbeInfo;    #[test]    fn remote_control_status_uses_camel_case_json() {        assert_eq!(            serde_json::to_string(&RemoteControlStatus::AlreadyEnabled).expect("serialize"),            "\"alreadyEnabled\""        );    }    #[test]    fn updater_reexec_waits_for_validated_restart() {        assert_eq!(            [                RestartIfRunningOutcome::Busy,                RestartIfRunningOutcome::NotReady,                RestartIfRunningOutcome::AlreadyCurrent,                RestartIfRunningOutcome::NotRunning,                RestartIfRunningOutcome::Restarted,            ]            .map(|outcome| {                should_reexec_updater(UpdaterRefreshMode::ReexecIfManagedBinaryChanged, outcome)            }),            [false, false, false, false, true]        );    }    #[test]    fn unchanged_updater_never_reexecs() {        assert_eq!(            [                RestartIfRunningOutcome::Busy,                RestartIfRunningOutcome::NotReady,                RestartIfRunningOutcome::AlreadyCurrent,                RestartIfRunningOutcome::NotRunning,                RestartIfRunningOutcome::Restarted,            ]            .map(|outcome| should_reexec_updater(UpdaterRefreshMode::None, outcome)),            [false, false, false, false, false]        );    }    #[test]    fn restart_decision_preserves_forced_refreshes() {        let current_info = ProbeInfo {            app_server_version: "0.1.0".to_string(),        };        assert_eq!(            [                restart_decision(                    RestartMode::IfVersionChanged,                    Some(&current_info),                    Some("0.1.0"),                ),                restart_decision(                    RestartMode::IfVersionChanged,                    /*info*/ None,                    /*managed_version*/ None,                ),                restart_decision(RestartMode::Always, Some(&current_info), Some("0.1.0")),                restart_decision(                    RestartMode::Always,                    /*info*/ None,                    /*managed_version*/ None,                ),            ],            [                RestartDecision::AlreadyCurrent,                RestartDecision::NotReady,                RestartDecision::Restart,                RestartDecision::Restart,            ]        );    }    #[test]    fn remote_control_start_output_serializes_inner_output_without_tag() {        let lifecycle_output = LifecycleOutput {            status: LifecycleStatus::AlreadyRunning,            backend: Some(BackendKind::Pid),            pid: None,            managed_codex_path: "codex".into(),            managed_codex_version: Some("1.2.3".to_string()),            socket_path: "codex.sock".into(),            cli_version: Some("1.2.3".to_string()),            app_server_version: Some("1.2.4".to_string()),        };        let output = RemoteControlStartOutput::Start(lifecycle_output.clone());        assert_eq!(            serde_json::to_value(&lifecycle_output).expect("serialize"),            serde_json::json!({                "status": "alreadyRunning",                "backend": "pid",                "managedCodexPath": "codex",                "managedCodexVersion": "1.2.3",                "socketPath": "codex.sock",                "cliVersion": "1.2.3",                "appServerVersion": "1.2.4",            })        );        assert_eq!(            serde_json::to_value(output).expect("serialize"),            serde_json::to_value(lifecycle_output).expect("serialize")        );        let bootstrap_output = BootstrapOutput {            status: BootstrapStatus::Bootstrapped,            backend: BackendKind::Pid,            auto_update_enabled: true,            remote_control_enabled: true,            managed_codex_path: "codex".into(),            managed_codex_version: Some("1.2.3".to_string()),            socket_path: "codex.sock".into(),            cli_version: "1.2.3".to_string(),            app_server_version: "1.2.4".to_string(),        };        let output = RemoteControlStartOutput::Bootstrap(bootstrap_output.clone());        assert_eq!(            serde_json::to_value(&bootstrap_output).expect("serialize"),            serde_json::json!({                "status": "bootstrapped",                "backend": "pid",                "autoUpdateEnabled": true,                "remoteControlEnabled": true,                "managedCodexPath": "codex",                "managedCodexVersion": "1.2.3",                "socketPath": "codex.sock",                "cliVersion": "1.2.3",                "appServerVersion": "1.2.4",            })        );        assert_eq!(            serde_json::to_value(output).expect("serialize"),            serde_json::to_value(bootstrap_output).expect("serialize")        );    }    #[tokio::test]    async fn not_ready_context_reports_daemon_app_server_before_stderr() {        let temp_dir = TempDir::new().expect("temp dir");        let daemon = Daemon {            socket_path: temp_dir.path().join("app-server-control.sock"),            pid_file: temp_dir.path().join("app-server.pid"),            update_pid_file: temp_dir.path().join("app-server-updater.pid"),            operation_lock_file: temp_dir.path().join("daemon.lock"),            settings_file: temp_dir.path().join("settings.json"),            managed_codex_bin: temp_dir.path().join("missing-codex"),        };        let stderr_log = daemon.pid_file.with_extension("stderr.log");        tokio::fs::write(&stderr_log, "unexpected argument")            .await            .expect("write stderr log");        assert_eq!(            daemon.app_server_not_ready_context().await,            format!(                "app server did not become ready on {}\n\n\                 Daemon used app-server:\n  path: {}\n  version: unknown\n\n\                 Managed app-server stderr ({}):\n  unexpected argument",                daemon.socket_path.display(),                daemon.managed_codex_bin.display(),                stderr_log.display()            )        );    }}