Files
codex/codex-rs/app-server-daemon/src/update_loop_tests.rs
T
Eric Traut adee0b04fa Preserve standalone release pins during daemon updates (#43521)
## Why

Bootstrapping the app-server daemon should preserve an explicitly selected release, even when that version is currently `latest`. Older managed binaries should also be able to serve app-server without supporting the updater command.

## What changed

- Record `latest` selections in `auto-update-version` in both standalone installers and clear the marker for explicit releases.
- Start the daemon updater only for a marked stable release whose binary supports `pid-update-loop`. Existing installs without a marker require a new `latest` installation to enable automatic updates.
- Recheck the selected release under the install lock so an in-flight update cannot overwrite a new pin, including installer calls from older updaters. Recheck selection before restarting app-server or replacing the updater.
- Cancel Unix installer process groups and clean up their owned fallback locks when the updater stops.

## Testing

Add coverage for channel markers, explicit pins of the current latest version, older updater guards, updater command support, and Unix installer cancellation with child-process and lock cleanup.

GitOrigin-RevId: 4d275237bd77d896bf64dc1b85a5bca3608142c7
2026-09-07 17:18:02 +00:00

170 lines
5.2 KiB
Rust

use std::sync::Mutex;
#[cfg(unix)]
use std::time::Duration;
use pretty_assertions::assert_eq;
use super::INSTALL_URL;
use super::InstallerHttp;
use super::InstallerResponse;
use super::fetch_installer_script;
use super::update_modes_for_identities;
use crate::RestartMode;
use crate::UpdaterRefreshMode;
use crate::managed_install::executable_identity_from_bytes;
#[test]
fn unchanged_updater_uses_version_based_restart() {
assert_eq!(
update_modes_for_identities(
&executable_identity_from_bytes(b"same"),
&executable_identity_from_bytes(b"same"),
),
(RestartMode::IfVersionChanged, UpdaterRefreshMode::None)
);
}
#[test]
fn changed_updater_forces_refresh_even_when_version_may_match() {
assert_eq!(
update_modes_for_identities(
&executable_identity_from_bytes(b"old"),
&executable_identity_from_bytes(b"new"),
),
(
RestartMode::Always,
UpdaterRefreshMode::ReexecIfManagedBinaryChanged,
)
);
}
#[tokio::test]
async fn installer_fetch_uses_exact_url_and_preserves_bytes() {
let script = b"#!/bin/sh\nprintf 'update bytes'\n".to_vec();
let http = FakeInstallerHttp::new(InstallerResponse::Success(script.clone()));
assert_eq!(
fetch_installer_script(&http)
.await
.expect("installer fetch should succeed"),
script
);
assert_eq!(http.requested_urls(), vec![INSTALL_URL.to_string()]);
}
#[tokio::test]
async fn installer_fetch_rejects_non_success_status() {
let http = FakeInstallerHttp::new(InstallerResponse::Unsuccessful { status: 503 });
let error = fetch_installer_script(&http)
.await
.expect_err("non-success response should fail");
assert!(error.to_string().contains("503"));
assert_eq!(http.requested_urls(), vec![INSTALL_URL.to_string()]);
}
struct FakeInstallerHttp {
response: InstallerResponse,
requested_urls: Mutex<Vec<String>>,
}
impl FakeInstallerHttp {
fn new(response: InstallerResponse) -> Self {
Self {
response,
requested_urls: Mutex::new(Vec::new()),
}
}
fn requested_urls(&self) -> Vec<String> {
self.requested_urls
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
}
}
impl InstallerHttp for FakeInstallerHttp {
async fn get(&self, url: &str) -> anyhow::Result<InstallerResponse> {
self.requested_urls
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(url.to_string());
Ok(self.response.clone())
}
}
#[cfg(unix)]
#[tokio::test]
async fn cancelling_installer_stops_children_and_releases_fallback_lock() {
let home = tempfile::TempDir::new().expect("home");
let ready = home.path().join("ready");
let delayed = home.path().join("delayed");
let lock = home.path().join("packages/standalone/install.lock.d");
let script = format!(
"mkdir -p '{lock}'\necho $$ > '{lock}/pid'\n(trap '' TERM; echo ready > '{ready}'; sleep 4; echo late > '{delayed}') &\nwait\n",
lock = lock.display(),
ready = ready.display(),
delayed = delayed.display(),
);
let (cancel, cancelled) = tokio::sync::oneshot::channel();
let ready_for_signal = ready.clone();
let signal_sender = tokio::spawn(async move {
let deadline = tokio::time::Instant::now() + Duration::from_secs(5);
while !ready_for_signal.exists() {
assert!(
tokio::time::Instant::now() < deadline,
"installer did not start"
);
tokio::time::sleep(Duration::from_millis(20)).await;
}
cancel.send(()).expect("cancel installer");
});
let result = tokio::time::timeout(
Duration::from_secs(10),
super::run_installer_script(script.as_bytes(), "0.150.0-test", home.path(), async {
cancelled.await.ok()
}),
)
.await
.expect("installer cancellation timed out")
.expect("installer cancellation failed");
signal_sender.await.expect("signal sender");
assert!(matches!(result, super::UpdateLoopControl::Stop));
assert!(!lock.exists());
tokio::time::sleep(Duration::from_secs(3)).await;
assert!(!delayed.exists());
}
#[cfg(windows)]
#[tokio::test]
async fn powershell_installer_is_noninteractive_and_reports_script_failure() {
let valid = FakeInstallerHttp::new(InstallerResponse::Success(
br#"
function Test-Installer {
if ($env:CODEX_NON_INTERACTIVE -ne '1') { throw 'interactive installer' }
}
Test-Installer
"#
.to_vec(),
));
let script = super::fetch_installer_script(&valid)
.await
.expect("fetch installer");
super::run_installer_script(&script, "0.150.0-x86_64-pc-windows-msvc")
.await
.expect("installer succeeds");
let failing = FakeInstallerHttp::new(InstallerResponse::Success(
b"throw 'installer failed'".to_vec(),
));
let script = super::fetch_installer_script(&failing)
.await
.expect("fetch failing installer");
assert!(
super::run_installer_script(&script, "0.150.0-x86_64-pc-windows-msvc")
.await
.is_err()
);
}