| #![cfg(unix)] |
|
|
| use std::path::PathBuf; |
| use std::process::Child; |
| use std::process::Command; |
| use std::process::Stdio; |
| use std::time::Duration; |
| use std::time::Instant; |
|
|
| use anyhow::Context; |
| use anyhow::Result; |
| use anyhow::ensure; |
| use pretty_assertions::assert_eq; |
| use serde_json::Value; |
| use tempfile::TempDir; |
|
|
| struct TestDaemon { |
| home: TempDir, |
| codex: PathBuf, |
| unmanaged: Option<Child>, |
| } |
|
|
| impl TestDaemon { |
| fn new() -> Result<Self> { |
| let home = tempfile::Builder::new().tempdir_in("/tmp")?; |
| let codex = codex_utils_cargo_bin::cargo_bin("codex")?; |
| let codex_source = std::fs::canonicalize(&codex)?; |
| let target = if cfg!(target_os = "macos") { |
| format!("{}-apple-darwin", std::env::consts::ARCH) |
| } else { |
| format!("{}-unknown-linux-musl", std::env::consts::ARCH) |
| }; |
| let standalone = home.path().join("packages/standalone"); |
| let release_name = format!("0.0.0-{target}"); |
| let managed = standalone |
| .join("releases") |
| .join(&release_name) |
| .join("bin/codex"); |
| std::fs::create_dir_all(managed.parent().context("managed bin parent")?)?; |
| std::fs::hard_link(&codex_source, &managed) |
| .or_else(|_| std::fs::copy(&codex_source, managed).map(|_| ()))?; |
| std::fs::write(standalone.join("auto-update-version"), &release_name)?; |
| std::os::unix::fs::symlink( |
| PathBuf::from("releases").join(release_name), |
| standalone.join("current"), |
| )?; |
| |
| let state = home.path().join("app-server-daemon"); |
| std::fs::create_dir(&state)?; |
| std::fs::write(state.join("app-server.stderr.log"), b"")?; |
| Ok(Self { |
| home, |
| codex, |
| unmanaged: None, |
| }) |
| } |
|
|
| fn command(&self) -> Command { |
| let mut command = Command::new(&self.codex); |
| command.env("CODEX_HOME", self.home.path()); |
| command |
| } |
|
|
| fn lifecycle(&self, action: &str) -> Result<Value> { |
| let output = self |
| .command() |
| .args(["app-server", "daemon", action]) |
| .output()?; |
| ensure!( |
| output.status.success(), |
| "daemon {action} failed: {}", |
| String::from_utf8_lossy(&output.stderr) |
| ); |
| Ok(serde_json::from_slice(&output.stdout)?) |
| } |
|
|
| fn pid(&self, name: &str) -> Result<u32> { |
| let record = std::fs::read(self.home.path().join("app-server-daemon").join(name)) |
| .with_context(|| format!("failed to read {name}"))?; |
| Ok(serde_json::from_slice::<Value>(&record)?["pid"] |
| .as_u64() |
| .context("pid missing")? as u32) |
| } |
| } |
|
|
| impl Drop for TestDaemon { |
| fn drop(&mut self) { |
| if let Some(mut child) = self.unmanaged.take() { |
| let _ = child.kill(); |
| let _ = child.wait(); |
| } |
| let _ = self.lifecycle("stop"); |
| if let Ok(pid) = self.pid("app-server-updater.pid") { |
| let _ = signal(pid, libc::SIGTERM); |
| } |
| } |
| } |
|
|
| fn signal(pid: u32, signal: libc::c_int) -> Result<()> { |
| let raw_pid = libc::pid_t::try_from(pid).context("pid out of range")?; |
| ensure!( |
| unsafe { libc::kill(raw_pid, signal) } == 0, |
| "failed to signal pid {pid}: {}", |
| std::io::Error::last_os_error() |
| ); |
| Ok(()) |
| } |
|
|
| fn wait_for_exit(pid: u32) -> Result<()> { |
| let deadline = Instant::now() + Duration::from_secs(30); |
| loop { |
| let output = Command::new("/bin/ps") |
| .args(["-p", &pid.to_string(), "-o", "stat="]) |
| .output() |
| .context("failed to invoke ps")?; |
| let state = String::from_utf8_lossy(&output.stdout); |
| if !output.status.success() || state.trim().starts_with('Z') { |
| return Ok(()); |
| } |
| ensure!(Instant::now() < deadline, "pid {pid} did not exit: {state}"); |
| std::thread::sleep(Duration::from_millis(50)); |
| } |
| } |
|
|
| #[cfg(any(target_os = "linux", target_os = "macos"))] |
| #[test] |
| fn managed_identity_survives_locale_and_timezone_changes() -> Result<()> { |
| let daemon = TestDaemon::new()?; |
| let mut original = Vec::new(); |
| let mut updater_pid = 0; |
| let pid_file = daemon.home.path().join("app-server-daemon/app-server.pid"); |
| for (action, locale, timezone, expected_status) in [ |
| ("start", "C", "UTC0", "started"), |
| ("version", "en_AU.UTF-8", "PST8PDT", "running"), |
| ("restart", "en_AU.UTF-8", "PST8PDT", "restarted"), |
| ("stop", "C", "UTC0", "stopped"), |
| ] { |
| let output = daemon |
| .command() |
| .args(["app-server", "daemon", action]) |
| .env("LC_ALL", locale) |
| .env("TZ", timezone) |
| .output()?; |
| ensure!( |
| output.status.success(), |
| "daemon {action} failed: {}", |
| String::from_utf8_lossy(&output.stderr) |
| ); |
| let output: Value = serde_json::from_slice(&output.stdout)?; |
| assert_eq!(output["status"], expected_status); |
| if action == "start" { |
| updater_pid = daemon.pid("app-server-updater.pid")?; |
| original = std::fs::read(&pid_file)?; |
| let record: Value = serde_json::from_slice(&original)?; |
| assert!(record["processIdentity"].is_object()); |
| |
| let legacy = Command::new("ps") |
| .args([ |
| "-p", |
| &daemon.pid("app-server.pid")?.to_string(), |
| "-o", |
| "lstart=", |
| ]) |
| .env("LC_ALL", locale) |
| .env("TZ", timezone) |
| .output()?; |
| ensure!(legacy.status.success(), "failed to read legacy start time"); |
| assert_eq!( |
| record["processStartTime"], |
| String::from_utf8(legacy.stdout)?.trim() |
| ); |
| } else if action == "version" { |
| assert_eq!(output["backend"], "pid"); |
| assert_eq!(daemon.pid("app-server-updater.pid")?, updater_pid); |
| assert_eq!(std::fs::read(&pid_file)?, original); |
|
|
| |
| let updater_socket = daemon |
| .home |
| .path() |
| .join("app-server-daemon/app-server-updater.sock"); |
| let deadline = Instant::now() + Duration::from_secs(30); |
| while !updater_socket.exists() { |
| ensure!(Instant::now() < deadline, "updater did not become ready"); |
| std::thread::sleep(Duration::from_millis(50)); |
| } |
| let mut legacy: Value = serde_json::from_slice(&original)?; |
| legacy |
| .as_object_mut() |
| .context("PID record object")? |
| .remove("processIdentity"); |
| let legacy_bytes = serde_json::to_vec(&legacy)?; |
| std::fs::write(&pid_file, &legacy_bytes)?; |
| let result = daemon |
| .command() |
| .args(["app-server", "daemon", "version"]) |
| .env("LC_ALL", locale) |
| .env("TZ", timezone) |
| .output(); |
| let preserved = std::fs::read(&pid_file); |
| |
| std::fs::write(&pid_file, &original)?; |
| let result = result?; |
| assert!(!result.status.success()); |
| assert_eq!(preserved?, legacy_bytes); |
| let stderr = |
| String::from_utf8(result.stderr)?.replace(&legacy["pid"].to_string(), "[PID]"); |
| insta::assert_snapshot!(stderr, @"Error: cannot verify pid-managed process [PID]: legacy start time changed; PID record retained. Retry with the locale and timezone used to start the daemon. If the system clock changed, stop the original process before restarting the daemon"); |
| } |
| if action == "restart" { |
| assert_eq!(daemon.pid("app-server-updater.pid")?, updater_pid); |
| } |
| } |
| assert!(!pid_file.exists()); |
| Ok(()) |
| } |
|
|
| #[test] |
| fn package_ownership_check_does_not_start_an_updater() -> Result<()> { |
| let daemon = TestDaemon::new()?; |
| let output = daemon |
| .command() |
| .args([ |
| "app-server", |
| "daemon", |
| "pid-update-loop", |
| "--check-package-ownership", |
| ]) |
| .output()?; |
| ensure!( |
| output.status.success(), |
| "ownership check failed: {}", |
| String::from_utf8_lossy(&output.stderr) |
| ); |
| let state = daemon.home.path().join("app-server-daemon"); |
| assert!(!state.join("app-server-updater.pid").exists()); |
| assert!(!state.join("daemon-updater.pid").exists()); |
| Ok(()) |
| } |
|
|
| #[test] |
| fn managed_starts_ensure_one_updater_and_recover_a_missing_one() -> Result<()> { |
| let daemon = TestDaemon::new()?; |
| assert_eq!(daemon.lifecycle("start")?["status"], "started"); |
| let backend_pid = daemon.pid("app-server.pid")?; |
| let updater_pid = daemon.pid("app-server-updater.pid")?; |
| assert_ne!(backend_pid, updater_pid); |
|
|
| assert_eq!(daemon.lifecycle("start")?["status"], "alreadyRunning"); |
| assert_eq!(daemon.pid("app-server.pid")?, backend_pid); |
| assert_eq!(daemon.pid("app-server-updater.pid")?, updater_pid); |
|
|
| signal(updater_pid, libc::SIGTERM)?; |
| wait_for_exit(updater_pid)?; |
| |
| let server_record_path = daemon.home.path().join("app-server-daemon/app-server.pid"); |
| let mut legacy: Value = serde_json::from_slice(&std::fs::read(&server_record_path)?)?; |
| let native = legacy.as_object_mut().unwrap().remove("processIdentity"); |
| std::fs::write(&server_record_path, serde_json::to_vec(&legacy)?)?; |
| assert_eq!(daemon.lifecycle("start")?["status"], "alreadyRunning"); |
| assert_eq!(daemon.pid("app-server.pid")?, backend_pid); |
| let replacement_pid = daemon.pid("app-server-updater.pid")?; |
| assert_ne!(replacement_pid, updater_pid); |
| if let Some(native) = native { |
| legacy["processIdentity"] = native; |
| let deadline = Instant::now() + Duration::from_secs(30); |
| loop { |
| let record: Value = serde_json::from_slice(&std::fs::read(&server_record_path)?)?; |
| if record == legacy { |
| break; |
| } |
| ensure!( |
| Instant::now() < deadline, |
| "legacy record was not upgraded: {record}" |
| ); |
| std::thread::sleep(Duration::from_millis(50)); |
| } |
| } |
| assert_eq!(daemon.lifecycle("restart")?["status"], "restarted"); |
| assert_ne!(daemon.pid("app-server.pid")?, backend_pid); |
| assert_eq!(daemon.pid("app-server-updater.pid")?, replacement_pid); |
| daemon.lifecycle("stop")?; |
| assert!( |
| !daemon |
| .home |
| .path() |
| .join("app-server-daemon/app-server.pid") |
| .exists() |
| ); |
| assert_eq!(daemon.lifecycle("start")?["status"], "started"); |
| assert!( |
| !daemon |
| .home |
| .path() |
| .join("packages/app-server-daemon") |
| .exists() |
| ); |
| Ok(()) |
| } |
|
|
| #[test] |
| fn managed_start_succeeds_when_updater_record_is_invalid() -> Result<()> { |
| let daemon = TestDaemon::new()?; |
| let state_dir = daemon.home.path().join("app-server-daemon"); |
| std::fs::create_dir_all(&state_dir)?; |
| std::fs::write(state_dir.join("app-server-updater.pid"), "not a PID record")?; |
|
|
| assert_eq!(daemon.lifecycle("start")?["status"], "started"); |
| let server_pid = daemon.pid("app-server.pid")?; |
| assert_eq!(daemon.lifecycle("start")?["status"], "alreadyRunning"); |
| assert_eq!(daemon.pid("app-server.pid")?, server_pid); |
| Ok(()) |
| } |
|
|
| #[cfg(any(target_os = "linux", target_os = "macos"))] |
| #[test] |
| fn wall_clock_shift_keeps_server_and_updater_managed() -> Result<()> { |
| let daemon = TestDaemon::new()?; |
| assert_eq!(daemon.lifecycle("start")?["status"], "started"); |
| let server_pid = daemon.pid("app-server.pid")?; |
| let updater_pid = daemon.pid("app-server-updater.pid")?; |
| for name in ["app-server.pid", "app-server-updater.pid"] { |
| let path = daemon.home.path().join("app-server-daemon").join(name); |
| let mut record: Value = serde_json::from_slice(&std::fs::read(&path)?)?; |
| record["processStartTime"] = "historical wall-clock start time".into(); |
| let replacement = path.with_extension("replacement"); |
| std::fs::write(&replacement, serde_json::to_vec(&record)?)?; |
| std::fs::rename(replacement, path)?; |
| } |
|
|
| let version = daemon.lifecycle("version")?; |
| assert_eq!(version["status"], "running"); |
| assert_eq!(version["backend"], "pid"); |
| assert_eq!(daemon.lifecycle("start")?["status"], "alreadyRunning"); |
| assert_eq!(daemon.pid("app-server.pid")?, server_pid); |
| assert_eq!(daemon.pid("app-server-updater.pid")?, updater_pid); |
| assert_eq!(daemon.lifecycle("restart")?["status"], "restarted"); |
| wait_for_exit(server_pid)?; |
| assert_ne!(daemon.pid("app-server.pid")?, server_pid); |
| assert_eq!(daemon.pid("app-server-updater.pid")?, updater_pid); |
| Ok(()) |
| } |
|
|
| #[test] |
| fn managed_start_keeps_updater_on_marker_mismatch_but_stops_it_for_pin() -> Result<()> { |
| let daemon = TestDaemon::new()?; |
| assert_eq!(daemon.lifecycle("start")?["status"], "started"); |
| let updater_pid = daemon.pid("app-server-updater.pid")?; |
| let marker = daemon |
| .home |
| .path() |
| .join("packages/standalone/auto-update-version"); |
| std::fs::write(&marker, "0.1.0-other-target")?; |
| assert_eq!(daemon.lifecycle("start")?["status"], "alreadyRunning"); |
| assert_eq!(daemon.pid("app-server-updater.pid")?, updater_pid); |
| std::thread::sleep(Duration::from_millis(250)); |
| let updater_state = Command::new("/bin/ps") |
| .args(["-p", &updater_pid.to_string(), "-o", "stat="]) |
| .output()?; |
| ensure!( |
| updater_state.status.success() |
| && !String::from_utf8_lossy(&updater_state.stdout) |
| .trim() |
| .starts_with('Z'), |
| "updater exited during the latest marker transition" |
| ); |
|
|
| std::fs::remove_file(marker)?; |
|
|
| assert_eq!(daemon.lifecycle("start")?["status"], "alreadyRunning"); |
| wait_for_exit(updater_pid)?; |
| assert!( |
| !daemon |
| .home |
| .path() |
| .join("app-server-daemon/app-server-updater.pid") |
| .exists() |
| ); |
| Ok(()) |
| } |
|
|
| #[test] |
| fn restart_applies_saved_updater_preference() -> Result<()> { |
| let daemon = TestDaemon::new()?; |
| assert_eq!(daemon.lifecycle("start")?["status"], "started"); |
| let updater_pid = daemon.pid("app-server-updater.pid")?; |
| let settings = daemon.home.path().join("app-server-daemon/settings.json"); |
| std::fs::write( |
| &settings, |
| serde_json::to_vec(&serde_json::json!({ |
| "updater": {"autoUpdateEnabled": false, "updateIntervalMinutes": 2}, |
| }))?, |
| )?; |
| assert_eq!(daemon.lifecycle("restart")?["status"], "restarted"); |
| wait_for_exit(updater_pid)?; |
| assert!(daemon.pid("app-server-updater.pid").is_err()); |
| assert_eq!(daemon.lifecycle("bootstrap")?["autoUpdateEnabled"], false); |
| assert!(daemon.pid("app-server-updater.pid").is_err()); |
|
|
| std::fs::write( |
| &settings, |
| serde_json::to_vec(&serde_json::json!({ |
| "updater": {"autoUpdateEnabled": true, "updateIntervalMinutes": 2}, |
| }))?, |
| )?; |
| assert_eq!(daemon.lifecycle("restart")?["status"], "restarted"); |
| assert_ne!(daemon.pid("app-server-updater.pid")?, updater_pid); |
|
|
| std::fs::write(&settings, "{malformed")?; |
| assert_eq!(daemon.lifecycle("stop")?["status"], "stopped"); |
| Ok(()) |
| } |
|
|
| #[test] |
| fn unmanaged_app_server_does_not_launch_updater() -> Result<()> { |
| let mut daemon = TestDaemon::new()?; |
| daemon.unmanaged = Some( |
| daemon |
| .command() |
| .args(["app-server", "--listen", "unix://"]) |
| .stdin(Stdio::null()) |
| .stdout(Stdio::null()) |
| .stderr(Stdio::null()) |
| .spawn()?, |
| ); |
| let deadline = Instant::now() + Duration::from_secs(30); |
| while !daemon |
| .command() |
| .args(["app-server", "daemon", "version"]) |
| .output()? |
| .status |
| .success() |
| { |
| ensure!(Instant::now() < deadline, "app server did not become ready"); |
| std::thread::sleep(Duration::from_millis(50)); |
| } |
|
|
| let output = daemon.lifecycle("start")?; |
| assert_eq!(output["status"], "alreadyRunning"); |
| assert_eq!(output["backend"], Value::Null); |
| assert_eq!(daemon.lifecycle("update")?["status"], "unsupported"); |
| assert!( |
| !daemon |
| .home |
| .path() |
| .join("app-server-daemon/app-server-updater.pid") |
| .exists() |
| ); |
| Ok(()) |
| } |
|
|
| #[test] |
| fn manual_update_rejects_an_unowned_installation() -> Result<()> { |
| let daemon = TestDaemon::new()?; |
| std::fs::remove_file( |
| daemon |
| .home |
| .path() |
| .join("packages/standalone/current/bin/codex"), |
| )?; |
|
|
| assert_eq!(daemon.lifecycle("update")?["status"], "unsupported"); |
| assert!(daemon.pid("app-server.pid").is_err()); |
| assert!(daemon.pid("app-server-updater.pid").is_err()); |
| Ok(()) |
| } |
|
|
| #[derive(Clone, Copy, PartialEq, Eq)] |
| enum InitialDaemon { |
| Missing, |
| Legacy, |
| } |
|
|
| fn packaged_daemon_launch(action: &str, initial: InitialDaemon) -> Result<()> { |
| use std::os::unix::fs::PermissionsExt; |
| let mut daemon = TestDaemon::new()?; |
| let standalone = daemon.home.path().join("packages/standalone"); |
| let package = if action == "start" && initial == InitialDaemon::Missing { |
| standalone.join("releases/caller") |
| } else { |
| daemon.home.path().join("cli-package") |
| }; |
| for directory in ["bin", "codex-path", "codex-resources"] { |
| std::fs::create_dir_all(package.join(directory))?; |
| } |
| std::fs::copy(&daemon.codex, package.join("bin/codex"))?; |
| daemon.codex = package.join("bin/codex"); |
| for helper in [ |
| "bin/codex-code-mode-host", |
| "codex-path/rg", |
| "codex-resources/bwrap", |
| ] { |
| std::fs::write(package.join(helper), b"runtime fixture")?; |
| std::fs::set_permissions(package.join(helper), std::fs::Permissions::from_mode(0o755))?; |
| } |
| let target = format!( |
| "{}-{}", |
| std::env::consts::ARCH, |
| if cfg!(target_os = "macos") { |
| "apple-darwin" |
| } else if cfg!(target_env = "gnu") { |
| "unknown-linux-gnu" |
| } else { |
| "unknown-linux-musl" |
| } |
| ); |
| std::fs::write( |
| package.join("codex-package.json"), |
| serde_json::to_vec(&serde_json::json!({ |
| "version": env!("CARGO_PKG_VERSION"), "target": target, "entrypoint": "bin/codex" |
| }))?, |
| )?; |
| if action == "start" && initial == InitialDaemon::Missing { |
| std::fs::remove_file(standalone.join("current"))?; |
| std::os::unix::fs::symlink(&package, standalone.join("current"))?; |
| } |
| let cli_selection = standalone.join("current").canonicalize()?; |
| let state = daemon.home.path().join("app-server-daemon"); |
| if initial == InitialDaemon::Missing { |
| std::fs::remove_file(state.join("app-server.stderr.log"))?; |
| } |
| std::fs::write( |
| state.join("settings.json"), |
| br#"{"shutdownGraceSeconds":0}"#, |
| )?; |
| let cli_before = daemon.codex.canonicalize()?; |
| let result = daemon |
| .command() |
| .args(["app-server", "daemon", action]) |
| .output()?; |
| ensure!( |
| result.status.success(), |
| "{}", |
| String::from_utf8_lossy(&result.stderr) |
| ); |
| assert_eq!( |
| String::from_utf8_lossy(&result.stderr).contains("Installing daemon from CLI version"), |
| initial == InitialDaemon::Missing |
| ); |
| let output: Value = serde_json::from_slice(&result.stdout)?; |
| let dedicated = daemon |
| .home |
| .path() |
| .canonicalize()? |
| .join("packages/app-server-daemon"); |
| let (initial_root, initial_pid_file) = match initial { |
| InitialDaemon::Missing => (&dedicated, "daemon.pid"), |
| InitialDaemon::Legacy => (&standalone, "app-server.pid"), |
| }; |
| assert_eq!( |
| output["managedCodexPath"], |
| initial_root |
| .canonicalize()? |
| .join("current/bin/codex") |
| .to_str() |
| .context("managed path is not UTF-8")? |
| ); |
| assert_eq!(daemon.codex.canonicalize()?, cli_before); |
| assert_eq!(standalone.join("current").canonicalize()?, cli_selection); |
| assert!(state.join(initial_pid_file).exists()); |
| let legacy_updater = match initial { |
| InitialDaemon::Missing => { |
| assert!(!state.join("app-server-updater.pid").exists()); |
| None |
| } |
| InitialDaemon::Legacy => Some(daemon.pid("app-server-updater.pid")?), |
| }; |
| if action == "start" { |
| let initial_current = initial_root.join("current"); |
| let current = dedicated.join("current"); |
| let original = initial_current.canonicalize()?; |
| let original_pid = daemon.pid(initial_pid_file)?; |
| let settings_before = std::fs::read(state.join("settings.json"))?; |
| let refused = daemon |
| .command() |
| .args(["app-server", "daemon", "update", "--from-cli"]) |
| .output()?; |
| assert!(!refused.status.success()); |
| assert!(String::from_utf8_lossy(&refused.stderr).contains("rerun with --yes")); |
| assert_eq!(daemon.pid(initial_pid_file)?, original_pid); |
| assert_eq!(initial_current.canonicalize()?, original); |
| if initial == InitialDaemon::Legacy { |
| assert!(!current.exists()); |
| } |
|
|
| std::fs::remove_file(package.join("bin/codex-code-mode-host"))?; |
| let invalid = daemon |
| .command() |
| .args(["app-server", "daemon", "update", "--from-cli", "--yes"]) |
| .output()?; |
| assert!(!invalid.status.success()); |
| assert_eq!(daemon.pid(initial_pid_file)?, original_pid); |
| assert_eq!(initial_current.canonicalize()?, original); |
| if initial == InitialDaemon::Legacy { |
| assert!(!current.exists()); |
| } |
| std::fs::write( |
| package.join("bin/codex-code-mode-host"), |
| b"replacement helper", |
| )?; |
| std::fs::set_permissions( |
| package.join("bin/codex-code-mode-host"), |
| std::fs::Permissions::from_mode(0o755), |
| )?; |
| let replaced = daemon |
| .command() |
| .args(["app-server", "daemon", "update", "--from-cli", "--yes"]) |
| .output()?; |
| ensure!( |
| replaced.status.success(), |
| "{}", |
| String::from_utf8_lossy(&replaced.stderr) |
| ); |
| let output: Value = serde_json::from_slice(&replaced.stdout)?; |
| assert_eq!(output["status"], "updated"); |
| assert_ne!(daemon.pid("daemon.pid")?, original_pid); |
| wait_for_exit(original_pid)?; |
| if let Some(pid) = legacy_updater { |
| wait_for_exit(pid)?; |
| } |
| assert!(!state.join("app-server.pid").exists()); |
| assert!(!state.join("app-server-updater.pid").exists()); |
| assert_eq!( |
| output["managedCodexPath"], |
| current |
| .join("bin/codex") |
| .to_str() |
| .context("managed path is not UTF-8")? |
| ); |
| assert_ne!(current.canonicalize()?, original); |
| assert!(!dedicated.join("auto-update-version").exists()); |
| assert_eq!(std::fs::read(state.join("settings.json"))?, settings_before); |
| assert_eq!( |
| std::fs::read(current.join("bin/codex-code-mode-host"))?, |
| b"replacement helper" |
| ); |
| if initial == InitialDaemon::Missing { |
| assert_eq!( |
| std::fs::read(original.join("bin/codex-code-mode-host"))?, |
| b"runtime fixture" |
| ); |
| } |
| assert_eq!(daemon.lifecycle("start")?["status"], "alreadyRunning"); |
| assert_eq!(daemon.lifecycle("stop")?["status"], "stopped"); |
| assert_eq!(standalone.join("current").canonicalize()?, cli_selection); |
| } |
| if action == "bootstrap" { |
| assert_eq!(output["autoUpdateEnabled"], false); |
| } |
| Ok(()) |
| } |
|
|
| #[test] |
| fn packaged_daemon_start_and_explicit_replacement() -> Result<()> { |
| packaged_daemon_launch("start", InitialDaemon::Missing) |
| } |
|
|
| #[test] |
| fn packaged_daemon_restart_seeds_local_package() -> Result<()> { |
| packaged_daemon_launch("restart", InitialDaemon::Missing) |
| } |
|
|
| #[test] |
| fn packaged_daemon_bootstrap_seeds_local_package() -> Result<()> { |
| packaged_daemon_launch("bootstrap", InitialDaemon::Missing) |
| } |
|
|
| #[test] |
| fn packaged_daemon_explicit_replacement_migrates_running_legacy() -> Result<()> { |
| packaged_daemon_launch("start", InitialDaemon::Legacy) |
| } |
|
|